Add iOS app with CarPlay and Siri
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>
This commit is contained in:
1 parent
45bc0eefe0
commit
fdf9e970b2
45 files changed
+9822
No files matched your search
@@ -0,0 +1,50 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user