├── 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 | ![Untitled 1](https://user-images.githubusercontent.com/57298155/233409352-db6ad4fa-307f-43fb-b920-8f48c322c789.png) 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 | --------------------------------------------------------------------------------