diff --git a/Sources/MusicAssistantCore/Domain/SongProject.swift b/Sources/MusicAssistantCore/Domain/SongProject.swift index ae86459..fffb11e 100644 --- a/Sources/MusicAssistantCore/Domain/SongProject.swift +++ b/Sources/MusicAssistantCore/Domain/SongProject.swift @@ -186,6 +186,46 @@ public extension SongProject { maqam = ManualAutoValue(mode: .auto) } + func emotionalArcPoint(for pointID: String) -> EmotionalArcPoint? { + emotionalArc.first { $0.id == pointID } + } + + func emotionalArcPoints(forSectionID sectionID: String) -> [EmotionalArcPoint] { + emotionalArc.filter { $0.sectionID == sectionID } + } + + mutating func upsertEmotionalArcPoint(_ point: EmotionalArcPoint) { + if let index = emotionalArc.firstIndex(where: { $0.id == point.id }) { + emotionalArc[index] = point + } else { + emotionalArc.append(point) + } + } + + mutating func removeEmotionalArcPoint(id pointID: String) { + emotionalArc.removeAll { $0.id == pointID } + } + + func productionDirection(for directionID: String) -> ProductionDirection? { + productionDirections.first { $0.id == directionID } + } + + func productionDirections(forSectionID sectionID: String) -> [ProductionDirection] { + productionDirections.filter { $0.sectionID == sectionID } + } + + mutating func upsertProductionDirection(_ direction: ProductionDirection) { + if let index = productionDirections.firstIndex(where: { $0.id == direction.id }) { + productionDirections[index] = direction + } else { + productionDirections.append(direction) + } + } + + mutating func removeProductionDirection(id directionID: String) { + productionDirections.removeAll { $0.id == directionID } + } + var selectedInstrumentTracks: [InstrumentTrack] { instruments.filter(\.selected) } diff --git a/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift b/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift index e96570a..fa2be4e 100644 --- a/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift +++ b/Tests/MusicAssistantCoreTests/SongProjectModelTests.swift @@ -307,4 +307,62 @@ final class SongProjectModelTests: XCTestCase { XCTAssertEqual(project.scale, ManualAutoValue(mode: .manual, value: "minor")) XCTAssertEqual(project.maqam, ManualAutoValue(mode: .auto)) } + + func testEmotionalArcAndProductionDirectionsAreModeled() { + var project = SongProject( + id: "project-9", + title: "Arc", + idea: "Build from intimate to explosive" + ) + let verseArc = EmotionalArcPoint( + id: "arc-verse", + sectionID: "verse", + emotion: "intimate", + energy: 3, + direction: "restrained" + ) + let chorusArc = EmotionalArcPoint( + id: "arc-chorus", + sectionID: "chorus", + emotion: "euphoric", + energy: 9, + direction: "wide and bright" + ) + let verseProduction = ProductionDirection( + id: "prod-verse", + sectionID: "verse", + text: "keep drums minimal" + ) + let chorusProduction = ProductionDirection( + id: "prod-chorus", + sectionID: "chorus", + text: "add layered harmonies and bigger percussion" + ) + + project.upsertEmotionalArcPoint(verseArc) + project.upsertEmotionalArcPoint(chorusArc) + project.upsertEmotionalArcPoint( + EmotionalArcPoint( + id: "arc-verse", + sectionID: "verse", + emotion: "focused", + energy: 4, + direction: "slightly warmer" + ) + ) + project.upsertProductionDirection(verseProduction) + project.upsertProductionDirection(chorusProduction) + + XCTAssertEqual(project.emotionalArc.count, 2) + XCTAssertEqual(project.emotionalArcPoint(for: "arc-verse")?.emotion, "focused") + XCTAssertEqual(project.emotionalArcPoints(forSectionID: "chorus"), [chorusArc]) + XCTAssertEqual(project.productionDirections(forSectionID: "verse"), [verseProduction]) + XCTAssertEqual(project.productionDirection(for: "prod-chorus"), chorusProduction) + + project.removeEmotionalArcPoint(id: "arc-chorus") + project.removeProductionDirection(id: "prod-verse") + + XCTAssertNil(project.emotionalArcPoint(for: "arc-chorus")) + XCTAssertNil(project.productionDirection(for: "prod-verse")) + } } diff --git a/docs/TASKS.md b/docs/TASKS.md index d47972c..92fa2c9 100644 --- a/docs/TASKS.md +++ b/docs/TASKS.md @@ -27,7 +27,7 @@ requirement is missing and blocks implementation, record it in - [x] Model multiple vocalists and section assignments. - [x] Model language/dialect and Arabic pronunciation settings. - [x] Model BPM, key, scale and maqam with Manual/Auto state. -- [ ] Model emotional arc and production directions. +- [x] Model emotional arc and production directions. - [ ] Add serialization tests. ## Phase 2 --- Project Persistence & Versions