├── Resources
├── caution.wav
├── toggle_on.wav
├── celebration.wav
├── piano
│ ├── small_A.mp3
│ ├── small_B.mp3
│ ├── small_C.mp3
│ ├── small_D.mp3
│ ├── small_E.mp3
│ ├── small_F.mp3
│ ├── small_G.mp3
│ ├── oneLine_A.mp3
│ ├── oneLine_B.mp3
│ ├── oneLine_C.mp3
│ ├── oneLine_D.mp3
│ ├── oneLine_E.mp3
│ ├── oneLine_F.mp3
│ ├── oneLine_G.mp3
│ ├── twoLine_A.mp3
│ ├── twoLine_B.mp3
│ ├── twoLine_C.mp3
│ ├── twoLine_D.mp3
│ ├── twoLine_E.mp3
│ ├── twoLine_F.mp3
│ ├── twoLine_G.mp3
│ ├── small_A_Sharp.mp3
│ ├── small_C_Sharp.mp3
│ ├── small_D_Sharp.mp3
│ ├── small_F_Sharp.mp3
│ ├── small_G_Sharp.mp3
│ ├── oneLine_A_Sharp.mp3
│ ├── oneLine_C_Sharp.mp3
│ ├── oneLine_D_Sharp.mp3
│ ├── oneLine_F_Sharp.mp3
│ ├── oneLine_G_Sharp.mp3
│ ├── twoLine_A_Sharp.mp3
│ ├── twoLine_C_Sharp.mp3
│ ├── twoLine_D_Sharp.mp3
│ ├── twoLine_F_Sharp.mp3
│ └── twoLine_G_Sharp.mp3
├── Assets.xcassets
│ ├── Contents.json
│ ├── Colors
│ │ ├── Contents.json
│ │ ├── DarkGray.colorset
│ │ │ └── Contents.json
│ │ ├── UIGray.colorset
│ │ │ └── Contents.json
│ │ ├── DisplayColor.colorset
│ │ │ └── Contents.json
│ │ ├── DarkerRed.colorset
│ │ │ └── Contents.json
│ │ └── LinkColor.colorset
│ │ │ └── Contents.json
│ ├── KeyImages
│ │ ├── Contents.json
│ │ ├── black.imageset
│ │ │ ├── black.png
│ │ │ └── Contents.json
│ │ ├── white_l_cutout.imageset
│ │ │ ├── white_l_cutout.png
│ │ │ └── Contents.json
│ │ ├── white_r_cutout.imageset
│ │ │ ├── white_r_cutout.png
│ │ │ └── Contents.json
│ │ └── white_lr_cutout.imageset
│ │ │ ├── white_lr_cutout.png
│ │ │ └── Contents.json
│ └── AppIcon.appiconset
│ │ ├── AppIcon-2.png
│ │ └── Contents.json
└── LICENSE.md
├── .swiftpm
├── playgrounds
│ ├── DocumentThumbnail.png
│ ├── Workspace.plist
│ ├── DocumentThumbnail.plist
│ └── CachedManifest.plist
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ ├── xcuserdata
│ │ └── henri.xcuserdatad
│ │ │ └── UserInterfaceState.xcuserstate
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ └── henri.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── Model
├── Octave.swift
├── Key.swift
├── Note.swift
└── MenuState.swift
├── MyApp.swift
├── View
├── UpperArea
│ ├── DisplayMenu
│ │ ├── DisplayShapeView.swift
│ │ ├── HelpLabelView.swift
│ │ └── HeaderView.swift
│ ├── AboveKeyboardButtonView.swift
│ ├── AboveKeyboardButtonBar.swift
│ ├── MenuButtonView.swift
│ └── MusicNotation
│ │ ├── NoteShape.swift
│ │ ├── NoteSheetView.swift
│ │ └── ViolinClefShape.swift
├── MusicKeyboard
│ ├── KeyView.swift
│ └── OctaveView.swift
└── ContentView.swift
├── Helper
└── SoundEngine.swift
├── Package.swift
├── README.md
└── ViewModel
└── AppState.swift
/Resources/caution.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/caution.wav
--------------------------------------------------------------------------------
/Resources/toggle_on.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/toggle_on.wav
--------------------------------------------------------------------------------
/Resources/celebration.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/celebration.wav
--------------------------------------------------------------------------------
/Resources/piano/small_A.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_A.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_B.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_B.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_C.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_C.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_D.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_D.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_E.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_E.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_F.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_F.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_G.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_G.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_A.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_A.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_B.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_B.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_C.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_C.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_D.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_D.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_E.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_E.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_F.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_F.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_G.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_G.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_A.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_A.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_B.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_B.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_C.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_C.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_D.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_D.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_E.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_E.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_F.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_F.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_G.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_G.mp3
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Resources/piano/small_A_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_A_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_C_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_C_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_D_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_D_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_F_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_F_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/small_G_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/small_G_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Colors/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/Resources/piano/oneLine_A_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_A_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_C_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_C_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_D_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_D_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_F_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_F_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/oneLine_G_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/oneLine_G_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_A_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_A_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_C_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_C_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_D_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_D_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_F_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_F_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/piano/twoLine_G_Sharp.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/piano/twoLine_G_Sharp.mp3
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/playgrounds/DocumentThumbnail.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/.swiftpm/playgrounds/DocumentThumbnail.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/Assets.xcassets/AppIcon.appiconset/AppIcon-2.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/black.imageset/black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/Assets.xcassets/KeyImages/black.imageset/black.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/white_l_cutout.imageset/white_l_cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/Assets.xcassets/KeyImages/white_l_cutout.imageset/white_l_cutout.png
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/white_r_cutout.imageset/white_r_cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/Assets.xcassets/KeyImages/white_r_cutout.imageset/white_r_cutout.png
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/white_lr_cutout.imageset/white_lr_cutout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/Resources/Assets.xcassets/KeyImages/white_lr_cutout.imageset/white_lr_cutout.png
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcuserdata/henri.xcuserdatad/UserInterfaceState.xcuserstate:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/henribredt/E-Piano-WWDC23/HEAD/.swiftpm/xcode/package.xcworkspace/xcuserdata/henri.xcuserdatad/UserInterfaceState.xcuserstate
--------------------------------------------------------------------------------
/Model/Octave.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum Octave: String, CaseIterable, Identifiable {
4 | case small = "small"
5 | case oneLine = "oneLine"
6 | case twoLine = "twoLine"
7 |
8 | var id: String { return self.rawValue }
9 | }
10 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/black.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "black.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/white_l_cutout.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "white_l_cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/white_r_cutout.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "white_r_cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/KeyImages/white_lr_cutout.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "white_lr_cutout.png",
5 | "idiom" : "universal"
6 | }
7 | ],
8 | "info" : {
9 | "author" : "xcode",
10 | "version" : 1
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/MyApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @available(iOS 16.0, *)
4 | @main
5 | struct MyApp: App {
6 | @StateObject var appState = AppState.shared
7 |
8 | var body: some Scene {
9 | WindowGroup {
10 | ContentView()
11 | .environmentObject(appState)
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "AppIcon-2.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/Model/Key.swift:
--------------------------------------------------------------------------------
1 | //
2 | // File.swift
3 | //
4 | //
5 | // Created by Henri Bredt on 18.04.23.
6 | //
7 |
8 | import Foundation
9 |
10 | enum Key: String {
11 | case white_r_cutout = "white_r_cutout"
12 | case white_l_cutout = "white_l_cutout"
13 | case white_lr_cutout = "white_lr_cutout"
14 | case black = "black"
15 | }
16 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Colors/DarkGray.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "extended-gray",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "white" : "0.150"
9 | }
10 | },
11 | "idiom" : "universal"
12 | }
13 | ],
14 | "info" : {
15 | "author" : "xcode",
16 | "version" : 1
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/.swiftpm/playgrounds/Workspace.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AppSettings
6 |
7 | appIconPlaceholderGlyphName
8 | note
9 | appSettingsVersion
10 | 1
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Colors/UIGray.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.780",
9 | "green" : "0.780",
10 | "red" : "0.780"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Colors/DisplayColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.682",
9 | "green" : "0.765",
10 | "red" : "0.722"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Colors/DarkerRed.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.134",
9 | "green" : "0.224",
10 | "red" : "0.798"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Resources/Assets.xcassets/Colors/LinkColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "display-p3",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.338",
9 | "green" : "0.374",
10 | "red" : "0.356"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Resources/LICENSE.md:
--------------------------------------------------------------------------------
1 | # LICENSE
2 |
3 | This project utilises the following assets
4 |
5 | - Piano Sounds: https://github.com/fuhton/piano-mp3 (MIT License)
6 | - Button Sounds: https://snd.dev/# SND01 "sine" by Yasuhiro Tsuchiya (Free for Personal and Commercial use)
7 |
8 | While creating the following tools we're of great help
9 | - SVG to SwiftUI Shape converter: https://svg-to-swiftui.quassum.com/
10 |
11 | Created by Henri Bredt as submission to the Apple Swift Student Challenge 2023
12 |
--------------------------------------------------------------------------------
/.swiftpm/playgrounds/DocumentThumbnail.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | DocumentThumbnailConfiguration
6 |
7 | accentColorHash
8 |
9 | Fkd2iMDgBpnGz6RJejYS1+g8UyBitkslD+2JCBKO1Ug=
10 |
11 | appIconHash
12 |
13 | gNZ0BmypKE4s6QFCTUxPCRffDom3FfInMdHJgnqBRYE=
14 |
15 | thumbnailIsPrerendered
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/View/UpperArea/DisplayMenu/DisplayShapeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView 2.swift
3 | //
4 | //
5 | // Created by Henri Bredt on 17.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(iOS 16.0, *)
11 | struct DisplayShapeView: View {
12 | let geo: GeometryProxy
13 | let upperGeo: GeometryProxy
14 |
15 | var body: some View {
16 | Rectangle()
17 | .fill(
18 | Color("DisplayColor")
19 | .shadow(.inner(radius: 2, x: 10, y: 15))
20 | .shadow(.inner(radius: 6, x: -1, y: -1))
21 |
22 | )
23 | .frame(width: geo.size.width/1.8, height: upperGeo.size.height/1.2)
24 | .cornerRadius(6)
25 | .shadow(radius: 5)
26 | .overlay(
27 | RoundedRectangle(cornerRadius: 8)
28 | .stroke(Color("DarkGray"), lineWidth: 2)
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/henri.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | E-Piano-WWDC23.swiftpm-Package.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 1
11 |
12 | E-Piano-WWDC23.xcscheme_^#shared#^_
13 |
14 | orderHint
15 | 0
16 |
17 | WWDC23.xcscheme_^#shared#^_
18 |
19 | orderHint
20 | 0
21 |
22 |
23 | SuppressBuildableAutocreation
24 |
25 | AppModule
26 |
27 | primary
28 |
29 |
30 | E-Piano-WWDC23
31 |
32 | primary
33 |
34 |
35 | E-Piano-WWDC23.swiftpm
36 |
37 | primary
38 |
39 |
40 | WWDC23
41 |
42 | primary
43 |
44 |
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Helper/SoundEngine.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SoundEngine.swift
3 | // WWDC23
4 | //
5 | // Created by Henri Bredt on 16.04.23.
6 | //
7 |
8 | import Foundation
9 | import AVFoundation
10 |
11 | struct SoundEngine {
12 | static var player: AVAudioPlayer?
13 |
14 | private static func playSound(resource: String, type: String) {
15 | guard let path = Bundle.main.path(forResource: resource, ofType: type) else {
16 | print("cant find file")
17 | return
18 | }
19 | let url = URL(fileURLWithPath: path)
20 |
21 | do {
22 | player = try AVAudioPlayer(contentsOf: url)
23 | player!.play()
24 |
25 | } catch let error {
26 | print(error.localizedDescription)
27 | }
28 | }
29 |
30 | static func buttonSound() {
31 | player?.stop()
32 | playSound(resource: "toggle_on", type: "wav")
33 | }
34 |
35 | static func playCelebration() {
36 | player?.stop()
37 | playSound(resource: "celebration", type: "wav")
38 | }
39 |
40 | static func play(note: Note){
41 | player?.stop()
42 | playSound(resource: note.rawValue, type: "mp3")
43 |
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/View/UpperArea/AboveKeyboardButtonView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct AboveKeyboardButtonView: View {
4 | @Binding var sizeDivisor: CGFloat
5 | let icon: String
6 | let action: () -> ()
7 |
8 | var body: some View {
9 | Button {
10 | action()
11 | SoundEngine.buttonSound()
12 | } label: {
13 | Rectangle()
14 | .fill(LinearGradient(
15 | gradient: .init(colors: [Color.white, Color("UIGray")]),
16 | startPoint: .init(x: 0.5, y: 0),
17 | endPoint: .init(x: 0.5, y: 0.6)
18 | ))
19 | .cornerRadius(5)
20 | .overlay(
21 | RoundedRectangle(cornerRadius: 5)
22 | .stroke(Color("DarkGray"), lineWidth: 2)
23 | )
24 | .overlay(
25 | Image(systemName: icon)
26 | .foregroundColor(Color("DarkGray"))
27 | .font(.title3)
28 | .padding(EdgeInsets(top: 6, leading: 18, bottom: 6, trailing: 18))
29 | )
30 | .frame(width: 90)
31 | .padding(.vertical, 10)
32 | .padding(.trailing, 6)
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.6
2 |
3 | // WARNING:
4 | // This file is automatically generated.
5 | // Do not edit it by hand because the contents will be replaced.
6 |
7 | import PackageDescription
8 | import AppleProductTypes
9 |
10 | let package = Package(
11 | name: "E-Piano-WWDC23",
12 | platforms: [
13 | .iOS("15.2")
14 | ],
15 | products: [
16 | .iOSApplication(
17 | name: "E-Piano-WWDC23",
18 | targets: ["AppModule"],
19 | bundleIdentifier: "de.henribredt.WWDC23",
20 | teamIdentifier: "8V3J763Y9A",
21 | displayVersion: "1.0",
22 | bundleVersion: "1",
23 | appIcon: .asset("AppIcon"),
24 | accentColor: .presetColor(.blue),
25 | supportedDeviceFamilies: [
26 | .pad,
27 | .phone
28 | ],
29 | supportedInterfaceOrientations: [
30 | .landscapeRight,
31 | .landscapeLeft,
32 | ],
33 | appCategory: .education
34 | )
35 | ],
36 | targets: [
37 | .executableTarget(
38 | name: "AppModule",
39 | path: ".",
40 | resources: [
41 | .process("Resources")
42 | ]
43 | )
44 | ]
45 | )
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # E-Piano-WWDC23
2 | ### A Swift Student Challenge Winner 2023 🎉
3 | A fun, retro-syle e-Piano that teaches how to read notes and play the piano. Created as a Swift Student Challenge 2023 Submission by [Henri Bredt](https://henribredt.de) in April 2023.
4 |
5 | **Watch the [Demo Video](https://youtu.be/0ZGPRZ1uUi0) on Youtube**
6 |
7 | 
8 |
9 | ## Features
10 | * Free piano play
11 | * Interactive note reading and piano playing course
12 | * Help mode highlighting the next correct key
13 | * Scrollable and resizable keyboard
14 | * Help labels
15 | * Retro look-and-feel
16 |
17 | ## Installation
18 | I'll published the app in the future on the App Store, until then you can clone this repository and run it on your iPad with the Swift Playgrounds app.
19 | If you clone this project from GitHub, you'll need to add **.swiftpm** to the enclosing folder in order to be able to open this project in Xcode or Swift Playgrouds.
20 |
21 | ## Credtis
22 |
23 | - Piano Sounds: https://github.com/fuhton/piano-mp3 (MIT License)
24 | - Button Sounds: https://snd.dev/# SND01 "sine" by Yasuhiro Tsuchiya (Free for Personal and Commercial use)
25 | - SVG to SwiftUI Shape converter: https://svg-to-swiftui.quassum.com/
26 |
--------------------------------------------------------------------------------
/View/UpperArea/DisplayMenu/HelpLabelView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Henri Bredt on 17.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @available(iOS 16.0, *)
11 | struct HelpLabelView: View {
12 | @EnvironmentObject var appState: AppState
13 |
14 | var body: some View {
15 | HStack(spacing: 10){
16 | if appState.currentMenuState.helpPlayNotesLabel {
17 | Text("Play all notes to continue.")
18 | }
19 | Text("Press")
20 | Text(appState.currentMenuState.helpLabel)
21 | .font(.system(.caption2, design: .monospaced, weight: .medium))
22 | .lineLimit(1)
23 | .allowsTightening(true)
24 | .padding(EdgeInsets(top: 3, leading: 9, bottom: 3, trailing: 9))
25 | .overlay{
26 | RoundedRectangle(cornerRadius: 100)
27 | .stroke(Color("DarkGray"), lineWidth: 1.2)
28 |
29 | }
30 | Text(appState.currentMenuState.helpDescription)
31 | }
32 | .font(.system(.footnote, design: .monospaced))
33 | }
34 | }
35 |
36 | @available(iOS 16.0, *)
37 | struct HelpLabelView_Previews: PreviewProvider {
38 | static var previews: some View {
39 | @StateObject var appState = AppState()
40 | HelpLabelView()
41 | .environmentObject(appState)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/View/UpperArea/AboveKeyboardButtonBar.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | /// sits right above the keyboard, features resizing buttons and product labels
4 | struct AboveKeyboardButtonBar: View {
5 | let geo: GeometryProxy
6 | @Binding var sizeDivisor: CGFloat // required for childs
7 |
8 | var body: some View {
9 | HStack(spacing: 10){
10 | VStack(alignment: .leading, spacing: 3){
11 | Text("ePianighi \"Craig 23\"").bold()
12 | Text("Educational E-Piano")
13 | }
14 | .font(.system(.footnote, design: .monospaced))
15 | .foregroundColor(Color("DarkGray"))
16 | .padding(.leading)
17 | Spacer()
18 |
19 | AboveKeyboardButtonView(sizeDivisor: $sizeDivisor, icon: "info.circle") {
20 | AppState.shared.showKeyboardHelpLabels.toggle()
21 | }
22 |
23 | AboveKeyboardButtonView(sizeDivisor: $sizeDivisor, icon: "minus.magnifyingglass") {
24 | sizeDivisor += 0.5
25 | }
26 | .disabled(sizeDivisor >= 6.5)
27 |
28 | AboveKeyboardButtonView(sizeDivisor: $sizeDivisor, icon: "plus.magnifyingglass") {
29 | sizeDivisor -= 0.5
30 | }
31 | .disabled(sizeDivisor <= 4)
32 | .padding(.trailing, 9)
33 | }
34 | .frame(width: geo.size.width, height: 60)
35 | .background(){
36 | Rectangle()
37 | .fill(Color("UIGray"))
38 | .shadow(color: .red.opacity(0.5), radius: 5, y: 10)
39 | .zIndex(1)
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/View/UpperArea/MenuButtonView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | struct MenuButtonView: View {
4 | let label: String
5 | let isPowerButton: Bool
6 | let action: () -> ()
7 |
8 | var body: some View {
9 | HStack{
10 | Button {
11 | action()
12 | } label: {
13 | Rectangle()
14 | .fill(LinearGradient(
15 | gradient: .init(colors: [Color.white, Color("UIGray")]),
16 | startPoint: .init(x: 0.5, y: 0),
17 | endPoint: .init(x: 0.5, y: 0.6)
18 | ))
19 | .cornerRadius(100)
20 | .frame(width: 90, height: 30)
21 | .overlay(
22 | RoundedRectangle(cornerRadius: 100)
23 | .stroke(Color("DarkGray"), lineWidth: 2)
24 | )
25 | .overlay {
26 | if isPowerButton {
27 | RoundedRectangle(cornerRadius: 100)
28 | .frame(width: 55, height: 6)
29 | .foregroundColor(AppState.shared.isOn ? Color("DarkerRed").opacity(0.8) : .gray.opacity(0.9))
30 | .shadow(color: Color("DarkerRed").opacity(AppState.shared.isOn ? 0.7 : 0.0), radius: 3)
31 | }
32 | }
33 | }
34 | Text(label.uppercased())
35 | .font(.system(.footnote, design: .monospaced))
36 | .foregroundColor(Color("DarkGray"))
37 | .padding(.leading)
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/View/UpperArea/MusicNotation/NoteShape.swift:
--------------------------------------------------------------------------------
1 |
2 | // created with https://svg-to-swiftui.quassum.com/
3 |
4 | import SwiftUI
5 |
6 | struct NoteShape: Shape {
7 | let helpLine: Bool
8 | func path(in rect: CGRect) -> Path {
9 | var path = Path()
10 | let width = rect.size.width
11 | let height = rect.size.height
12 | path.move(to: CGPoint(x: 0.496*width, y: 0))
13 | path.addCurve(to: CGPoint(x: 0.896*width, y: 0.5*height), control1: CGPoint(x: 0.71691*width, y: 0), control2: CGPoint(x: 0.896*width, y: 0.22386*height))
14 | path.addCurve(to: CGPoint(x: 0.496*width, y: height), control1: CGPoint(x: 0.896*width, y: 0.77614*height), control2: CGPoint(x: 0.71691*width, y: height))
15 | path.addCurve(to: CGPoint(x: 0.096*width, y: 0.5*height), control1: CGPoint(x: 0.27509*width, y: height), control2: CGPoint(x: 0.096*width, y: 0.77614*height))
16 | path.addCurve(to: CGPoint(x: 0.496*width, y: 0), control1: CGPoint(x: 0.096*width, y: 0.22386*height), control2: CGPoint(x: 0.27509*width, y: 0))
17 | path.closeSubpath()
18 | path.move(to: CGPoint(x: 0.41812*width, y: 0.1437*height))
19 | path.addCurve(to: CGPoint(x: 0.35006*width, y: 0.58978*height), control1: CGPoint(x: 0.33886*width, y: 0.19328*height), control2: CGPoint(x: 0.30839*width, y: 0.393*height))
20 | path.addCurve(to: CGPoint(x: 0.56903*width, y: 0.8563*height), control1: CGPoint(x: 0.39173*width, y: 0.78656*height), control2: CGPoint(x: 0.48977*width, y: 0.90588*height))
21 | path.addCurve(to: CGPoint(x: 0.63709*width, y: 0.41022*height), control1: CGPoint(x: 0.64829*width, y: 0.80672*height), control2: CGPoint(x: 0.67876*width, y: 0.607*height))
22 | path.addCurve(to: CGPoint(x: 0.41812*width, y: 0.1437*height), control1: CGPoint(x: 0.59542*width, y: 0.21344*height), control2: CGPoint(x: 0.49739*width, y: 0.09412*height))
23 | path.closeSubpath()
24 | if helpLine {
25 | path.addRect(CGRect(x: 0, y: 0.43448*height, width: width, height: 0.12414*height))
26 | }
27 | return path
28 | }
29 | }
30 |
31 |
--------------------------------------------------------------------------------
/.swiftpm/playgrounds/CachedManifest.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CachedManifest
6 |
7 | manifestData
8 |
9 | eyJkZXBlbmRlbmNpZXMiOltdLCJkaXNwbGF5TmFtZSI6IkUtUGlhbm8tV1dE
10 | QzIzIiwicGFja2FnZUtpbmQiOnsicm9vdCI6e319LCJwbGF0Zm9ybXMiOlt7
11 | Im9wdGlvbnMiOltdLCJwbGF0Zm9ybU5hbWUiOiJpb3MiLCJ2ZXJzaW9uIjoi
12 | MTUuMiJ9XSwicHJvZHVjdHMiOlt7Im5hbWUiOiJFLVBpYW5vLVdXREMyMyIs
13 | InNldHRpbmdzIjpbeyJidW5kbGVJZGVudGlmaWVyIjpbImRlLmhlbnJpYnJl
14 | ZHQuV1dEQzIzIl19LHsidGVhbUlkZW50aWZpZXIiOlsiOFYzSjc2M1k5QSJd
15 | fSx7ImRpc3BsYXlWZXJzaW9uIjpbIjEuMCJdfSx7ImJ1bmRsZVZlcnNpb24i
16 | OlsiMSJdfSx7ImlPU0FwcEluZm8iOlt7ImFjY2VudENvbG9yIjp7InByZXNl
17 | dENvbG9yIjp7InByZXNldENvbG9yIjp7InJhd1ZhbHVlIjoiYmx1ZSJ9fX0s
18 | ImFwcENhdGVnb3J5Ijp7InJhd1ZhbHVlIjoicHVibGljLmFwcC1jYXRlZ29y
19 | eS5lZHVjYXRpb24ifSwiYXBwSWNvbiI6eyJwbGFjZWhvbGRlciI6eyJpY29u
20 | Ijp7InJhd1ZhbHVlIjoibm90ZSJ9fX0sImNhcGFiaWxpdGllcyI6W10sInN1
21 | cHBvcnRlZERldmljZUZhbWlsaWVzIjpbInBhZCIsInBob25lIl0sInN1cHBv
22 | cnRlZEludGVyZmFjZU9yaWVudGF0aW9ucyI6W3sicG9ydHJhaXQiOnt9fSx7
23 | ImxhbmRzY2FwZVJpZ2h0Ijp7fX0seyJsYW5kc2NhcGVMZWZ0Ijp7fX0seyJw
24 | b3J0cmFpdFVwc2lkZURvd24iOnsiY29uZGl0aW9uIjp7ImRldmljZUZhbWls
25 | aWVzIjpbInBhZCJdfX19XX1dfV0sInRhcmdldHMiOlsiQXBwTW9kdWxlIl0s
26 | InR5cGUiOnsiZXhlY3V0YWJsZSI6bnVsbH19XSwidGFyZ2V0TWFwIjp7IkFw
27 | cE1vZHVsZSI6eyJkZXBlbmRlbmNpZXMiOltdLCJleGNsdWRlIjpbXSwibmFt
28 | ZSI6IkFwcE1vZHVsZSIsInBhdGgiOiIuIiwicmVzb3VyY2VzIjpbeyJwYXRo
29 | IjoiUmVzb3VyY2VzIiwicnVsZSI6eyJwcm9jZXNzIjp7fX19XSwic2V0dGlu
30 | Z3MiOltdLCJ0eXBlIjoiZXhlY3V0YWJsZSJ9fSwidGFyZ2V0cyI6W3siZGVw
31 | ZW5kZW5jaWVzIjpbXSwiZXhjbHVkZSI6W10sIm5hbWUiOiJBcHBNb2R1bGUi
32 | LCJwYXRoIjoiLiIsInJlc291cmNlcyI6W3sicGF0aCI6IlJlc291cmNlcyIs
33 | InJ1bGUiOnsicHJvY2VzcyI6e319fV0sInNldHRpbmdzIjpbXSwidHlwZSI6
34 | ImV4ZWN1dGFibGUifV0sInRvb2xzVmVyc2lvbiI6eyJfdmVyc2lvbiI6IjUu
35 | Ni4wIn19
36 |
37 | manifestHash
38 |
39 | o09OC+yrVntBK/jGv2G+J0b+GHN3q0Q4+S+37tywUkM=
40 |
41 | schemaVersion
42 | 4
43 | swiftPMVersionString
44 | 5.8.0
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/Model/Note.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum Note: String, Identifiable {
4 | case small_C = "small_C",
5 | small_D = "small_D",
6 | small_E = "small_E",
7 | small_F = "small_F",
8 | small_G = "small_G",
9 | small_A = "small_A",
10 | small_B = "small_B",
11 | small_C_Sharp = "small_C_Sharp",
12 | small_D_Sharp = "small_D_Sharp",
13 | small_F_Sharp = "small_F_Sharp",
14 | small_G_Sharp = "small_G_Sharp",
15 | small_A_Sharp = "small_A_Sharp"
16 |
17 | case oneLine_C = "oneLine_C",
18 | oneLine_D = "oneLine_D",
19 | oneLine_E = "oneLine_E",
20 | oneLine_F = "oneLine_F",
21 | oneLine_G = "oneLine_G",
22 | oneLine_A = "oneLine_A",
23 | oneLine_B = "oneLine_B",
24 | oneLine_C_Sharp = "oneLine_C_Sharp",
25 | oneLine_D_Sharp = "oneLine_D_Sharp",
26 | oneLine_F_Sharp = "oneLine_F_Sharp",
27 | oneLine_G_Sharp = "oneLine_G_Sharp",
28 | oneLine_A_Sharp = "oneLine_A_Sharp"
29 |
30 | case twoLine_C = "twoLine_C",
31 | twoLine_D = "twoLine_D",
32 | twoLine_E = "twoLine_E",
33 | twoLine_F = "twoLine_F",
34 | twoLine_G = "twoLine_G",
35 | twoLine_A = "twoLine_A",
36 | twoLine_B = "twoLine_B",
37 | twoLine_C_Sharp = "twoLine_C_Sharp",
38 | twoLine_D_Sharp = "twoLine_D_Sharp",
39 | twoLine_F_Sharp = "twoLine_F_Sharp",
40 | twoLine_G_Sharp = "twoLine_G_Sharp",
41 | twoLine_A_Sharp = "twoLine_A_Sharp"
42 |
43 | var id: String { return self.rawValue }
44 |
45 | func getHelpLabel() -> String {
46 | var helpText = self.rawValue
47 | helpText = helpText.replacingOccurrences(of: "_Sharp", with: "", options: [.caseInsensitive, .regularExpression])
48 | helpText = helpText.replacingOccurrences(of: "twoLine_", with: "", options: [.caseInsensitive, .regularExpression])
49 | helpText = helpText.replacingOccurrences(of: "oneLine_", with: "", options: [.caseInsensitive, .regularExpression])
50 | helpText = helpText.replacingOccurrences(of: "small_", with: "", options: [.caseInsensitive, .regularExpression])
51 |
52 | if self.rawValue.contains("Sharp") {
53 | helpText.append("#")
54 | }
55 |
56 | return helpText
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/View/UpperArea/DisplayMenu/HeaderView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Henri Bredt on 17.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HeaderView: View {
11 | @EnvironmentObject var appState: AppState
12 |
13 | var body: some View {
14 | VStack{
15 | if let headerText = appState.currentMenuState.headerText {
16 | HStack{
17 |
18 | // last played note label
19 | HStack(spacing: 4){
20 | if appState.currentNoteToDisplay != nil {
21 | Image(systemName: "music.note")
22 | .imageScale(.small)
23 | Text(appState.currentNoteToDisplay!.getHelpLabel())
24 | }
25 | Spacer()
26 | }
27 | .frame(width: 50)
28 |
29 | // reset last played note label after one second
30 | .task(id: appState.currentNoteToDisplay){
31 | if appState.currentNoteToDisplay != nil {
32 | DispatchQueue.main.asyncAfter(deadline: .now()+0.35){
33 | appState.currentNoteToDisplay = nil
34 | }
35 | }
36 | }
37 |
38 | Spacer()
39 | Text(headerText)
40 | Spacer()
41 |
42 | HStack(spacing: 4){
43 | Spacer()
44 | if appState.showHelp {
45 | Image(systemName: "eye.fill")
46 | }
47 |
48 | }
49 | .frame(width: 50)
50 | }
51 | .font(.system(.footnote, design: .monospaced))
52 |
53 | Rectangle()
54 | .fill(Color("DarkGray"))
55 | .frame(maxWidth: .infinity, minHeight: 1.5, maxHeight: 1.5)
56 | }
57 | }
58 | }
59 | }
60 |
61 | struct HeaderView_Previews: PreviewProvider {
62 | static var previews: some View {
63 | @StateObject var appState = AppState()
64 | HeaderView()
65 | .environmentObject(appState)
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/ViewModel/AppState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // AppState.swift
3 | // WWDC23
4 | //
5 | // Created by Henri Bredt on 16.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | class AppState: ObservableObject {
11 | public static let shared = AppState()
12 |
13 | // current (last) played note
14 | @Published var currentNote: Note? = nil
15 |
16 | // current (last) played note that should be displayed
17 | // will be reset after a short time for the display to just show it for a short moment, thats why its a separate variable from currentNote, both will have the same note or nil
18 | @Published var currentNoteToDisplay: Note? = nil
19 |
20 | // current menu state to display
21 | @Published var currentMenuState: MenuState = MenuState.flow[1]
22 |
23 | // keyboard is turn on/off
24 | @Published var isOn: Bool = false
25 |
26 | // help mode (highlight the correct next key)
27 | @Published var showHelp: Bool = false
28 |
29 | // used to determine if key help labels and note help labels are shown
30 | @Published var showHelpLabels: Bool = true
31 |
32 | // used to override showHelpLabels for the keyboard
33 | @Published var showKeyboardHelpLabels: Bool = true
34 |
35 | // track practice on note sheet progress
36 | @Published var practiceNotes : [Note]? = nil
37 | @Published var currentPracticeNoteIndex: Int = 0
38 |
39 | // used to track how many wrong answers were given, more than tree answers are wring it will activate help mode
40 | @Published var wrongAnswerCount: Int = 0
41 |
42 | // used to prevent users from manually progressing in the course while the system will automatically do this in 1.5s (if the user then presses the button a lesson would be skipped — so I prevent this)
43 | @Published var blockContinue: Bool = false
44 |
45 | //MARK: functions
46 |
47 | func increaseWrongAnswerCount() {
48 | wrongAnswerCount += 1
49 | if wrongAnswerCount >= 3 {
50 | showHelp = true
51 | }
52 | }
53 |
54 | func reset() {
55 | practiceNotes = nil
56 | currentPracticeNoteIndex = 0
57 | wrongAnswerCount = 0
58 | }
59 |
60 | func currentPracticeNote() -> Note? {
61 | if let notes = practiceNotes {
62 | guard currentPracticeNoteIndex < notes.count else {
63 | print("Index \(currentPracticeNoteIndex) not found in practiceNotes with \(practiceNotes?.count ?? -1) items. Returning nil")
64 | return nil
65 | }
66 | return notes[currentPracticeNoteIndex]
67 | }
68 | return nil
69 | }
70 | }
71 |
72 |
--------------------------------------------------------------------------------
/View/MusicKeyboard/KeyView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // KeyView.swift
3 | // WWDC23
4 | //
5 | // Created by Henri Bredt on 16.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | // Used to compose an Octave for the keyboard
11 | struct KeyView: View {
12 |
13 | @EnvironmentObject var appState: AppState
14 |
15 | let kind: Key
16 | let label: String
17 | let width: CGFloat
18 | let height: CGFloat
19 | let note: Note
20 |
21 | var body: some View {
22 | GeometryReader { geo in
23 | Button {
24 | appState.currentNote = note
25 | appState.currentNoteToDisplay = note
26 | SoundEngine.play(note: note)
27 |
28 | // activate help when the wrong key is pressed repetitively
29 | if appState.currentPracticeNote() != nil{
30 | if appState.currentNote != appState.currentPracticeNote() {
31 | appState.increaseWrongAnswerCount()
32 | } else if appState.wrongAnswerCount >= 3 {
33 | appState.wrongAnswerCount = 0
34 | appState.showHelp = false
35 | } else {
36 | // note was correct, reset wrongAnswerCount count
37 | appState.wrongAnswerCount = 0
38 | }
39 | }
40 | } label: {
41 | ZStack(alignment: .bottom){
42 | Image(kind.rawValue)
43 | .resizable()
44 | .scaledToFit()
45 | if appState.showHelpLabels && appState.showKeyboardHelpLabels {
46 | Text(label)
47 | .padding(.bottom, geo.size.height/9)
48 | .font(kind != .black ? .title3.bold() : .caption.bold())
49 | .foregroundColor(kind != .black ? .black : .white)
50 | .opacity(kind != .black ? 0.15 : 0.35)
51 | }
52 | }
53 | .frame(width: width, height: kind != .black ? height : height/1.85)
54 | .opacity(appState.showHelp && !(appState.currentPracticeNote() == note) ? 0.5 : 1 )
55 | }
56 | }
57 | .frame(width: width)
58 | }
59 | }
60 |
61 | struct KeyView_Previews: PreviewProvider {
62 |
63 | static var previews: some View {
64 | KeyView(kind: .white_r_cutout, label: "C", width: 135, height: 578, note: .small_A)
65 | .frame(width: 135 , height: 578)
66 | .previewLayout(.sizeThatFits)
67 | .previewDisplayName("White key")
68 |
69 | KeyView(kind: .black, label: "C#", width: 135, height: 578, note: .small_C_Sharp)
70 | .frame(width: 135 , height: 578)
71 | .previewLayout(.sizeThatFits)
72 | .previewDisplayName("Black key")
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/View/MusicKeyboard/OctaveView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // OctaveView.swift
3 | // WWDC23
4 | //
5 | // Created by Henri Bredt on 16.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct OctaveView: View {
11 | let drawingHeight: CGFloat
12 | let octave: Octave
13 |
14 | var body: some View {
15 | let width = drawingHeight*(135.0/578.0)
16 |
17 | // white keys
18 | HStack(spacing: 3){
19 | KeyView(kind: .white_r_cutout, label: "C\(getMarker(octave))", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_C")!)
20 | KeyView(kind: .white_lr_cutout, label: "D", width: width, height:drawingHeight, note: Note(rawValue: "\(octave.rawValue)_D")!)
21 | KeyView(kind: .white_l_cutout, label: "E", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_E")!)
22 | KeyView(kind: .white_r_cutout, label: "F", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_F")!)
23 | KeyView(kind: .white_lr_cutout, label: "G", width: width, height:drawingHeight, note: Note(rawValue: "\(octave.rawValue)_G")!)
24 | KeyView(kind: .white_lr_cutout, label: "A", width: width, height:drawingHeight, note: Note(rawValue: "\(octave.rawValue)_A")!)
25 | KeyView(kind: .white_l_cutout, label: "B", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_B")!)
26 | }
27 |
28 | // black keys
29 | .overlay(alignment: .top) {
30 | HStack(spacing: 3) {
31 | KeyView(kind: .black, label: "C#", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_C_Sharp")!)
32 | KeyView(kind: .black, label: "D#", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_D_Sharp")!)
33 | KeyView(kind: .black, label: "NONE", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_C")!)
34 | .opacity(0)
35 | KeyView(kind: .black, label: "F#", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_F_Sharp")!)
36 | KeyView(kind: .black, label: "G#", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_G_Sharp")!)
37 | KeyView(kind: .black, label: "A#", width: width, height: drawingHeight, note: Note(rawValue: "\(octave.rawValue)_A_Sharp")!)
38 | }
39 | }
40 | }
41 |
42 | func getMarker(_ octave: Octave) -> String {
43 | switch octave {
44 | case .small:
45 | return ""
46 | case .oneLine:
47 | return "\'"
48 | case .twoLine:
49 | return "\''"
50 | }
51 | }
52 | }
53 |
54 | struct OctaveView_Previews: PreviewProvider {
55 | static var previews: some View {
56 | OctaveView(drawingHeight: 300, octave: .oneLine)
57 | .padding()
58 | .frame(width: 550, height: 330)
59 | .previewLayout(.sizeThatFits)
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/View/UpperArea/MusicNotation/NoteSheetView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SwiftUIView.swift
3 | //
4 | //
5 | // Created by Henri Bredt on 17.04.23.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct NoteSheetView: View {
11 | @EnvironmentObject var appState: AppState
12 |
13 | let noteHeight: CGFloat
14 | let notes: [Note]
15 |
16 | var body: some View {
17 | lines
18 | .overlay(alignment: .leading){
19 | ViolinClefShape()
20 | .frame(width: noteHeight*7.5, height: noteHeight*7.5*1052/744)
21 | .offset(x: -1.5*noteHeight)
22 | }
23 | .overlay {
24 | HStack(spacing: noteHeight*2){
25 | ForEach(Array(notes.enumerated()), id: \.offset) { index, note in
26 | NoteShape(helpLine: getNoteNeedsHelpLine(note: note))
27 | .offset(y: getNoteOffsetInViolinClef(note: note)*noteHeight)
28 | .aspectRatio(250/145, contentMode: .fill)
29 | .frame(width: noteHeight*145/250, height: noteHeight)
30 | .overlay {
31 | // help label
32 | if appState.showHelpLabels {
33 | Text(note.getHelpLabel())
34 | .offset(y: -3.5*noteHeight)
35 | }
36 | }
37 | .opacity(appState.currentPracticeNoteIndex > index ? 0.5 : 1 )
38 | }
39 | }
40 | }
41 | .onChange(of: appState.currentNote) { currentNote in
42 | if currentNote == appState.currentPracticeNote() {
43 |
44 | if appState.currentPracticeNoteIndex < appState.practiceNotes?.count ?? 0 {
45 | appState.currentPracticeNoteIndex += 1
46 | }
47 |
48 | if appState.currentPracticeNoteIndex >= appState.practiceNotes?.count ?? 0 {
49 |
50 | // multi page sheet: show immediately next sheet
51 | if appState.currentMenuState.isMultiStateMusicSheet {
52 | appState.currentMenuState.showNextMenuState()
53 | return
54 | }
55 |
56 | // end
57 |
58 | appState.blockContinue = true // prevent the user from skipping a menu item while in auto transition mode
59 | DispatchQueue.main.asyncAfter(deadline: .now()+1.3) {
60 | SoundEngine.playCelebration()
61 | DispatchQueue.main.asyncAfter(deadline: .now()+0.5) {
62 | appState.currentMenuState.showNextMenuState()
63 | appState.blockContinue = false
64 | }
65 | }
66 | } else {
67 | // next
68 | appState.currentNote = nil
69 | }
70 | }
71 | }
72 | }
73 |
74 | func getNoteOffsetInViolinClef(note: Note) -> CGFloat {
75 | switch note {
76 | case .oneLine_C:
77 | return 3
78 | case .oneLine_D:
79 | return 2.5
80 | case .oneLine_E:
81 | return 2
82 | case .oneLine_F:
83 | return 1.5
84 | case .oneLine_G:
85 | return 1
86 | case .oneLine_A:
87 | return 0.5
88 | case .oneLine_B:
89 | return 0
90 |
91 | default:
92 | fatalError("Note \(note.rawValue) can not be drawn in violin clef")
93 | }
94 | }
95 |
96 | func getNoteNeedsHelpLine(note: Note) -> Bool {
97 | if note == .oneLine_C {
98 | return true
99 | } else {
100 | return false
101 | }
102 | }
103 |
104 | // lines of the note sheet
105 | var lines: some View {
106 | ZStack{
107 | line.offset(y: -2*noteHeight)
108 | line.offset(y: -1*noteHeight)
109 | line.offset(y: 0)
110 | line.offset(y: 1*noteHeight)
111 | line.offset(y: 2*noteHeight)
112 | }
113 | }
114 |
115 | var line: some View {
116 | Rectangle()
117 | .frame(maxWidth: .infinity)
118 | .frame(height: 2)
119 | }
120 | }
121 |
122 | struct NoteSheetView_Previews: PreviewProvider {
123 | static var previews: some View {
124 | NoteSheetView(noteHeight: 30, notes: [.oneLine_C, .oneLine_D, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_A, .oneLine_B])
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/View/ContentView.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 |
3 | @available(iOS 16.0, *)
4 | struct ContentView: View {
5 | @EnvironmentObject var appState: AppState
6 |
7 | @State private var sizeDivisor: CGFloat = 5
8 |
9 | var body: some View {
10 | GeometryReader { geo in
11 | VStack(spacing: 0){
12 | ZStack(alignment: .center){
13 | GeometryReader { upperGeo in
14 | Rectangle()
15 | .fill(Color("UIGray"))
16 |
17 | HStack{
18 | Spacer()
19 |
20 | // Display
21 | VStack{
22 | Spacer()
23 | DisplayShapeView(geo: geo, upperGeo: upperGeo)
24 | .task(id: appState.currentMenuState){
25 | appState.practiceNotes = appState.currentMenuState.notes
26 | appState.currentPracticeNoteIndex = 0
27 | appState.currentNote = nil
28 | appState.showHelp = false
29 | appState.showHelpLabels = appState.currentMenuState.showUIHelpLabels
30 | }
31 |
32 | // Menu
33 | .overlay{
34 | VStack {
35 | HeaderView()
36 | if appState.isOn { Spacer() }
37 | appState.currentMenuState.view
38 | .multilineTextAlignment(.center)
39 | .font(.system(.body, design: .monospaced))
40 | .lineSpacing(3)
41 | if appState.isOn { Spacer() }
42 | HelpLabelView()
43 | }
44 | .foregroundColor(Color("DarkGray"))
45 | .shadow(color: .black.opacity(0.4), radius: 2, x: 2, y: 3)
46 | .opacity(0.75)
47 | .padding(25)
48 | }
49 | Spacer()
50 | }
51 |
52 |
53 | Spacer()
54 | VStack(alignment: .leading, spacing: 30){
55 |
56 | MenuButtonView(label: "power", isPowerButton: true) {
57 | if appState.isOn {
58 | appState.isOn.toggle()
59 | appState.currentMenuState = MenuState.flow[1]
60 | appState.reset()
61 | SoundEngine.buttonSound()
62 | } else {
63 | appState.isOn.toggle()
64 | appState.currentMenuState = MenuState.flow[2]
65 | SoundEngine.playCelebration()
66 | }
67 | }
68 |
69 | MenuButtonView(label: "info", isPowerButton: false) {
70 | if appState.currentMenuState.playingHelpAllowed() {
71 | appState.showHelp.toggle()
72 | SoundEngine.buttonSound()
73 | return
74 | }
75 |
76 | if appState.currentMenuState == MenuState.flow[2] {
77 | appState.currentMenuState = MenuState.flow[0]
78 | SoundEngine.buttonSound()
79 | return
80 | }
81 |
82 | if appState.currentMenuState == MenuState.flow[0] {
83 | appState.currentMenuState = MenuState.flow[2]
84 | SoundEngine.buttonSound()
85 | return
86 | }
87 | }
88 |
89 | MenuButtonView(label: "enter", isPowerButton: false) {
90 | if appState.isOn{
91 | if appState.blockContinue == false {
92 | appState.currentMenuState.showNextMenuState()
93 | SoundEngine.buttonSound()
94 | }
95 | }
96 | }
97 | }
98 | Spacer()
99 | }
100 | .padding(.top)
101 | }
102 |
103 | }
104 | .padding(.bottom, 1)
105 |
106 | AboveKeyboardButtonBar(geo: geo, sizeDivisor: $sizeDivisor)
107 | // I can't fix the wired button animation, mmmm :/
108 | // .animation(.none)
109 |
110 | // Keyboard
111 | ScrollView(.horizontal, showsIndicators: false){
112 | HStack(spacing: 3){
113 | ForEach(Octave.allCases, id: \.self) { octave in
114 | OctaveView(drawingHeight: geo.size.height/sizeDivisor*2, octave: octave)
115 | }
116 | }
117 | .disabled(!appState.isOn)
118 | }
119 | .frame(width: geo.size.width, height: geo.size.height/sizeDivisor*2)
120 | .padding(.bottom, 24)
121 | .padding(.top, 3)
122 | .background{
123 | Color.black
124 | }
125 | }
126 | }
127 | .background{
128 | Color.black
129 | }
130 | .ignoresSafeArea()
131 | }
132 |
133 | }
134 |
135 | @available(iOS 16.0, *)
136 | struct ContentView_Previews: PreviewProvider {
137 |
138 | static var previews: some View {
139 | @StateObject var appState = AppState()
140 | ContentView()
141 | .previewInterfaceOrientation(.landscapeLeft)
142 | .environmentObject(appState)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/View/UpperArea/MusicNotation/ViolinClefShape.swift:
--------------------------------------------------------------------------------
1 |
2 | // created with https://svg-to-swiftui.quassum.com/
3 |
4 | import SwiftUI
5 |
6 | struct ViolinClefShape: Shape {
7 | func path(in rect: CGRect) -> Path {
8 | var path = Path()
9 | let width = rect.size.width
10 | let height = rect.size.height
11 | path.move(to: CGPoint(x: 0.37481*width, y: 0.79868*height))
12 | path.addCurve(to: CGPoint(x: 0.43958*width, y: 0.85105*height), control1: CGPoint(x: 0.37481*width, y: 0.82131*height), control2: CGPoint(x: 0.41129*width, y: 0.84727*height))
13 | path.addCurve(to: CGPoint(x: 0.49472*width, y: 0.85211*height), control1: CGPoint(x: 0.43958*width, y: 0.85105*height), control2: CGPoint(x: 0.46983*width, y: 0.85638*height))
14 | path.addCurve(to: CGPoint(x: 0.57619*width, y: 0.77158*height), control1: CGPoint(x: 0.49922*width, y: 0.85283*height), control2: CGPoint(x: 0.57468*width, y: 0.84305*height))
15 | path.addLine(to: CGPoint(x: 0.56027*width, y: 0.68459*height))
16 | path.addCurve(to: CGPoint(x: 0.61617*width, y: 0.66703*height), control1: CGPoint(x: 0.57088*width, y: 0.68331*height), control2: CGPoint(x: 0.60549*width, y: 0.67152*height))
17 | path.addCurve(to: CGPoint(x: 0.66972*width, y: 0.62383*height), control1: CGPoint(x: 0.64548*width, y: 0.65474*height), control2: CGPoint(x: 0.65831*width, y: 0.63697*height))
18 | path.addCurve(to: CGPoint(x: 0.69009*width, y: 0.56914*height), control1: CGPoint(x: 0.68255*width, y: 0.6041*height), control2: CGPoint(x: 0.69009*width, y: 0.59062*height))
19 | path.addCurve(to: CGPoint(x: 0.55099*width, y: 0.47075*height), control1: CGPoint(x: 0.69009*width, y: 0.5148*height), control2: CGPoint(x: 0.62781*width, y: 0.47075*height))
20 | path.addCurve(to: CGPoint(x: 0.51883*width, y: 0.47341*height), control1: CGPoint(x: 0.53992*width, y: 0.47075*height), control2: CGPoint(x: 0.52916*width, y: 0.47168*height))
21 | path.addLine(to: CGPoint(x: 0.5051*width, y: 0.4036*height))
22 | path.addCurve(to: CGPoint(x: 0.62748*width, y: 0.26647*height), control1: CGPoint(x: 0.55708*width, y: 0.36274*height), control2: CGPoint(x: 0.60978*width, y: 0.3125*height))
23 | path.addCurve(to: CGPoint(x: 0.59052*width, y: 0.147*height), control1: CGPoint(x: 0.64483*width, y: 0.20567*height), control2: CGPoint(x: 0.63653*width, y: 0.1646*height))
24 | path.addCurve(to: CGPoint(x: 0.46681*width, y: 0.2398*height), control1: CGPoint(x: 0.54827*width, y: 0.14006*height), control2: CGPoint(x: 0.497*width, y: 0.18087*height))
25 | path.addCurve(to: CGPoint(x: 0.4834*width, y: 0.35608*height), control1: CGPoint(x: 0.44719*width, y: 0.27127*height), control2: CGPoint(x: 0.47218*width, y: 0.33139*height))
26 | path.addCurve(to: CGPoint(x: 0.30991*width, y: 0.54946*height), control1: CGPoint(x: 0.40071*width, y: 0.41213*height), control2: CGPoint(x: 0.31845*width, y: 0.45232*height))
27 | path.addCurve(to: CGPoint(x: 0.50463*width, y: 0.68718*height), control1: CGPoint(x: 0.30991*width, y: 0.62553*height), control2: CGPoint(x: 0.39709*width, y: 0.68718*height))
28 | path.addCurve(to: CGPoint(x: 0.55119*width, y: 0.68559*height), control1: CGPoint(x: 0.51491*width, y: 0.68718*height), control2: CGPoint(x: 0.53188*width, y: 0.68735*height))
29 | path.addLine(to: CGPoint(x: 0.56027*width, y: 0.77245*height))
30 | path.addCurve(to: CGPoint(x: 0.49537*width, y: 0.84459*height), control1: CGPoint(x: 0.56027*width, y: 0.77245*height), control2: CGPoint(x: 0.56376*width, y: 0.83598*height))
31 | path.addCurve(to: CGPoint(x: 0.44958*width, y: 0.83926*height), control1: CGPoint(x: 0.46823*width, y: 0.84801*height), control2: CGPoint(x: 0.44205*width, y: 0.85196*height))
32 | path.addCurve(to: CGPoint(x: 0.49537*width, y: 0.79868*height), control1: CGPoint(x: 0.47073*width, y: 0.83147*height), control2: CGPoint(x: 0.49537*width, y: 0.81905*height))
33 | path.addCurve(to: CGPoint(x: 0.4351*width, y: 0.75277*height), control1: CGPoint(x: 0.49537*width, y: 0.77333*height), control2: CGPoint(x: 0.46838*width, y: 0.75277*height))
34 | path.addCurve(to: CGPoint(x: 0.37481*width, y: 0.79868*height), control1: CGPoint(x: 0.4018*width, y: 0.75277*height), control2: CGPoint(x: 0.37481*width, y: 0.77333*height))
35 | path.closeSubpath()
36 | path.move(to: CGPoint(x: 0.49707*width, y: 0.4099*height))
37 | path.addLine(to: CGPoint(x: 0.50988*width, y: 0.47512*height))
38 | path.addCurve(to: CGPoint(x: 0.41191*width, y: 0.56914*height), control1: CGPoint(x: 0.45314*width, y: 0.48752*height), control2: CGPoint(x: 0.41191*width, y: 0.52493*height))
39 | path.addCurve(to: CGPoint(x: 0.49421*width, y: 0.63063*height), control1: CGPoint(x: 0.41191*width, y: 0.61917*height), control2: CGPoint(x: 0.4965*width, y: 0.6425*height))
40 | path.addCurve(to: CGPoint(x: 0.44901*width, y: 0.58225*height), control1: CGPoint(x: 0.46123*width, y: 0.62057*height), control2: CGPoint(x: 0.44901*width, y: 0.60926*height))
41 | path.addCurve(to: CGPoint(x: 0.51845*width, y: 0.51877*height), control1: CGPoint(x: 0.44901*width, y: 0.55172*height), control2: CGPoint(x: 0.47852*width, y: 0.52607*height))
42 | path.addLine(to: CGPoint(x: 0.54891*width, y: 0.67404*height))
43 | path.addCurve(to: CGPoint(x: 0.52666*width, y: 0.67735*height), control1: CGPoint(x: 0.54206*width, y: 0.67535*height), control2: CGPoint(x: 0.53467*width, y: 0.67646*height))
44 | path.addCurve(to: CGPoint(x: 0.35627*width, y: 0.55602*height), control1: CGPoint(x: 0.41075*width, y: 0.67571*height), control2: CGPoint(x: 0.35627*width, y: 0.63209*height))
45 | path.addCurve(to: CGPoint(x: 0.49707*width, y: 0.4099*height), control1: CGPoint(x: 0.35627*width, y: 0.5134*height), control2: CGPoint(x: 0.42724*width, y: 0.46425*height))
46 | path.closeSubpath()
47 | path.move(to: CGPoint(x: 0.55781*width, y: 0.67213*height))
48 | path.addLine(to: CGPoint(x: 0.52744*width, y: 0.51746*height))
49 | path.addCurve(to: CGPoint(x: 0.54173*width, y: 0.51667*height), control1: CGPoint(x: 0.53209*width, y: 0.51695*height), control2: CGPoint(x: 0.53687*width, y: 0.51667*height))
50 | path.addCurve(to: CGPoint(x: 0.63445*width, y: 0.58225*height), control1: CGPoint(x: 0.59294*width, y: 0.51667*height), control2: CGPoint(x: 0.63445*width, y: 0.54604*height))
51 | path.addCurve(to: CGPoint(x: 0.55781*width, y: 0.67213*height), control1: CGPoint(x: 0.63343*width, y: 0.61139*height), control2: CGPoint(x: 0.63147*width, y: 0.65479*height))
52 | path.closeSubpath()
53 | path.move(to: CGPoint(x: 0.49321*width, y: 0.34648*height))
54 | path.addCurve(to: CGPoint(x: 0.50347*width, y: 0.25269*height), control1: CGPoint(x: 0.47232*width, y: 0.29663*height), control2: CGPoint(x: 0.49121*width, y: 0.27179*height))
55 | path.addCurve(to: CGPoint(x: 0.58391*width, y: 0.17657*height), control1: CGPoint(x: 0.53245*width, y: 0.2076*height), control2: CGPoint(x: 0.55795*width, y: 0.19121*height))
56 | path.addCurve(to: CGPoint(x: 0.59737*width, y: 0.25269*height), control1: CGPoint(x: 0.61768*width, y: 0.18594*height), control2: CGPoint(x: 0.60244*width, y: 0.25269*height))
57 | path.addCurve(to: CGPoint(x: 0.49321*width, y: 0.34648*height), control1: CGPoint(x: 0.58147*width, y: 0.29208*height), control2: CGPoint(x: 0.52443*width, y: 0.32338*height))
58 | path.closeSubpath()
59 | return path
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Model/MenuState.swift:
--------------------------------------------------------------------------------
1 | //
2 | // MenuState.swift
3 | //
4 | //
5 | // Created by Henri Bredt on 17.04.23.
6 | //
7 |
8 | import Foundation
9 | import SwiftUI
10 |
11 | struct MenuState {
12 |
13 | init(id: Int, view: AnyView, notes: [Note]? = nil, isMultiStateMusicSheet: Bool = false, headerText: String?, showUIHelpLabels: Bool = true, helpPlayNotesLabel: Bool = false, helpLabel: String, helpDescription: String, showNextMenuState: @escaping () -> ()) {
14 | self.id = id
15 | self.view = view
16 | self.notes = notes
17 | self.isMultiStateMusicSheet = isMultiStateMusicSheet
18 | self.headerText = headerText
19 | self.showUIHelpLabels = showUIHelpLabels
20 | self.helpPlayNotesLabel = helpPlayNotesLabel
21 | self.helpLabel = helpLabel
22 | self.helpDescription = helpDescription
23 | self.showNextMenuState = showNextMenuState
24 | }
25 |
26 | let id: Int
27 | let view: AnyView // center view to show
28 | let notes: [Note]? // notes of current menu state
29 | let isMultiStateMusicSheet: Bool // if music is spread over multiple menu states, will transition immediately to next
30 | let headerText: String? // text in the header
31 | let showUIHelpLabels: Bool // show help labels on keys and in notation
32 | let helpPlayNotesLabel: Bool // if a label saying "Play all notes to continue" should be shown in lower area
33 | let helpLabel: String // label in the lower area explaining the button
34 | let helpDescription: String // label in the lower area explaining what the button does
35 | let showNextMenuState: () -> () // what happens on enter button press
36 |
37 | func playingHelpAllowed() -> Bool {
38 | return notes != nil
39 | }
40 | }
41 |
42 | extension MenuState: Comparable {
43 | static func < (lhs: MenuState, rhs: MenuState) -> Bool {
44 | lhs.id < rhs.id
45 | }
46 |
47 | static func == (lhs: MenuState, rhs: MenuState) -> Bool {
48 | lhs.id == rhs.id
49 | }
50 | }
51 |
52 | extension MenuState {
53 | static var flow: [MenuState] = [
54 | MenuState(
55 | id: 0, // INFO
56 | view:
57 | AnyView(
58 | VStack(spacing: 6){
59 | Text("About ePianighi \"Craig 23\"").bold()
60 | Text("Swift Student Challenge Submission 2023 by [Henri Bredt](https://henribredt.de)")
61 | .font(.system(.callout, design: .monospaced))
62 | Text("Resources and Credits").bold()
63 | .font(.system(.caption, design: .monospaced))
64 | .padding(.top, 33)
65 | Text("Piano Sounds: https://github.com/fuhton/piano-mp3\nButton Sounds: https://snd.dev\nSVG to SwiftUI Shape : https://svg-to-swiftui.quassum.com")
66 | .font(.system(.caption, design: .monospaced))
67 | }
68 | .tint(Color("LinkColor"))
69 | .padding(.bottom, 30)
70 | ),
71 | headerText: nil,
72 | helpPlayNotesLabel: false,
73 | helpLabel: "INFO",
74 | helpDescription: "to dismiss.",
75 | showNextMenuState: {
76 | AppState.shared.currentMenuState = MenuState.flow[2]
77 | }
78 | ),
79 | MenuState(
80 | id: 1, // OFF
81 | view: AnyView(EmptyView()),
82 | headerText: nil,
83 | helpPlayNotesLabel: false,
84 | helpLabel: "POWER",
85 | helpDescription: "to turn me on",
86 | showNextMenuState: {}
87 | ),
88 |
89 | MenuState(
90 | id: 2, // HOME
91 | view:
92 | AnyView(
93 | VStack(spacing: 6){
94 | Image(systemName: "pianokeys")
95 | .resizable()
96 | .aspectRatio(contentMode: .fit)
97 | .frame(maxHeight: 40)
98 | .padding(.bottom)
99 | Text("ePianighi \"Craig 23\"")
100 | .bold()
101 | Text("Your educational E-Piano")
102 | .font(.system(.caption, design: .monospaced))
103 | }
104 | .padding(10).padding(.bottom, 30)
105 | ),
106 | headerText: "WELCOME",
107 | helpPlayNotesLabel: false,
108 | helpLabel: "ENTER",
109 | helpDescription: "to learn how to play the piano. Or just play.",
110 | showNextMenuState: {
111 | AppState.shared.currentMenuState = MenuState.flow[3]
112 | }
113 | ),
114 | MenuState(
115 | id: 3, // COURSE START
116 | view:
117 | AnyView(Text("Good to know before we start: You can adjust the keyboard size using the magnifying buttons and you can scroll the keyboard to make other keys visible. Make sure the Volume is turned up and your device is not muted.")),
118 | headerText: "Beginners Course: Learn how to play the Piano",
119 | helpPlayNotesLabel: false,
120 | helpLabel: "ENTER",
121 | helpDescription: "to continue.",
122 | showNextMenuState: {
123 | AppState.shared.currentMenuState = MenuState.flow[4]
124 | }
125 | ),
126 | MenuState(
127 | id: 4,
128 | view:
129 | AnyView(
130 | VStack{
131 | Image(systemName: "music.quarternote.3")
132 | .resizable()
133 | .aspectRatio(contentMode: .fit)
134 | .frame(maxHeight: 40)
135 | .padding(.bottom)
136 | Text("Let's learn how to play the piano and read notes!")
137 | .padding(.bottom, 25)
138 | }
139 | ),
140 | headerText: "Beginners Course: Learn how to play the Piano",
141 | helpLabel: "ENTER",
142 | helpDescription: "to continue.",
143 | showNextMenuState: {
144 | AppState.shared.currentMenuState = MenuState.flow[5]
145 | }
146 | ),
147 | MenuState(
148 | id: 5,
149 | view: AnyView(Text("The Middle C is our starting and a reference point on the keyboard. It's marked with C'. Find it on the keyboard next to the center two black keys. On the next page you will see the notation of that note and start playing.")),
150 | headerText: "1. The Middle C - Theory",
151 | helpPlayNotesLabel: false,
152 | helpLabel: "ENTER",
153 | helpDescription: "to continue.",
154 | showNextMenuState: {
155 | AppState.shared.currentMenuState = MenuState.flow[6]
156 | }
157 | ),
158 | MenuState(
159 | id: 6,
160 | view:
161 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_C] )),
162 | notes: [.oneLine_C],
163 | headerText: "1. The Middle C - Practice",
164 | helpPlayNotesLabel: true,
165 | helpLabel: "INFO",
166 | helpDescription: "for help.",
167 | showNextMenuState: {
168 | AppState.shared.currentMenuState = MenuState.flow[7]
169 | }
170 | ),
171 | MenuState(
172 | id: 7,
173 | view:
174 | AnyView(Text("While looking at the keyboard you might have noticed a pattern: The key arrangements and the notes are repetitive. Starting with the \"C\", after every seven white keys a new repetition begins. That's an Octave: The seven white keys represent the full-tones C, D, E, F, G, A and B. There are Octaves with higher and lower pitches.")),
175 | headerText: "2. An Octave — Theory",
176 | helpPlayNotesLabel: false,
177 | helpLabel: "ENTER",
178 | helpDescription: "to continue.",
179 | showNextMenuState: {
180 | AppState.shared.currentMenuState = MenuState.flow[8]
181 | }
182 | ),
183 | MenuState(
184 | id: 8,
185 | view:
186 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_C, .oneLine_D, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_A, .oneLine_B] )),
187 | notes: [.oneLine_C, .oneLine_D, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_A, .oneLine_B],
188 | headerText: "2. An Octave — Practice",
189 | helpPlayNotesLabel: true,
190 | helpLabel: "INFO",
191 | helpDescription: "for help.",
192 | showNextMenuState: {
193 | AppState.shared.currentMenuState = MenuState.flow[9]
194 | }
195 | ),
196 | MenuState(
197 | id: 9,
198 | view:
199 | AnyView(Text("You might have noticed the large symbol on the left side of the music sheet. That's the treble clef and it indicates which notes to play with your right hand. It's like a musical map, with higher lines representing higher pitches.")),
200 | headerText: "3. The Treble Clef & G — Theory",
201 | helpPlayNotesLabel: false,
202 | helpLabel: "ENTER",
203 | helpDescription: "to contine.",
204 | showNextMenuState: {
205 | AppState.shared.currentMenuState = MenuState.flow[10]
206 | }
207 | ),
208 | MenuState(
209 | id: 10,
210 | view:
211 | AnyView(Text("The note \"G\" is important in the context of the treble clef because it determines the clef's position on the staff and serves as a reference point for identifying other notes. Take a look on the next page: The Note \"G\" is on the line that goes trough the inner circle of the treble clef. This helps to quickly spot the \"G\".")),
212 | headerText: "3. The Treble Clef & G — Theory",
213 | helpPlayNotesLabel: false,
214 | helpLabel: "ENTER",
215 | helpDescription: "to continue.",
216 | showNextMenuState: {
217 | AppState.shared.currentMenuState = MenuState.flow[11]
218 | }
219 | ),
220 | MenuState(
221 | id: 11,
222 | view:
223 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_G] )),
224 | notes: [.oneLine_G],
225 | headerText: "3. The Treble Clef & G — Practice",
226 | helpPlayNotesLabel: true,
227 | helpLabel: "INFO",
228 | helpDescription: "for help.",
229 | showNextMenuState: {
230 | AppState.shared.currentMenuState = MenuState.flow[12]
231 | }
232 | ),
233 | MenuState(
234 | id: 12,
235 | view:
236 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_C, .oneLine_D, .oneLine_E, .oneLine_D, .oneLine_E, .oneLine_F, .oneLine_G] )),
237 | notes: [.oneLine_C, .oneLine_D, .oneLine_E, .oneLine_D, .oneLine_E, .oneLine_F, .oneLine_G],
238 | headerText: "3. The Treble Clef & G — Practice",
239 | helpPlayNotesLabel: true,
240 | helpLabel: "INFO",
241 | helpDescription: "for help.", showNextMenuState: {
242 | AppState.shared.currentMenuState = MenuState.flow[13]
243 | }
244 | ),
245 | MenuState(
246 | id: 13,
247 | view:
248 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_G, .oneLine_E, .oneLine_G, .oneLine_C, .oneLine_G] )),
249 | notes: [.oneLine_G, .oneLine_E, .oneLine_G, .oneLine_C, .oneLine_G],
250 | headerText: "3. The Treble Clef & G — Practice",
251 | helpPlayNotesLabel: true,
252 | helpLabel: "INFO",
253 | helpDescription: "for help.", showNextMenuState: {
254 | AppState.shared.currentMenuState = MenuState.flow[14]
255 | }
256 | ),
257 | MenuState(
258 | id: 14,
259 | view:
260 | AnyView(Text("You're progressing swiftly and are now ready to play your first real piece of piano music. It's a part of Beethoven's Symphony No. 9 \"Ode to Joy\" — a very well known piece and the European anthem.")),
261 | headerText: "4. Play — Ode to Joy (Beethoven)",
262 | helpPlayNotesLabel: false,
263 | helpLabel: "ENTER",
264 | helpDescription: "to continue.",
265 | showNextMenuState: {
266 | AppState.shared.currentMenuState = MenuState.flow[15]
267 | }
268 | ),
269 |
270 | MenuState(
271 | id: 15,
272 | view:
273 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_E, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_G, .oneLine_F, .oneLine_E, .oneLine_D] )),
274 | notes: [.oneLine_E, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_G, .oneLine_F, .oneLine_E, .oneLine_D],
275 | isMultiStateMusicSheet: true,
276 | headerText: "4. Play — Ode to Joy (Beethoven)",
277 | helpPlayNotesLabel: true,
278 | helpLabel: "INFO",
279 | helpDescription: "for help.", showNextMenuState: {
280 | AppState.shared.currentMenuState = MenuState.flow[16]
281 | }
282 | ),
283 | MenuState(
284 | id: 16,
285 | view:
286 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_C, .oneLine_C, .oneLine_D, .oneLine_E, .oneLine_D, .oneLine_C, .oneLine_C] )),
287 | notes: [.oneLine_C, .oneLine_C, .oneLine_D, .oneLine_E, .oneLine_D, .oneLine_C, .oneLine_C], headerText: "4. Play — Ode to Joy (Beethoven)",
288 | helpPlayNotesLabel: true,
289 | helpLabel: "INFO",
290 | helpDescription: "for help.",
291 | showNextMenuState: {
292 | AppState.shared.currentMenuState = MenuState.flow[17]
293 | }
294 | ),
295 |
296 | MenuState(
297 | id: 17,
298 | view:
299 | AnyView(Text("Well done!\n\nSo far the notes and the keys had some help-labels to get you started. Let's try it without them, like a real pianist.")),
300 | headerText: "4. Play — Ode to Joy (Beethoven)",
301 | showUIHelpLabels: false,
302 | helpPlayNotesLabel: false,
303 | helpLabel: "ENTER",
304 | helpDescription: "to continue.",
305 | showNextMenuState: {
306 | AppState.shared.currentMenuState = MenuState.flow[18]
307 | }
308 | ),
309 | MenuState(
310 | id: 18,
311 | view:
312 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_E, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_G, .oneLine_F, .oneLine_E, .oneLine_D] )),
313 | notes:
314 | [.oneLine_E, .oneLine_E, .oneLine_F, .oneLine_G, .oneLine_G, .oneLine_F, .oneLine_E, .oneLine_D],
315 | isMultiStateMusicSheet: true,
316 | headerText: "4. Play — Ode to Joy (Beethoven)",
317 | showUIHelpLabels: false,
318 | helpPlayNotesLabel: true,
319 | helpLabel: "INFO",
320 | helpDescription: "for help.",
321 | showNextMenuState: {
322 | AppState.shared.currentMenuState = MenuState.flow[19]
323 | }
324 | ),
325 | MenuState(
326 | id: 19,
327 | view:
328 | AnyView(NoteSheetView(noteHeight: 20, notes: [.oneLine_C, .oneLine_C, .oneLine_D, .oneLine_E, .oneLine_D, .oneLine_C, .oneLine_C] )),
329 | notes: [.oneLine_C, .oneLine_C, .oneLine_D, .oneLine_E, .oneLine_D, .oneLine_C, .oneLine_C],
330 | headerText: "4. Play — Ode to Joy (Beethoven)",
331 | showUIHelpLabels: false,
332 | helpPlayNotesLabel: true,
333 | helpLabel: "INFO",
334 | helpDescription: "for help.",
335 | showNextMenuState: {
336 | AppState.shared.currentMenuState = MenuState.flow[20]
337 | }
338 | ),
339 | MenuState(
340 | id: 20,
341 | view:
342 | AnyView(Text("Well done!\n\n You've successfully finished the Beginners Course.\nLet's jam together at WWDC 23 — see you there!")),
343 | isMultiStateMusicSheet: false,
344 | headerText: "Beginners Course: Learn how to play the Piano",
345 | showUIHelpLabels: false,
346 | helpPlayNotesLabel: false,
347 | helpLabel: "ENTER",
348 | helpDescription: "to return to the menu.", showNextMenuState: {
349 | AppState.shared.currentMenuState = MenuState.flow[2]
350 | }
351 | ),
352 | ]
353 | }
354 |
--------------------------------------------------------------------------------