Implement initial song project domain models

This commit is contained in:
Codex
2026-09-13 13:18:08 +02:00
parent 57867bb3cc
commit 1993800f27
19 changed files with 1359 additions and 4 deletions
@@ -0,0 +1,194 @@
import MusicAssistantCore
import XCTest
final class SongProjectModelTests: XCTestCase {
func testSongProjectStoresCoreDomainCollections() {
let section = SongSection(
id: "section-verse-1",
type: .verse,
title: "Verse 1",
startTime: 12,
endTime: 42,
lyrics: "Draft verse",
emotion: "hopeful",
energy: 6,
vocalDirection: "soft lead",
productionDirection: "light percussion"
)
let placement = InstrumentPlacement(
sectionId: section.id,
startTime: section.startTime,
endTime: section.endTime,
direction: "enter after the first line"
)
let instrument = InstrumentTrack(
instrumentId: "oud",
selected: true,
variant: "Arabic oud",
playingStyle: "picked",
role: "main melodic hook",
autoArrangementEnabled: false,
placements: [placement]
)
let vocalist = Vocalist(
id: "lead",
label: "Lead vocalist",
voiceType: "warm tenor",
genderSelection: "male",
performanceStyle: "sung",
assignedSections: [section.id]
)
let project = SongProject(
id: "project-1",
title: "Desert Lights",
idea: "A hopeful Arabic pop song",
duration: SongDuration(seconds: 180),
conversationMode: .discuss,
languages: [LanguageSelection(id: "arabic", name: "Arabic")],
dialects: [DialectSelection(id: "levantine", name: "Levantine", languageID: "arabic")],
arabicPronunciationSettings: ArabicPronunciationSettings(isEnabled: true),
genres: [GenreStyle(id: "arabic-pop", name: "Arabic Pop")],
moods: [MoodTag(id: "hopeful", name: "Hopeful")],
emotionalArc: [EmotionalArcPoint(id: "arc-1", sectionID: section.id, emotion: "hopeful")],
bpm: ManualAutoValue(mode: .manual, value: 96),
key: ManualAutoValue(mode: .auto),
scale: ManualAutoValue(mode: .auto),
maqam: ManualAutoValue(mode: .manual, value: "Hijaz"),
sections: [section],
instruments: [instrument],
vocalists: [vocalist],
lyrics: Lyrics(text: "Draft lyrics", sectionTexts: [section.id: "Draft verse"]),
productionDirections: [ProductionDirection(id: "prod-1", sectionID: section.id, text: "add lift")],
sunoOutput: SunoOutput(lyricsText: "Draft lyrics", stylePrompt: "Arabic pop with oud")
)
XCTAssertEqual(project.sections.first?.type, .verse)
XCTAssertEqual(project.instruments.first?.instrumentId, "oud")
XCTAssertEqual(project.instruments.first?.placements.first?.sectionId, section.id)
XCTAssertEqual(project.vocalists.first?.assignedSections, [section.id])
XCTAssertEqual(project.bpm?.mode, .manual)
XCTAssertEqual(project.maqam?.value, "Hijaz")
}
func testSongProjectSnapshotCopiesVersionableState() {
let project = SongProject(
id: "project-2",
title: "Night Drive",
idea: "Electronic track",
genres: [GenreStyle(id: "synthwave", name: "Synthwave")],
instruments: [InstrumentTrack(instrumentId: "synthesizer")]
)
let snapshot = SongProjectSnapshot(project: project)
let version = SongVersion(label: "Initial direction", snapshot: snapshot)
XCTAssertEqual(version.snapshot.title, project.title)
XCTAssertEqual(version.snapshot.idea, project.idea)
XCTAssertEqual(version.snapshot.genres, project.genres)
XCTAssertEqual(version.snapshot.instruments, project.instruments)
}
func testGenreStylesAreModeledAsMultiSelect() {
let arabicPop = GenreStyle(id: "arabic-pop", name: "Arabic Pop")
let synthwave = GenreStyle(id: "synthwave", name: "Synthwave")
var project = SongProject(
id: "project-3",
title: "Hybrid",
idea: "Blend traditional melody with electronic production"
)
project.selectGenreStyle(arabicPop)
project.selectGenreStyle(synthwave)
project.selectGenreStyle(arabicPop)
XCTAssertEqual(project.genres, [arabicPop, synthwave])
XCTAssertTrue(project.isGenreStyleSelected(arabicPop))
XCTAssertEqual(project.selectedGenreStyleIDs, ["arabic-pop", "synthwave"])
project.deselectGenreStyle(id: arabicPop.id)
XCTAssertEqual(project.genres, [synthwave])
XCTAssertFalse(project.isGenreStyleSelected(arabicPop))
}
func testSongSectionsKeepExplicitOrderAndOptionalTimestamps() {
var project = SongProject(
id: "project-4",
title: "Section Flow",
idea: "Build a classic song structure"
)
let intro = SongSection(id: "intro", type: .intro, title: "Intro")
let verse = SongSection(
id: "verse",
type: .verse,
title: "Verse",
startTime: 15,
endTime: 45
)
let chorus = SongSection(id: "chorus", type: .chorus, title: "Chorus")
project.appendSection(verse)
project.appendSection(chorus)
project.appendSection(intro)
project.moveSection(id: intro.id, toOrder: 0)
XCTAssertEqual(project.orderedSections.map(\.id), ["intro", "verse", "chorus"])
XCTAssertNil(project.orderedSections.first?.startTime)
XCTAssertNil(project.orderedSections.first?.endTime)
XCTAssertEqual(project.orderedSections[1].startTime, 15)
XCTAssertEqual(project.orderedSections[1].endTime, 45)
XCTAssertEqual(project.orderedSections.map(\.order), [0, 1, 2])
}
func testInstrumentsTrackVariantsRolesSelectionAndPlacements() {
var project = SongProject(
id: "project-5",
title: "Instrument Plan",
idea: "Feature oud and violin with clear entries"
)
let oud = InstrumentTrack(
instrumentId: "oud",
selected: true,
variant: "Arabic oud",
playingStyle: "tremolo",
role: "main hook",
autoArrangementEnabled: false
)
let introPlacement = InstrumentPlacement(
sectionId: "intro",
startTime: 0,
endTime: 12,
direction: "solo intro"
)
let chorusPlacement = InstrumentPlacement(
sectionId: "chorus",
direction: "double the vocal melody"
)
project.selectInstrument(oud)
project.addInstrumentPlacement(introPlacement, to: oud.instrumentId)
project.addInstrumentPlacement(chorusPlacement, to: "violin")
project.configureInstrument(
id: "violin",
variant: "solo violin",
playingStyle: "legato",
role: "countermelody",
autoArrangementEnabled: true
)
XCTAssertEqual(project.selectedInstrumentIDs, ["oud", "violin"])
XCTAssertEqual(project.instrumentTrack(for: "oud")?.variant, "Arabic oud")
XCTAssertEqual(project.instrumentTrack(for: "oud")?.role, "main hook")
XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [introPlacement])
XCTAssertEqual(project.instrumentTrack(for: "violin")?.variant, "solo violin")
XCTAssertEqual(project.instrumentTrack(for: "violin")?.playingStyle, "legato")
XCTAssertEqual(project.instrumentTrack(for: "violin")?.role, "countermelody")
XCTAssertEqual(project.instrumentTrack(for: "violin")?.placements, [chorusPlacement])
project.deselectInstrument(id: "oud")
XCTAssertFalse(project.isInstrumentSelected(id: "oud"))
XCTAssertEqual(project.selectedInstrumentIDs, ["violin"])
XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [introPlacement])
}
}