Model vocalist section assignments

This commit is contained in:
Codex
2026-09-13 13:19:33 +02:00
parent 1993800f27
commit 1b523a924d
3 changed files with 88 additions and 1 deletions
@@ -169,6 +169,44 @@ public extension SongProject {
}
}
var vocalistIDs: Set<String> {
Set(vocalists.map(\.id))
}
func vocalist(for vocalistID: String) -> Vocalist? {
vocalists.first { $0.id == vocalistID }
}
func vocalistsAssigned(toSectionID sectionID: String) -> [Vocalist] {
vocalists.filter { $0.assignedSections.contains(sectionID) }
}
mutating func upsertVocalist(_ vocalist: Vocalist) {
var normalizedVocalist = vocalist
normalizedVocalist.assignedSections = Self.uniqueOrderedIDs(vocalist.assignedSections)
if let index = vocalists.firstIndex(where: { $0.id == vocalist.id }) {
vocalists[index] = normalizedVocalist
} else {
vocalists.append(normalizedVocalist)
}
}
mutating func removeVocalist(id vocalistID: String) {
vocalists.removeAll { $0.id == vocalistID }
}
mutating func assignSection(_ sectionID: String, toVocalistID vocalistID: String) {
guard let index = vocalists.firstIndex(where: { $0.id == vocalistID }) else { return }
guard !vocalists[index].assignedSections.contains(sectionID) else { return }
vocalists[index].assignedSections.append(sectionID)
}
mutating func unassignSection(_ sectionID: String, fromVocalistID vocalistID: String) {
guard let index = vocalists.firstIndex(where: { $0.id == vocalistID }) else { return }
vocalists[index].assignedSections.removeAll { $0 == sectionID }
}
mutating func appendSection(_ section: SongSection) {
var orderedSection = section
orderedSection.order = nextSectionOrder
@@ -200,6 +238,11 @@ public extension SongProject {
return orderedSection
}
}
private static func uniqueOrderedIDs(_ ids: [String]) -> [String] {
var seenIDs = Set<String>()
return ids.filter { seenIDs.insert($0).inserted }
}
}
public enum ConversationMode: String, Codable, Equatable, Sendable {
@@ -191,4 +191,48 @@ final class SongProjectModelTests: XCTestCase {
XCTAssertEqual(project.selectedInstrumentIDs, ["violin"])
XCTAssertEqual(project.instrumentTrack(for: "oud")?.placements, [introPlacement])
}
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"))
}
}
+1 -1
View File
@@ -24,7 +24,7 @@ requirement is missing and blocks implementation, record it in
- [x] Model genres/styles as multi-select.
- [x] Model song sections with order and optional timestamps.
- [x] Model instruments, variants, roles and placements.
- [ ] Model multiple vocalists and section assignments.
- [x] Model multiple vocalists and section assignments.
- [ ] Model language/dialect and Arabic pronunciation settings.
- [ ] Model BPM, key, scale and maqam with Manual/Auto state.
- [ ] Model emotional arc and production directions.