Model vocalist section assignments
This commit is contained in:
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user