diff --git a/Apps/LekaActivityUIExplorer/Resources/GEKNewSystem/activities/activity-xylophone-heptatonic.yml b/Apps/LekaActivityUIExplorer/Resources/GEKNewSystem/activities/activity-xylophone-heptatonic.yml new file mode 100644 index 0000000000..0616c02d6f --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Resources/GEKNewSystem/activities/activity-xylophone-heptatonic.yml @@ -0,0 +1,15 @@ +# Leka - iOS Monorepo +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +id: cd5d93afbd8f4071b25479cd6372a7a9 +name: Xylophone Heptatonic +description: L'objectif est de jouer de la musique à l'aide du xylophone +image: activity_color_recognition_1 +sequence: + - exercises: + - instructions: Joue du xylophone avec Leka + interface: musicalInstruments + payload: + instrument: xylophone + scale: majorHeptatonic diff --git a/Apps/LekaActivityUIExplorer/Resources/GEKNewSystem/activities/activity-xylophone-pentatonic.yml b/Apps/LekaActivityUIExplorer/Resources/GEKNewSystem/activities/activity-xylophone-pentatonic.yml new file mode 100644 index 0000000000..0615ee6a0f --- /dev/null +++ b/Apps/LekaActivityUIExplorer/Resources/GEKNewSystem/activities/activity-xylophone-pentatonic.yml @@ -0,0 +1,15 @@ +# Leka - iOS Monorepo +# Copyright 2023 APF France handicap +# SPDX-License-Identifier: Apache-2.0 + +id: 264d720bf8904043b97d5d7b3d0e7124 +name: Xylophone Pentatonic +description: L'objectif est de jouer de la musique à l'aide du xylophone +image: activity_color_recognition_1 +sequence: + - exercises: + - instructions: Joue du xylophone avec Leka + interface: musicalInstruments + payload: + instrument: xylophone + scale: majorPentatonic diff --git a/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift b/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift index 8ab826dd70..0f5972b022 100644 --- a/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift +++ b/Apps/LekaActivityUIExplorer/Sources/GEKNewSystem/GEKNewSystemView.swift @@ -36,6 +36,8 @@ let kActivities: [Activity] = [ ContentKit.decodeActivity("remote-standard"), ContentKit.decodeActivity("remote-arrow"), ContentKit.decodeActivity("activity-hideAndSeek"), + ContentKit.decodeActivity("activity-xylophone-pentatonic"), + ContentKit.decodeActivity("activity-xylophone-heptatonic"), ] struct GEKNewSystemView: View { diff --git a/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift b/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift new file mode 100644 index 0000000000..ffc25c4798 --- /dev/null +++ b/Modules/ContentKit/Sources/Exercise/Exercice+MusicalInstrument.swift @@ -0,0 +1,23 @@ +// Leka - iOS Monorepo +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +public enum MusicalInstrument { + + public struct Payload: Codable { + public let instrument: String + public let scale: String + + enum CodingKeys: String, CodingKey { + case instrument, scale + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + self.instrument = try container.decode(String.self, forKey: .instrument) + self.scale = try container.decode(String.self, forKey: .scale) + } + } + +} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift index f36817c240..a582f558a9 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Interface.swift @@ -17,6 +17,7 @@ extension Exercise { case remoteStandard case remoteArrow case hideAndSeek + case musicalInstruments } } diff --git a/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift b/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift index 1a726f57c0..114c191c47 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise+Payload.swift @@ -11,3 +11,4 @@ extension DragAndDropIntoZones.Payload: ExercisePayloadProtocol {} extension DragAndDropToAssociate.Payload: ExercisePayloadProtocol {} extension AudioRecordingPlayer.Payload: ExercisePayloadProtocol {} extension HideAndSeek.Payload: ExercisePayloadProtocol {} +extension MusicalInstrument.Payload: ExercisePayloadProtocol {} diff --git a/Modules/ContentKit/Sources/Exercise/Exercise.swift b/Modules/ContentKit/Sources/Exercise/Exercise.swift index 7185d2bb9a..21776141a4 100644 --- a/Modules/ContentKit/Sources/Exercise/Exercise.swift +++ b/Modules/ContentKit/Sources/Exercise/Exercise.swift @@ -46,6 +46,9 @@ public struct Exercise: Codable { case (.hideAndSeek, .none): payload = try container.decode(HideAndSeek.Payload.self, forKey: .payload) + case (.musicalInstruments, .none): + payload = try container.decode(MusicalInstrument.Payload.self, forKey: .payload) + case (.remoteStandard, .none), (.remoteArrow, .none): payload = nil diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift index 9a82766672..afea638e82 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyGameplay.swift @@ -4,13 +4,14 @@ import AudioKit import Combine +import ContentKit import SwiftUI public class MelodyGameplay: ObservableObject { @Published public var progress: CGFloat = 0.0 @Published var state: ExerciseState = .idle - private let xyloPlayer = MIDIPlayer(name: "Xylophone", samples: xyloSamples) + private let xyloPlayer = MIDIPlayer(instrument: .xylophone) private let song: MelodySongModel @@ -68,6 +69,6 @@ public class MelodyGameplay: ObservableObject { let index = kListOfTiles.firstIndex(where: { $0.noteNumber == currentNoteNumber }) - return kListOfTiles[index!].color + return kListOfTiles[index!].color.screen } } diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift index fd7ff0be61..463c3beda2 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyView.swift @@ -2,9 +2,16 @@ // Copyright 2023 APF France handicap // SPDX-License-Identifier: Apache-2.0 -import DesignKit +import AudioKit +import RobotKit import SwiftUI +public struct XylophoneTile: Identifiable { + public var id: Int + var noteNumber: MIDINoteNumber + var color: Robot.Color +} + public let kListOfTiles: [XylophoneTile] = [ XylophoneTile(id: 0, noteNumber: 24, color: .pink), XylophoneTile(id: 1, noteNumber: 26, color: .red), @@ -19,7 +26,7 @@ public struct MelodyView: View { @ObservedObject private var viewModel: MelodyViewModel let defaultTilesSpacing: CGFloat = 16 - let tilesNumber = 7 + let tileNumber = 7 public init(gameplay: MelodyGameplay) { self.viewModel = MelodyViewModel(gameplay: gameplay) @@ -36,9 +43,11 @@ public struct MelodyView: View { Button { viewModel.onTileTapped(tile: tile) } label: { - tile.color + tile.color.screen } - .buttonStyle(XylophoneTileButtonStyle(index: tile.id, tilesNumber: tilesNumber)) + .buttonStyle( + MusicalInstrumentView.XylophoneView.TileButtonStyle(index: tile.id, tileNumber: tileNumber) + ) .compositingGroup() } } diff --git a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift index b0f9ad5920..68adf1bbbe 100644 --- a/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift +++ b/Modules/GameEngineKit/Sources/Staging/Melody/MelodyViewModel.swift @@ -9,7 +9,7 @@ public class MelodyViewModel: Identifiable, ObservableObject { public var gameplay: MelodyGameplay @Published public var progress: CGFloat - @Published var state: ExerciseState + @Published var state: ExerciseState private var cancellables: Set = [] diff --git a/Modules/GameEngineKit/Sources/Staging/Xylophone/XylophoneView.swift b/Modules/GameEngineKit/Sources/Staging/Xylophone/XylophoneView.swift deleted file mode 100644 index a7c77bea4e..0000000000 --- a/Modules/GameEngineKit/Sources/Staging/Xylophone/XylophoneView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// Leka - iOS Monorepo -// Copyright 2023 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -import AudioKit -import DesignKit -import SwiftUI - -public struct XylophoneTile: Identifiable, Hashable { - public var id: Int - var noteNumber: MIDINoteNumber - var color: Color -} - -let kListOfXylophoneTiles: [XylophoneTile] = [ - XylophoneTile(id: 0, noteNumber: 24, color: .green), - XylophoneTile(id: 1, noteNumber: 26, color: .purple), - XylophoneTile(id: 2, noteNumber: 28, color: .red), - XylophoneTile(id: 3, noteNumber: 29, color: .yellow), - XylophoneTile(id: 4, noteNumber: 31, color: .blue), -] - -public struct XylophoneView: View { - @StateObject var xyloPlayer = MIDIPlayer(name: "Xylophone", samples: xyloSamples) - let defaultTilesSpacing: CGFloat = 40 - let tilesNumber = kListOfXylophoneTiles.count - - public init() { - // Nothing to do - } - - public var body: some View { - HStack(spacing: defaultTilesSpacing) { - ForEach(kListOfXylophoneTiles) { tile in - Button { - xyloPlayer.noteOn(number: tile.noteNumber) - // TODO(@ladislas): Light on Leka lights with tile.color - print("Leka is \(tile.color)") - } label: { - tile.color - } - .buttonStyle(XylophoneTileButtonStyle(index: tile.id, tilesNumber: tilesNumber)) - .compositingGroup() - } - } - } -} - -struct XylophoneView_Previews: - PreviewProvider -{ - static var previews: some View { - XylophoneView() - .previewInterfaceOrientation(.landscapeLeft) - } -} diff --git a/Modules/GameEngineKit/Sources/Staging/XylophoneTile.swift b/Modules/GameEngineKit/Sources/Staging/XylophoneTile.swift deleted file mode 100644 index 00c7a69207..0000000000 --- a/Modules/GameEngineKit/Sources/Staging/XylophoneTile.swift +++ /dev/null @@ -1,80 +0,0 @@ -// Leka - iOS Monorepo -// Copyright 2023 APF France handicap -// SPDX-License-Identifier: Apache-2.0 - -import DesignKit -import SwiftUI - -struct XylophoneTileButtonStyle: ButtonStyle { - let xyloAttachColor = Color(red: 0.87, green: 0.65, blue: 0.54) - let defaultMaxTileHeight: Int = 500 - let defaultTileHeightGap: Int = 250 - let defaultTileWidth: CGFloat = 130 - let defaultTilesScaleFeedback: CGFloat = 0.98 - let defaultTilesRotationFeedback: CGFloat = -1 - - let index: Int - let tilesNumber: Int - - func makeBody(configuration: Self.Configuration) -> some View { - configuration.label - .overlay { - VStack { - Spacer() - Circle() - .fill(xyloAttachColor) - .shadow( - color: .black.opacity(0.4), - radius: 3, x: 0, y: 3 - ) - Spacer() - Circle() - .fill(xyloAttachColor) - .shadow( - color: .black.opacity(0.4), - radius: 3, x: 0, y: 3 - ) - Spacer() - } - .frame(width: 44) - } - .overlay { - RoundedRectangle(cornerRadius: 7, style: .circular) - .stroke(.black.opacity(configuration.isPressed ? 0.3 : 0), lineWidth: 20) - } - .clipShape(RoundedRectangle(cornerRadius: 7, style: .circular)) - .frame(width: defaultTileWidth, height: setSizeFromIndex()) - .scaleEffect( - configuration.isPressed ? defaultTilesScaleFeedback : 1, - anchor: .center - ) - .rotationEffect( - Angle(degrees: configuration.isPressed ? defaultTilesRotationFeedback : 0), - anchor: .center - ) - .shadow( - color: .black.opacity(0.4), - radius: 3, x: 0, y: 3 - ) - } - - private func setSizeFromIndex() -> CGFloat { - let sizeDiff = defaultTileHeightGap / tilesNumber - let tileHeight = defaultMaxTileHeight - index * sizeDiff - - return CGFloat(tileHeight) - } -} - -struct XylophoneTileButtonStyle_Previews: - PreviewProvider -{ - static var previews: some View { - Button { - // Nothing to do - } label: { - Color(.red) - } - .buttonStyle(XylophoneTileButtonStyle(index: 0, tilesNumber: 1)) - } -} diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift new file mode 100644 index 0000000000..10fa66721a --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView+TileButtonStyle.swift @@ -0,0 +1,82 @@ +// Leka - iOS Monorepo +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import DesignKit +import RobotKit +import SwiftUI + +extension MusicalInstrumentView.XylophoneView { + + struct TileButtonStyle: ButtonStyle { + let xyloAttachColor = Color(red: 0.87, green: 0.65, blue: 0.54) + let defaultMaxTileHeight: Int = 500 + let defaultTileHeightGap: Int = 250 + let defaultTileWidth: CGFloat = 130 + let defaultTilesScaleFeedback: CGFloat = 0.98 + let defaultTilesRotationFeedback: CGFloat = -1 + + let index: Int + let tileNumber: Int + + func makeBody(configuration: Self.Configuration) -> some View { + configuration.label + .overlay { + VStack { + Spacer() + Circle() + .fill(xyloAttachColor) + .shadow( + color: .black.opacity(0.4), + radius: 3, x: 0, y: 3 + ) + Spacer() + Circle() + .fill(xyloAttachColor) + .shadow( + color: .black.opacity(0.4), + radius: 3, x: 0, y: 3 + ) + Spacer() + } + .frame(width: 44) + } + .overlay { + RoundedRectangle(cornerRadius: 7, style: .circular) + .stroke(.black.opacity(configuration.isPressed ? 0.3 : 0), lineWidth: 20) + } + .clipShape(RoundedRectangle(cornerRadius: 7, style: .circular)) + .frame(width: defaultTileWidth, height: setSizeFromIndex()) + .scaleEffect( + configuration.isPressed ? defaultTilesScaleFeedback : 1, + anchor: .center + ) + .rotationEffect( + Angle(degrees: configuration.isPressed ? defaultTilesRotationFeedback : 0), + anchor: .center + ) + .shadow( + color: .black.opacity(0.4), + radius: 3, x: 0, y: 3 + ) + } + + private func setSizeFromIndex() -> CGFloat { + let sizeDiff = defaultTileHeightGap / tileNumber + let tileHeight = defaultMaxTileHeight - index * sizeDiff + + return CGFloat(tileHeight) + } + } + +} + +#Preview { + Button { + // Nothing to do + } label: { + Color(.red) + } + .buttonStyle(MusicalInstrumentView.XylophoneView.TileButtonStyle(index: 0, tileNumber: 1)) +} diff --git a/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift new file mode 100644 index 0000000000..1cdd72d76c --- /dev/null +++ b/Modules/GameEngineKit/Sources/_NewSystem/Exercises/Specialized/Instrument/MusicalInstrumentView+XylophoneView.swift @@ -0,0 +1,48 @@ +// Leka - iOS Monorepo +// Copyright 2023 APF France handicap +// SPDX-License-Identifier: Apache-2.0 + +import AudioKit +import RobotKit +import SwiftUI + +extension MusicalInstrumentView { + + struct XylophoneView: View { + @ObservedObject var xyloPlayer: MIDIPlayer + let tilesSpacing: CGFloat + let tileNumber: Int + let tileColors: [Robot.Color] = [.pink, .red, .orange, .yellow, .green, .blue, .purple] + let scale: MIDIScale + + init(midiPlayer: MIDIPlayer, scale: MIDIScale) { + self.xyloPlayer = midiPlayer + self.scale = scale + self.tileNumber = scale.notes.count + self.tilesSpacing = scale.self == .majorPentatonic ? 40 : 16 + } + + var body: some View { + HStack(spacing: tilesSpacing) { + ForEach(0..