import Foundation /// A one-tap stem mix. [build] returns nil when the track lacks the stems it needs. struct MixPreset: Identifiable, Equatable { let label: String let build: ([String]) -> [String: StemSetting]? var id: String { label } static func == (a: MixPreset, b: MixPreset) -> Bool { a.label == b.label } } enum MixPresets { static let full = MixPreset(label: "Full Mix") { _ in [:] } static let karaoke = MixPreset(label: "Karaoke") { n in n.contains("vocals") ? ["vocals": StemSetting(muted: true)] : nil } static let all: [MixPreset] = [ full, karaoke, MixPreset(label: "A Cappella") { n in n.contains("vocals") ? ["vocals": StemSetting(solo: true)] : nil }, MixPreset(label: "No Drums") { n in n.contains("drums") ? ["drums": StemSetting(muted: true)] : nil }, MixPreset(label: "Rhythm") { n in n.contains("drums") && n.contains("bass") ? ["drums": StemSetting(solo: true), "bass": StemSetting(solo: true)] : nil }, MixPreset(label: "No Guitar") { n in n.contains("guitar") ? ["guitar": StemSetting(muted: true)] : nil }, MixPreset(label: "No Bass") { n in n.contains("bass") ? ["bass": StemSetting(muted: true)] : nil }, ] /// Drops stems left at their defaults, so mixes compare by what they actually change. static func normalized(_ m: [String: StemSetting]) -> [String: StemSetting] { m.filter { $0.value != StemSetting() } } /// The preset [mix] matches for these stems, or nil for a hand-made mix. static func current(_ names: [String], _ mix: [String: StemSetting]) -> MixPreset? { let m = normalized(mix) return all.first { p in p.build(names).map { normalized($0) == m } ?? false } } /// The preset after the current one (a custom mix goes back to Full), skipping ones these stems can't do. static func next(_ names: [String], _ mix: [String: StemSetting]) -> MixPreset { let usable = all.filter { $0.build(names) != nil } guard let cur = current(names, mix), let i = usable.firstIndex(of: cur) else { return full } return usable[(i + 1) % usable.count] } /// True when the vocals stem can't be heard (muted, or another stem soloed). static func vocalsSilent(_ names: [String], _ mix: [String: StemSetting]) -> Bool { if !names.contains("vocals") { return false } let v = mix["vocals"] ?? StemSetting() let anySolo = names.contains { mix[$0]?.solo == true } return v.muted || anySolo && !v.solo || v.level <= 0 } }