SwiftUI port of the Android app under ios/, with CarPlay browse and Now Playing, Siri play-media intents, and a build-ipa.sh script that makes an unsigned IPA for sideloading. README gets an iOS section and the vibecoded disclaimer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
51 lines
2.5 KiB
Swift
51 lines
2.5 KiB
Swift
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
|
|
}
|
|
}
|