626 lines
25 KiB
Swift
626 lines
25 KiB
Swift
import Foundation
|
|
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 testVersionHistoryStoresIndependentSnapshots() {
|
|
var project = SongProject(
|
|
id: "versioned-project",
|
|
title: "First Title",
|
|
idea: "Original idea",
|
|
genres: [GenreStyle(id: "folk", name: "Folk")]
|
|
)
|
|
let createdAt = Date(timeIntervalSince1970: 500)
|
|
|
|
let version = project.createVersion(
|
|
id: "version-1",
|
|
label: "Initial snapshot",
|
|
createdAt: createdAt
|
|
)
|
|
|
|
project.title = "Changed Title"
|
|
project.idea = "Changed idea"
|
|
project.selectGenreStyle(GenreStyle(id: "electronic", name: "Electronic"))
|
|
|
|
XCTAssertEqual(project.versions.count, 1)
|
|
XCTAssertEqual(project.version(id: "version-1"), version)
|
|
XCTAssertEqual(project.versions.first?.label, "Initial snapshot")
|
|
XCTAssertEqual(project.versions.first?.createdAt, createdAt)
|
|
XCTAssertEqual(project.versions.first?.snapshot.title, "First Title")
|
|
XCTAssertEqual(project.versions.first?.snapshot.idea, "Original idea")
|
|
XCTAssertEqual(project.versions.first?.snapshot.genres, [GenreStyle(id: "folk", name: "Folk")])
|
|
XCTAssertEqual(project.title, "Changed Title")
|
|
XCTAssertEqual(project.genres.map(\.id), ["folk", "electronic"])
|
|
}
|
|
|
|
func testRestoresPreviousVersionWithoutDestroyingLaterVersions() {
|
|
let createdAt = Date(timeIntervalSince1970: 100)
|
|
let restoredAt = Date(timeIntervalSince1970: 900)
|
|
var project = SongProject(
|
|
id: "restorable-project",
|
|
title: "First Title",
|
|
idea: "Original idea",
|
|
duration: SongDuration(seconds: 180),
|
|
genres: [GenreStyle(id: "folk", name: "Folk")],
|
|
instruments: [InstrumentTrack(instrumentId: "piano")],
|
|
lyrics: Lyrics(text: "Original lyrics"),
|
|
createdAt: createdAt,
|
|
updatedAt: Date(timeIntervalSince1970: 200)
|
|
)
|
|
|
|
_ = project.createVersion(
|
|
id: "version-1",
|
|
label: "Original direction",
|
|
createdAt: Date(timeIntervalSince1970: 300)
|
|
)
|
|
|
|
project.title = "Second Title"
|
|
project.idea = "Second idea"
|
|
project.duration = SongDuration(seconds: 210)
|
|
project.selectGenreStyle(GenreStyle(id: "electronic", name: "Electronic"))
|
|
project.selectInstrument(InstrumentTrack(instrumentId: "oud"))
|
|
project.lyrics = Lyrics(text: "Second lyrics")
|
|
_ = project.createVersion(
|
|
id: "version-2",
|
|
label: "Later direction",
|
|
createdAt: Date(timeIntervalSince1970: 400)
|
|
)
|
|
|
|
project.title = "Current Title"
|
|
project.idea = "Current idea"
|
|
project.duration = SongDuration(seconds: 240)
|
|
project.deselectGenreStyle(id: "folk")
|
|
project.lyrics = Lyrics(text: "Current lyrics")
|
|
let versionsBeforeRestore = project.versions
|
|
|
|
XCTAssertTrue(project.restoreVersion(id: "version-1", restoredAt: restoredAt))
|
|
|
|
XCTAssertEqual(project.title, "First Title")
|
|
XCTAssertEqual(project.idea, "Original idea")
|
|
XCTAssertEqual(project.duration, SongDuration(seconds: 180))
|
|
XCTAssertEqual(project.genres, [GenreStyle(id: "folk", name: "Folk")])
|
|
XCTAssertEqual(project.instruments, [InstrumentTrack(instrumentId: "piano")])
|
|
XCTAssertEqual(project.lyrics, Lyrics(text: "Original lyrics"))
|
|
XCTAssertEqual(project.createdAt, createdAt)
|
|
XCTAssertEqual(project.updatedAt, restoredAt)
|
|
XCTAssertEqual(project.versions, versionsBeforeRestore)
|
|
XCTAssertEqual(project.version(id: "version-2")?.snapshot.title, "Second Title")
|
|
|
|
let restoredProject = project
|
|
XCTAssertFalse(project.restoreVersion(id: "missing-version", restoredAt: Date(timeIntervalSince1970: 1_000)))
|
|
XCTAssertEqual(project, restoredProject)
|
|
}
|
|
|
|
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])
|
|
}
|
|
|
|
func testCatalogSelectionCanBeToggledWithoutReplacingExistingArrangement() {
|
|
let oudPlacement = InstrumentPlacement(sectionId: "intro", direction: "Opening melody")
|
|
var project = SongProject(
|
|
id: "catalog-selection-project",
|
|
title: "Catalog Selection",
|
|
idea: "Select and deselect instruments",
|
|
instruments: [
|
|
InstrumentTrack(
|
|
instrumentId: "oud",
|
|
selected: false,
|
|
variant: "Arabic oud",
|
|
playingStyle: "tremolo",
|
|
role: "main hook",
|
|
autoArrangementEnabled: false,
|
|
placements: [oudPlacement]
|
|
)
|
|
]
|
|
)
|
|
|
|
project.setInstrumentSelected(id: "oud", isSelected: true, variant: "Oud")
|
|
|
|
XCTAssertTrue(project.isInstrumentSelected(id: "oud"))
|
|
XCTAssertEqual(project.instrumentTrack(for: "oud")?.variant, "Arabic oud")
|
|
XCTAssertEqual(project.instrumentTrack(for: "oud")?.role, "main hook")
|
|
XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [oudPlacement])
|
|
|
|
project.setInstrumentSelected(id: "oud", isSelected: false)
|
|
project.setInstrumentSelected(id: "violin", isSelected: true, variant: "Violin")
|
|
|
|
XCTAssertFalse(project.isInstrumentSelected(id: "oud"))
|
|
XCTAssertTrue(project.isInstrumentSelected(id: "violin"))
|
|
XCTAssertEqual(project.instrumentTrack(for: "violin")?.variant, "Violin")
|
|
}
|
|
|
|
func testMultipleVocalistsCanBeAssignedToSections() {
|
|
var project = SongProject(
|
|
id: "project-6",
|
|
title: "Vocal Arrangement",
|
|
idea: "Lead and harmony share selected sections"
|
|
)
|
|
let lead = Vocalist(
|
|
id: "lead",
|
|
label: "Lead",
|
|
voiceType: "tenor",
|
|
genderSelection: "male",
|
|
performanceStyle: "sung",
|
|
assignedSections: ["verse", "verse"]
|
|
)
|
|
let harmony = Vocalist(
|
|
id: "harmony",
|
|
label: "Harmony",
|
|
voiceType: "alto",
|
|
genderSelection: "female",
|
|
performanceStyle: "harmony"
|
|
)
|
|
|
|
project.upsertVocalist(lead)
|
|
project.upsertVocalist(harmony)
|
|
project.assignSection("chorus", toVocalistID: "lead")
|
|
project.assignSection("chorus", toVocalistID: "lead")
|
|
project.assignSection("chorus", toVocalistID: "harmony")
|
|
project.assignSection("bridge", toVocalistID: "harmony")
|
|
|
|
XCTAssertEqual(project.vocalistIDs, ["lead", "harmony"])
|
|
XCTAssertEqual(project.vocalist(for: "lead")?.assignedSections, ["verse", "chorus"])
|
|
XCTAssertEqual(project.vocalist(for: "harmony")?.assignedSections, ["chorus", "bridge"])
|
|
XCTAssertEqual(project.vocalistsAssigned(toSectionID: "chorus").map(\.id), ["lead", "harmony"])
|
|
|
|
project.unassignSection("chorus", fromVocalistID: "lead")
|
|
|
|
XCTAssertEqual(project.vocalist(for: "lead")?.assignedSections, ["verse"])
|
|
XCTAssertEqual(project.vocalistsAssigned(toSectionID: "chorus").map(\.id), ["harmony"])
|
|
|
|
project.removeVocalist(id: "harmony")
|
|
|
|
XCTAssertNil(project.vocalist(for: "harmony"))
|
|
}
|
|
|
|
func testLanguagesDialectsAndArabicPronunciationSettingsAreModeled() {
|
|
var project = SongProject(
|
|
id: "project-7",
|
|
title: "Multilingual Song",
|
|
idea: "Arabic and English hook"
|
|
)
|
|
let arabic = LanguageSelection(id: "arabic", name: "Arabic")
|
|
let english = LanguageSelection(id: "english", name: "English")
|
|
let levantine = DialectSelection(id: "levantine", name: "Levantine", languageID: arabic.id)
|
|
let gulf = DialectSelection(id: "gulf", name: "Gulf", languageID: arabic.id)
|
|
|
|
project.upsertLanguage(arabic)
|
|
project.upsertLanguage(english)
|
|
project.upsertLanguage(LanguageSelection(id: "english", name: "English"))
|
|
project.upsertDialect(levantine)
|
|
project.upsertDialect(gulf)
|
|
project.arabicPronunciationSettings = ArabicPronunciationSettings(
|
|
isEnabled: true,
|
|
diacritizationPolicy: .pronunciationTargeted,
|
|
tanweenPolicy: .preserveUserInput,
|
|
userNotes: "Preserve dialect phrasing",
|
|
preservedSpellings: ["ya habibi"]
|
|
)
|
|
|
|
XCTAssertEqual(project.languageIDs, ["arabic", "english"])
|
|
XCTAssertEqual(project.dialectIDs, ["levantine", "gulf"])
|
|
XCTAssertEqual(project.language(for: arabic.id), arabic)
|
|
XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [levantine, gulf])
|
|
XCTAssertTrue(project.arabicPronunciationSettings.isEnabled)
|
|
XCTAssertEqual(project.arabicPronunciationSettings.diacritizationPolicy, .pronunciationTargeted)
|
|
XCTAssertEqual(project.arabicPronunciationSettings.tanweenPolicy, .preserveUserInput)
|
|
|
|
project.removeDialect(id: gulf.id)
|
|
|
|
XCTAssertNil(project.dialect(for: gulf.id))
|
|
XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [levantine])
|
|
|
|
project.removeLanguage(id: arabic.id)
|
|
|
|
XCTAssertNil(project.language(for: arabic.id))
|
|
XCTAssertEqual(project.dialects(forLanguageID: arabic.id), [])
|
|
XCTAssertEqual(project.languageIDs, ["english"])
|
|
}
|
|
|
|
func testApplyingReviewedArabicLyricsUpdatesMatchingSunoLyrics() {
|
|
var project = SongProject(
|
|
id: "project-arabic-review",
|
|
title: "Arabic Review",
|
|
idea: "Review pronunciation",
|
|
lyrics: Lyrics(text: "Original lyrics"),
|
|
sunoOutput: SunoOutput(lyricsText: "Original lyrics", stylePrompt: "Arabic pop")
|
|
)
|
|
|
|
project.applyReviewedArabicLyrics("Reviewed lyrics")
|
|
|
|
XCTAssertEqual(project.lyrics.text, "Reviewed lyrics")
|
|
XCTAssertEqual(project.sunoOutput?.lyricsText, "Reviewed lyrics")
|
|
}
|
|
|
|
func testApplyingReviewedArabicLyricsPreservesEditedSunoLyrics() {
|
|
var project = SongProject(
|
|
id: "project-arabic-review-manual-output",
|
|
title: "Arabic Review",
|
|
idea: "Review pronunciation",
|
|
lyrics: Lyrics(text: "Original lyrics"),
|
|
sunoOutput: SunoOutput(lyricsText: "Manually edited output", stylePrompt: "Arabic pop")
|
|
)
|
|
|
|
project.applyReviewedArabicLyrics("Reviewed lyrics")
|
|
|
|
XCTAssertEqual(project.lyrics.text, "Reviewed lyrics")
|
|
XCTAssertEqual(project.sunoOutput?.lyricsText, "Manually edited output")
|
|
}
|
|
|
|
func testMusicalParametersSupportManualAndAutoModes() {
|
|
var project = SongProject(
|
|
id: "project-8",
|
|
title: "Musical Parameters",
|
|
idea: "Control tempo and harmony"
|
|
)
|
|
|
|
project.setBPMManually(104)
|
|
project.setKeyManually("D")
|
|
project.setScaleAutomatic()
|
|
project.setMaqamManually("Bayati")
|
|
|
|
XCTAssertEqual(project.bpm, ManualAutoValue(mode: .manual, value: 104))
|
|
XCTAssertEqual(project.key, ManualAutoValue(mode: .manual, value: "D"))
|
|
XCTAssertEqual(project.scale, ManualAutoValue(mode: .auto))
|
|
XCTAssertEqual(project.maqam, ManualAutoValue(mode: .manual, value: "Bayati"))
|
|
|
|
project.setBPMAutomatic()
|
|
project.setKeyAutomatic()
|
|
project.setScaleManually("minor")
|
|
project.setMaqamAutomatic()
|
|
|
|
XCTAssertEqual(project.bpm, ManualAutoValue(mode: .auto))
|
|
XCTAssertEqual(project.key, ManualAutoValue(mode: .auto))
|
|
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"))
|
|
}
|
|
|
|
func testSongProjectRoundTripsThroughJSONSerialization() throws {
|
|
let createdAt = Date(timeIntervalSince1970: 1_700_000_000)
|
|
let updatedAt = Date(timeIntervalSince1970: 1_700_000_120)
|
|
let generatedAt = Date(timeIntervalSince1970: 1_700_000_240)
|
|
let section = SongSection(
|
|
id: "verse",
|
|
type: .verse,
|
|
title: "Verse",
|
|
order: 0,
|
|
startTime: 8,
|
|
endTime: 38,
|
|
lyrics: "Draft verse",
|
|
emotion: "reflective",
|
|
energy: 4,
|
|
vocalDirection: "close vocal",
|
|
productionDirection: "subtle pad"
|
|
)
|
|
let instrument = InstrumentTrack(
|
|
instrumentId: "oud",
|
|
selected: true,
|
|
variant: "Arabic oud",
|
|
playingStyle: "picked",
|
|
role: "lead motif",
|
|
autoArrangementEnabled: false,
|
|
placements: [
|
|
InstrumentPlacement(
|
|
sectionId: section.id,
|
|
startTime: section.startTime,
|
|
endTime: section.endTime,
|
|
direction: "answer the vocal"
|
|
)
|
|
]
|
|
)
|
|
var project = SongProject(
|
|
id: "serialized-project",
|
|
title: "Serializable Song",
|
|
idea: "A multilingual cinematic song",
|
|
duration: SongDuration(seconds: 210, description: "about three and a half minutes"),
|
|
conversationMode: .discuss,
|
|
languages: [LanguageSelection(id: "arabic", name: "Arabic")],
|
|
dialects: [DialectSelection(id: "levantine", name: "Levantine", languageID: "arabic")],
|
|
arabicPronunciationSettings: ArabicPronunciationSettings(
|
|
isEnabled: true,
|
|
diacritizationPolicy: .pronunciationTargeted,
|
|
tanweenPolicy: .preserveUserInput,
|
|
userNotes: "Keep dialect phrasing natural",
|
|
preservedSpellings: ["habibi"]
|
|
),
|
|
genres: [GenreStyle(id: "cinematic-pop", name: "Cinematic Pop", aliases: ["orchestral pop"])],
|
|
moods: [MoodTag(id: "yearning", name: "Yearning")],
|
|
emotionalArc: [
|
|
EmotionalArcPoint(
|
|
id: "arc-verse",
|
|
sectionID: section.id,
|
|
emotion: "reflective",
|
|
energy: 4,
|
|
direction: "slowly opening"
|
|
)
|
|
],
|
|
bpm: ManualAutoValue(mode: .manual, value: 92),
|
|
key: ManualAutoValue(mode: .manual, value: "D"),
|
|
scale: ManualAutoValue(mode: .auto),
|
|
maqam: ManualAutoValue(mode: .manual, value: "Hijaz"),
|
|
sections: [section],
|
|
instruments: [instrument],
|
|
vocalists: [
|
|
Vocalist(
|
|
id: "lead",
|
|
label: "Lead",
|
|
voiceType: "tenor",
|
|
genderSelection: "male",
|
|
performanceStyle: "sung",
|
|
assignedSections: [section.id]
|
|
)
|
|
],
|
|
lyrics: Lyrics(text: "Full lyric draft", sectionTexts: [section.id: "Draft verse"]),
|
|
productionDirections: [
|
|
ProductionDirection(id: "prod-verse", sectionID: section.id, text: "keep percussion sparse")
|
|
],
|
|
sunoOutput: SunoOutput(
|
|
lyricsText: "Full lyric draft",
|
|
stylePrompt: "Cinematic pop with oud",
|
|
additionalFields: ["style": "cinematic"],
|
|
generatedAt: generatedAt
|
|
),
|
|
createdAt: createdAt,
|
|
updatedAt: updatedAt
|
|
)
|
|
project.versions = [
|
|
SongVersion(
|
|
id: "version-1",
|
|
label: "Initial approved direction",
|
|
snapshot: SongProjectSnapshot(project: project),
|
|
createdAt: updatedAt
|
|
)
|
|
]
|
|
|
|
let encoder = JSONEncoder()
|
|
let data = try encoder.encode(project)
|
|
let decodedProject = try JSONDecoder().decode(SongProject.self, from: data)
|
|
|
|
XCTAssertEqual(decodedProject, project)
|
|
}
|
|
}
|