Files
stemify/ios/Stemify/Playback/MixPresets.swift
T
cdnutterandClaude Opus 5.5 fdf9e970b2 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>
2026-10-20 07:15:47 -07:00

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
}
}