Add vocalist preset selector
This commit is contained in:
@@ -799,6 +799,50 @@ public struct InstrumentPlacement: Codable, Equatable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct VocalistPreset: Codable, Equatable, Identifiable, Sendable {
|
||||
public let id: String
|
||||
public var label: String
|
||||
public var familyCategory: String
|
||||
public var regionOrigin: String?
|
||||
public var voiceType: String?
|
||||
public var genderSelection: String?
|
||||
public var performanceStyle: String?
|
||||
public var aliases: [String]
|
||||
public var searchTerms: [String]
|
||||
|
||||
public init(
|
||||
id: String,
|
||||
label: String,
|
||||
familyCategory: String,
|
||||
regionOrigin: String? = nil,
|
||||
voiceType: String? = nil,
|
||||
genderSelection: String? = nil,
|
||||
performanceStyle: String? = nil,
|
||||
aliases: [String] = [],
|
||||
searchTerms: [String] = []
|
||||
) {
|
||||
self.id = id
|
||||
self.label = label
|
||||
self.familyCategory = familyCategory
|
||||
self.regionOrigin = regionOrigin
|
||||
self.voiceType = voiceType
|
||||
self.genderSelection = genderSelection
|
||||
self.performanceStyle = performanceStyle
|
||||
self.aliases = aliases
|
||||
self.searchTerms = searchTerms
|
||||
}
|
||||
|
||||
public var vocalist: Vocalist {
|
||||
Vocalist(
|
||||
id: id,
|
||||
label: label,
|
||||
voiceType: voiceType,
|
||||
genderSelection: genderSelection,
|
||||
performanceStyle: performanceStyle
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct Vocalist: Codable, Equatable, Identifiable, Sendable {
|
||||
public let id: String
|
||||
public var label: String
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
import Foundation
|
||||
|
||||
public struct LocalVocalistPresetCatalog: Sendable {
|
||||
public let items: [VocalistPreset]
|
||||
|
||||
public init(items: [VocalistPreset] = LocalVocalistPresetCatalog.defaultItems) {
|
||||
self.items = items
|
||||
}
|
||||
|
||||
public var familyCategories: [String] {
|
||||
Array(Set(items.map(\.familyCategory)))
|
||||
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
|
||||
}
|
||||
|
||||
public func vocalists(
|
||||
matching query: String,
|
||||
familyCategory: String? = nil
|
||||
) -> [VocalistPreset] {
|
||||
let searchTokens = Self.searchTokens(for: query)
|
||||
let selectedFamilyCategory = familyCategory?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
|
||||
return items
|
||||
.filter { item in
|
||||
let matchesSearch = searchTokens.isEmpty || searchTokens.allSatisfy { token in
|
||||
Self.searchableTerms(for: item).contains { $0.contains(token) }
|
||||
}
|
||||
let matchesFamily = selectedFamilyCategory?.isEmpty != false || item.familyCategory == selectedFamilyCategory
|
||||
return matchesSearch && matchesFamily
|
||||
}
|
||||
.sorted { $0.label.localizedCaseInsensitiveCompare($1.label) == .orderedAscending }
|
||||
}
|
||||
|
||||
public static let defaultItems: [VocalistPreset] = leadVoices + rapAndSpoken + harmonyAndGroups + arabicAndTraditional
|
||||
|
||||
private static let leadVoices: [VocalistPreset] = [
|
||||
item("lead-female", "Female Lead Vocal", "Lead Voices", "Global", "melodic lead", "female", "expressive lead", aliases: ["female singer"]),
|
||||
item("lead-male", "Male Lead Vocal", "Lead Voices", "Global", "melodic lead", "male", "expressive lead", aliases: ["male singer"]),
|
||||
item("lead-neutral", "Neutral Lead Vocal", "Lead Voices", "Global", "melodic lead", "neutral", "expressive lead"),
|
||||
item("soprano", "Soprano Lead", "Lead Voices", "Global", "soprano", "female", "clear high lead"),
|
||||
item("mezzo-soprano", "Mezzo-Soprano Lead", "Lead Voices", "Global", "mezzo-soprano", "female", "warm lead"),
|
||||
item("alto", "Alto Lead", "Lead Voices", "Global", "alto", "female", "warm low lead"),
|
||||
item("tenor", "Tenor Lead", "Lead Voices", "Global", "tenor", "male", "clear lead"),
|
||||
item("baritone", "Baritone Lead", "Lead Voices", "Global", "baritone", "male", "warm low lead"),
|
||||
item("bass-vocal", "Bass Lead", "Lead Voices", "Global", "bass", "male", "deep lead"),
|
||||
item("child-vocal", "Child Vocal", "Lead Voices", "Global", "child voice", "child", "clear youthful lead")
|
||||
]
|
||||
|
||||
private static let rapAndSpoken: [VocalistPreset] = [
|
||||
item("male-rapper", "Male Rap Vocal", "Rap & Spoken", "Global", "rap", "male", "rhythmic rap", aliases: ["male rapper"], terms: ["راب"]),
|
||||
item("female-rapper", "Female Rap Vocal", "Rap & Spoken", "Global", "rap", "female", "rhythmic rap", aliases: ["female rapper"], terms: ["راب"]),
|
||||
item("neutral-rapper", "Neutral Rap Vocal", "Rap & Spoken", "Global", "rap", "neutral", "rhythmic rap", terms: ["راب"]),
|
||||
item("spoken-word", "Spoken Word", "Rap & Spoken", "Global", "spoken word", "neutral", "narrated delivery", aliases: ["narration"]),
|
||||
item("melodic-rap", "Melodic Rap Vocal", "Rap & Spoken", "Global", "rap/singing", "neutral", "melodic rap")
|
||||
]
|
||||
|
||||
private static let harmonyAndGroups: [VocalistPreset] = [
|
||||
item("female-harmony", "Female Harmony Vocal", "Harmony & Groups", "Global", "harmony", "female", "soft harmony", aliases: ["female backing vocal"]),
|
||||
item("male-harmony", "Male Harmony Vocal", "Harmony & Groups", "Global", "harmony", "male", "soft harmony", aliases: ["male backing vocal"]),
|
||||
item("mixed-harmony", "Mixed Harmony Vocals", "Harmony & Groups", "Global", "harmony", "mixed", "layered harmony"),
|
||||
item("female-choir", "Female Choir", "Harmony & Groups", "Global", "choir", "female", "layered choir"),
|
||||
item("male-choir", "Male Choir", "Harmony & Groups", "Global", "choir", "male", "layered choir"),
|
||||
item("mixed-choir", "Mixed Choir", "Harmony & Groups", "Global", "choir", "mixed", "wide choral arrangement"),
|
||||
item("children-choir", "Children's Choir", "Harmony & Groups", "Global", "children choir", "mixed", "bright choir"),
|
||||
item("duet", "Duet Leads", "Harmony & Groups", "Global", "duet", "mixed", "call and response"),
|
||||
item("vocal-ensemble", "Vocal Ensemble", "Harmony & Groups", "Global", "ensemble", "mixed", "layered ensemble")
|
||||
]
|
||||
|
||||
private static let arabicAndTraditional: [VocalistPreset] = [
|
||||
item("arabic-male-lead", "Arabic Male Lead", "Arabic & Traditional", "Arab World", "ornamented male lead", "male", "Arabic melodic delivery", terms: ["صوت عربي رجالي"]),
|
||||
item("arabic-female-lead", "Arabic Female Lead", "Arabic & Traditional", "Arab World", "ornamented female lead", "female", "Arabic melodic delivery", terms: ["صوت عربي نسائي"]),
|
||||
item("khaleeji-male-lead", "Khaleeji Male Lead", "Arabic & Traditional", "Gulf", "male lead", "male", "Khaleeji delivery", terms: ["خليجي"]),
|
||||
item("khaleeji-female-lead", "Khaleeji Female Lead", "Arabic & Traditional", "Gulf", "female lead", "female", "Khaleeji delivery", terms: ["خليجي"]),
|
||||
item("arabic-chorus", "Arabic Chorus", "Arabic & Traditional", "Arab World", "chorus", "mixed", "Arabic group response", terms: ["كورال عربي"]),
|
||||
item("sufi-ensemble", "Sufi Vocal Ensemble", "Arabic & Traditional", "Middle East & South Asia", "ensemble", "mixed", "devotional call and response"),
|
||||
item("folk-storyteller", "Folk Storyteller", "Arabic & Traditional", "Global", "spoken/sung folk vocal", "neutral", "narrative folk delivery")
|
||||
]
|
||||
|
||||
private static func item(
|
||||
_ id: String,
|
||||
_ label: String,
|
||||
_ familyCategory: String,
|
||||
_ regionOrigin: String? = nil,
|
||||
_ voiceType: String? = nil,
|
||||
_ genderSelection: String? = nil,
|
||||
_ performanceStyle: String? = nil,
|
||||
aliases: [String] = [],
|
||||
terms: [String] = []
|
||||
) -> VocalistPreset {
|
||||
VocalistPreset(
|
||||
id: id,
|
||||
label: label,
|
||||
familyCategory: familyCategory,
|
||||
regionOrigin: regionOrigin,
|
||||
voiceType: voiceType,
|
||||
genderSelection: genderSelection,
|
||||
performanceStyle: performanceStyle,
|
||||
aliases: aliases,
|
||||
searchTerms: terms
|
||||
)
|
||||
}
|
||||
|
||||
private static func searchableTerms(for item: VocalistPreset) -> [String] {
|
||||
searchTokens(
|
||||
for: [
|
||||
item.label,
|
||||
item.familyCategory,
|
||||
item.regionOrigin ?? "",
|
||||
item.voiceType ?? "",
|
||||
item.genderSelection ?? "",
|
||||
item.performanceStyle ?? ""
|
||||
] + item.aliases + item.searchTerms
|
||||
)
|
||||
}
|
||||
|
||||
private static func searchTokens(for query: String) -> [String] {
|
||||
searchTokens(for: [query])
|
||||
}
|
||||
|
||||
private static func searchTokens(for values: [String]) -> [String] {
|
||||
values
|
||||
.joined(separator: " ")
|
||||
.folding(
|
||||
options: [.caseInsensitive, .diacriticInsensitive, .widthInsensitive],
|
||||
locale: Locale(identifier: "en_US_POSIX")
|
||||
)
|
||||
.split(whereSeparator: { $0.isWhitespace || $0 == "-" || $0 == "/" })
|
||||
.map(String.init)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user