Compare commits

...
4 Commits
Author SHA1 Message Date
diyaa 9ddac746e5 Add instrument family filtering 2026-09-14 01:04:44 +02:00
diyaa ecbf07d1ea Add searchable instrument browser 2026-09-14 00:55:41 +02:00
diyaa b2bf04e78b Confirm catalog data separation 2026-09-14 00:31:11 +02:00
diyaa b51102b65d Add worldwide instrument catalog 2026-09-13 23:52:07 +02:00
5 changed files with 464 additions and 4 deletions
@@ -0,0 +1,72 @@
import MusicAssistantCore
import SwiftUI
struct InstrumentBrowserView: View {
private let catalog: LocalInstrumentCatalog
@State private var searchText = ""
@State private var selectedFamilyCategory: String?
@Environment(\.dismiss) private var dismiss
init(catalog: LocalInstrumentCatalog = LocalInstrumentCatalog()) {
self.catalog = catalog
}
var body: some View {
NavigationStack {
VStack(spacing: 0) {
Picker("Family", selection: $selectedFamilyCategory) {
Text("All families").tag(String?.none)
ForEach(catalog.familyCategories, id: \.self) { familyCategory in
Text(familyCategory).tag(Optional(familyCategory))
}
}
.pickerStyle(.menu)
.padding(.horizontal)
.padding(.vertical, 8)
Divider()
List(
catalog.instruments(
matching: searchText,
familyCategory: selectedFamilyCategory
),
id: \.id
) { instrument in
VStack(alignment: .leading, spacing: 4) {
Text(instrument.name)
.font(.body.weight(.medium))
HStack(spacing: 6) {
Text(instrument.familyCategory)
if let regionOrigin = instrument.regionOrigin {
Text(regionOrigin)
}
}
.font(.caption)
.foregroundStyle(.secondary)
.lineLimit(1)
}
.padding(.vertical, 2)
}
}
.navigationTitle("Instrument Catalog")
.searchable(text: $searchText, prompt: "Search instruments")
.toolbar {
ToolbarItem(placement: .cancellationAction) {
Button("Done") {
dismiss()
}
}
}
}
.frame(minWidth: 460, minHeight: 520)
}
}
#Preview {
InstrumentBrowserView()
}
@@ -9,6 +9,7 @@ struct ProjectInspectorView: View {
@State private var newInstrumentName = "" @State private var newInstrumentName = ""
@State private var newVocalistLabel = "" @State private var newVocalistLabel = ""
@State private var newLanguageName = "" @State private var newLanguageName = ""
@State private var isInstrumentBrowserPresented = false
@State private var isSaving = false @State private var isSaving = false
@State private var saveStatus: SaveStatus? @State private var saveStatus: SaveStatus?
@@ -62,6 +63,12 @@ struct ProjectInspectorView: View {
} }
Section("Instruments") { Section("Instruments") {
Button {
isInstrumentBrowserPresented = true
} label: {
Label("Browse catalog", systemImage: "magnifyingglass")
}
ForEach($project.instruments, id: \.instrumentId) { $instrument in ForEach($project.instruments, id: \.instrumentId) { $instrument in
Toggle(isOn: $instrument.selected) { Toggle(isOn: $instrument.selected) {
Text(instrument.variant?.isEmpty == false ? instrument.variant ?? instrument.instrumentId : instrument.instrumentId) Text(instrument.variant?.isEmpty == false ? instrument.variant ?? instrument.instrumentId : instrument.instrumentId)
@@ -138,6 +145,9 @@ struct ProjectInspectorView: View {
} }
} }
.background(Color(nsColor: .windowBackgroundColor)) .background(Color(nsColor: .windowBackgroundColor))
.sheet(isPresented: $isInstrumentBrowserPresented) {
InstrumentBrowserView()
}
} }
private var durationSecondsBinding: Binding<String> { private var durationSecondsBinding: Binding<String> {
@@ -0,0 +1,328 @@
import Foundation
public struct LocalInstrumentCatalog: Sendable {
public let items: [InstrumentCatalogItem]
public init(items: [InstrumentCatalogItem] = LocalInstrumentCatalog.worldwideItems) {
self.items = items
}
public var familyCategories: [String] {
Array(Set(items.map(\.familyCategory)))
.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending }
}
public func instruments(
matching query: String,
familyCategory: String? = nil
) -> [InstrumentCatalogItem] {
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.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending }
}
public static let worldwideItems: [InstrumentCatalogItem] =
western + middleEastern + african + southAsian + eastAsian + southeastAsian + latinAmerican + folkAndModern
private static let western: [InstrumentCatalogItem] = [
item("piano", "Piano", "Keyboard", "Western"),
item("upright-piano", "Upright Piano", "Keyboard", "Western"),
item("grand-piano", "Grand Piano", "Keyboard", "Western"),
item("electric-piano", "Electric Piano", "Keyboard", "Western"),
item("harpsichord", "Harpsichord", "Keyboard", "Western"),
item("celesta", "Celesta", "Keyboard", "Western"),
item("pipe-organ", "Pipe Organ", "Keyboard", "Western"),
item("electric-organ", "Electric Organ", "Keyboard", "Western"),
item("accordion", "Accordion", "Free reed", "Europe"),
item("concertina", "Concertina", "Free reed", "Europe"),
item("melodica", "Melodica", "Free reed", "Western"),
item("violin", "Violin", "Bowed strings", "Western", aliases: ["fiddle"]),
item("viola", "Viola", "Bowed strings", "Western"),
item("cello", "Cello", "Bowed strings", "Western", aliases: ["violoncello"]),
item("double-bass", "Double Bass", "Bowed strings", "Western", aliases: ["upright bass"]),
item("concert-harp", "Concert Harp", "Plucked strings", "Western"),
item("classical-guitar", "Classical Guitar", "Plucked strings", "Western", aliases: ["nylon-string guitar"]),
item("acoustic-guitar", "Acoustic Guitar", "Plucked strings", "Western"),
item("electric-guitar", "Electric Guitar", "Plucked strings", "Western"),
item("bass-guitar", "Bass Guitar", "Plucked strings", "Western"),
item("twelve-string-guitar", "Twelve-string Guitar", "Plucked strings", "Western"),
item("mandolin", "Mandolin", "Plucked strings", "Europe"),
item("banjo", "Banjo", "Plucked strings", "North America"),
item("ukulele", "Ukulele", "Plucked strings", "Oceania"),
item("resonator-guitar", "Resonator Guitar", "Plucked strings", "North America", aliases: ["dobro"]),
item("lap-steel-guitar", "Lap Steel Guitar", "Plucked strings", "North America"),
item("pedal-steel-guitar", "Pedal Steel Guitar", "Plucked strings", "North America"),
item("flute", "Concert Flute", "Woodwinds", "Western", aliases: ["flute"]),
item("piccolo", "Piccolo", "Woodwinds", "Western"),
item("recorder", "Recorder", "Woodwinds", "Europe"),
item("oboe", "Oboe", "Woodwinds", "Western"),
item("english-horn", "English Horn", "Woodwinds", "Western", aliases: ["cor anglais"]),
item("bassoon", "Bassoon", "Woodwinds", "Western"),
item("contrabassoon", "Contrabassoon", "Woodwinds", "Western"),
item("clarinet", "Clarinet", "Woodwinds", "Western"),
item("bass-clarinet", "Bass Clarinet", "Woodwinds", "Western"),
item("soprano-saxophone", "Soprano Saxophone", "Woodwinds", "Western", aliases: ["soprano sax"]),
item("alto-saxophone", "Alto Saxophone", "Woodwinds", "Western", aliases: ["alto sax"]),
item("tenor-saxophone", "Tenor Saxophone", "Woodwinds", "Western", aliases: ["tenor sax"]),
item("baritone-saxophone", "Baritone Saxophone", "Woodwinds", "Western", aliases: ["baritone sax"]),
item("trumpet", "Trumpet", "Brass", "Western"),
item("cornet", "Cornet", "Brass", "Western"),
item("flugelhorn", "Flugelhorn", "Brass", "Western"),
item("trombone", "Trombone", "Brass", "Western"),
item("bass-trombone", "Bass Trombone", "Brass", "Western"),
item("french-horn", "French Horn", "Brass", "Western", aliases: ["horn"]),
item("tuba", "Tuba", "Brass", "Western"),
item("euphonium", "Euphonium", "Brass", "Western"),
item("sousaphone", "Sousaphone", "Brass", "North America"),
item("drum-kit", "Drum Kit", "Percussion", "Western", aliases: ["drum set"]),
item("snare-drum", "Snare Drum", "Percussion", "Western"),
item("bass-drum", "Bass Drum", "Percussion", "Western", aliases: ["kick drum"]),
item("timpani", "Timpani", "Percussion", "Western", aliases: ["kettle drums"]),
item("xylophone", "Xylophone", "Mallet percussion", "Western"),
item("marimba", "Marimba", "Mallet percussion", "Central America"),
item("vibraphone", "Vibraphone", "Mallet percussion", "Western", aliases: ["vibes"]),
item("glockenspiel", "Glockenspiel", "Mallet percussion", "Europe"),
item("tubular-bells", "Tubular Bells", "Mallet percussion", "Western", aliases: ["orchestral chimes"]),
item("triangle", "Triangle", "Percussion", "Western"),
item("tambourine", "Tambourine", "Frame drums", "Western"),
item("cowbell", "Cowbell", "Percussion", "Western"),
item("cymbals", "Cymbals", "Percussion", "Western"),
item("gong", "Gong", "Percussion", "East Asia"),
item("castanets", "Castanets", "Percussion", "Spain"),
item("claves", "Claves", "Percussion", "Latin America"),
item("harmonica", "Harmonica", "Free reed", "Western", aliases: ["mouth organ"])
]
private static let middleEastern: [InstrumentCatalogItem] = [
item("oud", "Oud", "Plucked strings", "Middle East", aliases: ["ud"], terms: ["عود"]),
item("qanun", "Qanun", "Zithers", "Middle East", aliases: ["kanun"], terms: ["قانون"]),
item("ney", "Ney", "End-blown flutes", "Middle East", aliases: ["nai"], terms: ["ناي"]),
item("daf", "Daf", "Frame drums", "Middle East", terms: ["دف"]),
item("riq", "Riq", "Frame drums", "Middle East", aliases: ["riqq"], terms: ["رق"]),
item("darbuka", "Darbuka", "Goblet drums", "Middle East", aliases: ["doumbek", "tabla"], terms: ["دربكة"]),
item("kamancheh", "Kamancheh", "Bowed strings", "Middle East", aliases: ["kamanche"]),
item("santur", "Santur", "Hammered zithers", "Middle East"),
item("baglama", "Baglama", "Plucked strings", "Middle East", aliases: ["saz"]),
item("buzuq", "Buzuq", "Plucked strings", "Middle East", aliases: ["bozuk"]),
item("mijwiz", "Mijwiz", "Double reeds", "Middle East"),
item("arghul", "Arghul", "Double reeds", "Middle East"),
item("zurna", "Zurna", "Double reeds", "Middle East"),
item("naqqara", "Naqqara", "Kettle drums", "Middle East"),
item("tombak", "Tombak", "Goblet drums", "Middle East", aliases: ["zarb"]),
item("setar", "Setar", "Plucked strings", "Middle East"),
item("tar", "Tar", "Plucked strings", "Middle East"),
item("duduk", "Duduk", "Double reeds", "Middle East"),
item("kemence", "Kemenche", "Bowed strings", "Middle East"),
item("saz-bagpipes", "Tulum", "Bagpipes", "Middle East")
]
private static let african: [InstrumentCatalogItem] = [
item("djembe", "Djembe", "Hand drums", "West Africa"),
item("talking-drum", "Talking Drum", "Tension drums", "West Africa", aliases: ["dundun"]),
item("kora", "Kora", "Harps", "West Africa"),
item("balafon", "Balafon", "Mallet percussion", "West Africa"),
item("mbira", "Mbira", "Lamellophones", "Southern Africa", aliases: ["thumb piano"]),
item("kalimba", "Kalimba", "Lamellophones", "Africa"),
item("shekere", "Shekere", "Shakers", "West Africa"),
item("udu", "Udu", "Clay drums", "West Africa"),
item("ngoni", "Ngoni", "Plucked strings", "West Africa"),
item("xalam", "Xalam", "Plucked strings", "West Africa"),
item("dunun", "Dunun", "Bass drums", "West Africa"),
item("ashiko", "Ashiko", "Hand drums", "West Africa"),
item("marovany", "Marovany", "Box zithers", "Madagascar"),
item("endongo", "Endongo", "Harps", "East Africa"),
item("masenqo", "Masenqo", "Bowed strings", "East Africa"),
item("krin", "Krin", "Slit drums", "West Africa"),
item("agogo", "Agogo Bells", "Percussion", "West Africa"),
item("goje", "Goje", "Bowed strings", "West Africa"),
item("adungu", "Adungu", "Harps", "East Africa"),
item("ekwe", "Ekwe", "Slit drums", "West Africa")
]
private static let southAsian: [InstrumentCatalogItem] = [
item("sitar", "Sitar", "Plucked strings", "South Asia"),
item("tabla", "Tabla", "Hand drums", "South Asia"),
item("tanpura", "Tanpura", "Drone strings", "South Asia", aliases: ["tambura"]),
item("sarod", "Sarod", "Plucked strings", "South Asia"),
item("bansuri", "Bansuri", "Side-blown flutes", "South Asia"),
item("shehnai", "Shehnai", "Double reeds", "South Asia"),
item("santoor", "Santoor", "Hammered zithers", "South Asia"),
item("harmonium", "Harmonium", "Free reed", "South Asia"),
item("mridangam", "Mridangam", "Hand drums", "South Asia"),
item("veena", "Veena", "Plucked strings", "South Asia"),
item("sarangi", "Sarangi", "Bowed strings", "South Asia"),
item("dholak", "Dholak", "Hand drums", "South Asia"),
item("dhol", "Dhol", "Bass drums", "South Asia"),
item("ghatam", "Ghatam", "Clay drums", "South Asia"),
item("kanjira", "Kanjira", "Frame drums", "South Asia"),
item("nadaswaram", "Nadaswaram", "Double reeds", "South Asia"),
item("esraj", "Esraj", "Bowed strings", "South Asia"),
item("pakhawaj", "Pakhawaj", "Hand drums", "South Asia"),
item("khol", "Khol", "Hand drums", "South Asia"),
item("ektara", "Ektara", "Drone strings", "South Asia"),
item("rabab", "Rabab", "Plucked strings", "South Asia"),
item("morsing", "Morsing", "Jaw harps", "South Asia"),
item("nagada", "Nagada", "Kettle drums", "South Asia")
]
private static let eastAsian: [InstrumentCatalogItem] = [
item("erhu", "Erhu", "Bowed strings", "East Asia"),
item("guzheng", "Guzheng", "Zithers", "East Asia", aliases: ["Chinese zither"]),
item("pipa", "Pipa", "Plucked strings", "East Asia"),
item("dizi", "Dizi", "Side-blown flutes", "East Asia"),
item("xiao", "Xiao", "End-blown flutes", "East Asia"),
item("suona", "Suona", "Double reeds", "East Asia"),
item("sheng", "Sheng", "Mouth organs", "East Asia"),
item("yangqin", "Yangqin", "Hammered zithers", "East Asia"),
item("ruan", "Ruan", "Plucked strings", "East Asia"),
item("guqin", "Guqin", "Zithers", "East Asia"),
item("liuqin", "Liuqin", "Plucked strings", "East Asia"),
item("hulusi", "Hulusi", "Free reed", "East Asia"),
item("morin-khuur", "Morin Khuur", "Bowed strings", "Central Asia", aliases: ["horsehead fiddle"]),
item("koto", "Koto", "Zithers", "Japan"),
item("shamisen", "Shamisen", "Plucked strings", "Japan"),
item("shakuhachi", "Shakuhachi", "End-blown flutes", "Japan"),
item("biwa", "Biwa", "Plucked strings", "Japan"),
item("taiko", "Taiko", "Drums", "Japan"),
item("hichiriki", "Hichiriki", "Double reeds", "Japan"),
item("ryuteki", "Ryuteki", "Side-blown flutes", "Japan"),
item("gayageum", "Gayageum", "Zithers", "Korea"),
item("geomungo", "Geomungo", "Zithers", "Korea"),
item("haegeum", "Haegeum", "Bowed strings", "Korea"),
item("daegeum", "Daegeum", "Side-blown flutes", "Korea"),
item("janggu", "Janggu", "Hourglass drums", "Korea"),
item("piri", "Piri", "Double reeds", "Korea"),
item("ajaeng", "Ajaeng", "Bowed strings", "Korea"),
item("bawu", "Bawu", "Free reed", "East Asia"),
item("konghou", "Konghou", "Harps", "East Asia")
]
private static let southeastAsian: [InstrumentCatalogItem] = [
item("angklung", "Angklung", "Shaken idiophones", "Southeast Asia"),
item("gamelan", "Gamelan", "Metallophones", "Southeast Asia"),
item("kendang", "Kendang", "Hand drums", "Southeast Asia"),
item("suling", "Suling", "Side-blown flutes", "Southeast Asia"),
item("rebab", "Rebab", "Bowed strings", "Southeast Asia"),
item("kulintang", "Kulintang", "Gong chimes", "Southeast Asia"),
item("agung", "Agung", "Gongs", "Southeast Asia"),
item("khene", "Khene", "Mouth organs", "Southeast Asia"),
item("ranat-ek", "Ranat Ek", "Mallet percussion", "Southeast Asia"),
item("saw-duang", "Saw Duang", "Bowed strings", "Southeast Asia"),
item("tro", "Tro", "Bowed strings", "Southeast Asia"),
item("saung-gauk", "Saung Gauk", "Harps", "Southeast Asia"),
item("pat-waing", "Pat Waing", "Drums", "Southeast Asia"),
item("rondalla", "Rondalla", "Plucked strings", "Southeast Asia"),
item("bandurria", "Bandurria", "Plucked strings", "Southeast Asia"),
item("tinikling-bamboo", "Bamboo Percussion", "Percussion", "Southeast Asia"),
item("sape", "Sape", "Plucked strings", "Southeast Asia"),
item("serunai", "Serunai", "Double reeds", "Southeast Asia")
]
private static let latinAmerican: [InstrumentCatalogItem] = [
item("conga", "Conga", "Hand drums", "Latin America"),
item("bongo", "Bongo", "Hand drums", "Latin America"),
item("timbales", "Timbales", "Drums", "Latin America"),
item("cajon", "Cajon", "Box drums", "Latin America"),
item("guiro", "Guiro", "Scrapers", "Latin America"),
item("maracas", "Maracas", "Shakers", "Latin America"),
item("cuica", "Cuica", "Friction drums", "Latin America"),
item("berimbau", "Berimbau", "Musical bows", "Brazil"),
item("bandoneon", "Bandoneon", "Free reed", "South America"),
item("charango", "Charango", "Plucked strings", "Andes"),
item("cuatro", "Cuatro", "Plucked strings", "Latin America"),
item("tres", "Tres", "Plucked strings", "Cuba"),
item("vihuela", "Vihuela", "Plucked strings", "Mexico"),
item("requinto", "Requinto", "Plucked strings", "Latin America"),
item("zampogna", "Zampona", "Panpipes", "Andes", aliases: ["siku"]),
item("quena", "Quena", "End-blown flutes", "Andes"),
item("bombo-leguero", "Bombo Leguero", "Bass drums", "South America"),
item("surdo", "Surdo", "Bass drums", "Brazil"),
item("pandeiro", "Pandeiro", "Frame drums", "Brazil"),
item("candombe-drums", "Candombe Drums", "Hand drums", "South America"),
item("tambora", "Tambora", "Bass drums", "Caribbean"),
item("guira", "Guira", "Scrapers", "Caribbean"),
item("bata-drums", "Bata Drums", "Hand drums", "Caribbean"),
item("arpa-llanera", "Arpa Llanera", "Harps", "Latin America"),
item("marimbola", "Marimbola", "Lamellophones", "Caribbean"),
item("tiple", "Tiple", "Plucked strings", "South America")
]
private static let folkAndModern: [InstrumentCatalogItem] = [
item("bagpipes", "Bagpipes", "Bagpipes", "Europe"),
item("tin-whistle", "Tin Whistle", "Folk winds", "Ireland", aliases: ["penny whistle"]),
item("bodhran", "Bodhran", "Frame drums", "Ireland"),
item("dulcimer", "Appalachian Dulcimer", "Folk strings", "North America"),
item("hammered-dulcimer", "Hammered Dulcimer", "Hammered zithers", "Europe"),
item("hurdy-gurdy", "Hurdy-gurdy", "Folk strings", "Europe"),
item("nyckelharpa", "Nyckelharpa", "Bowed strings", "Europe"),
item("balalaika", "Balalaika", "Plucked strings", "Europe"),
item("domra", "Domra", "Plucked strings", "Europe"),
item("bouzouki", "Bouzouki", "Plucked strings", "Greece"),
item("cimbalom", "Cimbalom", "Hammered zithers", "Europe"),
item("jaw-harp", "Jaw Harp", "Jaw harps", "Folk", aliases: ["mouth harp"]),
item("didgeridoo", "Didgeridoo", "Aerophones", "Oceania"),
item("clapsticks", "Clapsticks", "Percussion", "Oceania"),
item("native-american-flute", "Native American Flute", "End-blown flutes", "North America"),
item("fiddle", "Fiddle", "Folk strings", "Folk"),
item("washboards", "Washboard", "Percussion", "North America"),
item("musical-saw", "Musical Saw", "Folk instruments", "Folk"),
item("theremin", "Theremin", "Electronic", "Modern"),
item("modular-synthesizer", "Modular Synthesizer", "Electronic", "Modern", aliases: ["modular synth"]),
item("synthesizer", "Synthesizer", "Electronic", "Modern", aliases: ["synth"]),
item("drum-machine", "Drum Machine", "Electronic", "Modern"),
item("sampler", "Sampler", "Electronic", "Modern"),
item("turntables", "Turntables", "Electronic", "Modern", aliases: ["DJ decks"]),
item("midi-controller", "MIDI Controller", "Electronic", "Modern"),
item("electric-violin", "Electric Violin", "Electronic strings", "Modern"),
item("electric-cello", "Electric Cello", "Electronic strings", "Modern"),
item("electronic-drum-pad", "Electronic Drum Pad", "Electronic percussion", "Modern"),
item("vocoder", "Vocoder", "Electronic", "Modern"),
item("keytar", "Keytar", "Electronic", "Modern")
]
private static func item(
_ id: String,
_ name: String,
_ familyCategory: String,
_ regionOrigin: String? = nil,
aliases: [String] = [],
terms: [String] = []
) -> InstrumentCatalogItem {
InstrumentCatalogItem(
id: id,
name: name,
familyCategory: familyCategory,
regionOrigin: regionOrigin,
aliases: aliases,
searchTerms: terms
)
}
private static func searchableTerms(for item: InstrumentCatalogItem) -> [String] {
searchTokens(for: [item.name] + 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 == "-" })
.map(String.init)
}
}
@@ -0,0 +1,50 @@
import MusicAssistantCore
import XCTest
final class LocalInstrumentCatalogTests: XCTestCase {
func testWorldwideCatalogCoversRequiredRegionsAndInstrumentTypes() {
let catalog = LocalInstrumentCatalog()
let expectedRegions = [
"Western", "Middle East", "Africa", "South Asia", "East Asia", "Southeast Asia", "Latin America", "Modern"
]
let expectedInstrumentIDs = [
"piano", "oud", "djembe", "sitar", "erhu", "angklung", "charango", "bagpipes", "synthesizer"
]
XCTAssertGreaterThanOrEqual(catalog.items.count, 200)
XCTAssertTrue(expectedRegions.allSatisfy { region in
catalog.items.contains { $0.regionOrigin?.contains(region) == true }
})
XCTAssertTrue(expectedInstrumentIDs.allSatisfy { id in
catalog.items.contains { $0.id == id }
})
}
func testWorldwideCatalogHasUniqueStableInstrumentIDs() {
let items = LocalInstrumentCatalog.worldwideItems
XCTAssertEqual(Set(items.map(\.id)).count, items.count)
XCTAssertTrue(items.allSatisfy { !$0.name.isEmpty && !$0.familyCategory.isEmpty })
}
func testSearchMatchesNamesAliasesAndArabicSearchTerms() {
let catalog = LocalInstrumentCatalog()
XCTAssertEqual(catalog.instruments(matching: "concert flute").map(\.id), ["flute"])
XCTAssertEqual(catalog.instruments(matching: "nylon string").map(\.id), ["classical-guitar"])
XCTAssertEqual(catalog.instruments(matching: "عود").map(\.id), ["oud"])
}
func testFamilyCategoriesAndFilteringComeFromCatalogMetadata() {
let catalog = LocalInstrumentCatalog()
let pluckedStrings = catalog.instruments(matching: "", familyCategory: "Plucked strings")
XCTAssertTrue(catalog.familyCategories.contains("Plucked strings"))
XCTAssertTrue(pluckedStrings.contains { $0.id == "oud" })
XCTAssertTrue(pluckedStrings.allSatisfy { $0.familyCategory == "Plucked strings" })
XCTAssertTrue(
catalog.instruments(matching: "guitar", familyCategory: "Plucked strings")
.allSatisfy { $0.familyCategory == "Plucked strings" }
)
}
}
+4 -4
View File
@@ -77,13 +77,13 @@ requirement is missing and blocks implementation, record it in
- [x] Create structured catalog models for instrument id, name, - [x] Create structured catalog models for instrument id, name,
family/category, optional region/origin and aliases/search terms. family/category, optional region/origin and aliases/search terms.
- [ ] Provide large worldwide catalog data covering Western, Middle - [x] Provide large worldwide catalog data covering Western, Middle
Eastern, African, South Asian, East Asian, Southeast Asian, Latin Eastern, African, South Asian, East Asian, Southeast Asian, Latin
American, traditional, folk, orchestral, electronic and modern American, traditional, folk, orchestral, electronic and modern
instruments. instruments.
- [ ] Keep catalog data out of SwiftUI views. - [x] Keep catalog data out of SwiftUI views.
- [ ] Build searchable instrument browser. - [x] Build searchable instrument browser.
- [ ] Add browsing/filtering by family/category. - [x] Add browsing/filtering by family/category.
- [ ] Add browsing/filtering by region/origin where useful. - [ ] Add browsing/filtering by region/origin where useful.
- [ ] Add checkbox-based multi-select and deselect behavior. - [ ] Add checkbox-based multi-select and deselect behavior.
- [ ] Persist selected instruments on the current Song Project. - [ ] Persist selected instruments on the current Song Project.