broken changes

This commit is contained in:
Mark Gregory
2026-11-05 16:35:24 +00:00
parent 7bf251c5e6
commit 1b5e48dfa1
7 changed files with 225 additions and 44 deletions
+89
View File
@@ -0,0 +1,89 @@
import SwiftUI
import CoreAudio
struct DJAudioRoute: Codable, Equatable {
var uid = ""
var firstChannel = 0
}
struct DJAudioRouting: Codable, Equatable {
var mode = "Internal"
var main = DJAudioRoute()
var deckA = DJAudioRoute()
var deckB = DJAudioRoute()
var cue = DJAudioRoute()
func route(_ letter: String) -> DJAudioRoute { mode == "External" ? (letter == "A" ? deckA : deckB) : main }
static func channelMap(channels: Int,first: Int) throws -> [Int32] {
guard channels >= 2,first >= 0,first % 2 == 0,first+1 < channels else { throw LibraryError.message("Choose an available stereo channel pair.") }
var map = [Int32](repeating:-1,count:channels);map[first] = 0;map[first+1] = 1;return map
}
func validate(devices: [DJAudioDevice],defaultID: AudioDeviceID?) throws {
func resolved(_ route: DJAudioRoute) throws -> DJAudioDevice {
guard let device = devices.first(where:{route.uid.isEmpty ? $0.id == defaultID : $0.uid == route.uid}) else { throw LibraryError.message("An audio output is unavailable. Reconnect it or choose another device.") }
_ = try Self.channelMap(channels:device.outputChannels,first:route.firstChannel);return device
}
let outputs = mode == "External" ? [deckA,deckB] : [main]
for route in outputs { _ = try resolved(route) }
if mode == "External",try resolved(deckA).id == resolved(deckB).id,deckA.firstChannel == deckB.firstChannel {
throw LibraryError.message("External mixer decks need separate output devices or channel pairs.")
}
if !cue.uid.isEmpty {
let cueDevice = try resolved(cue)
for route in outputs where try resolved(route).id == cueDevice.id && route.firstChannel == cue.firstChannel {
throw LibraryError.message("Pre-cueing overlaps a public output. Choose another channel pair or device.")
}
}
}
}
@MainActor final class DJAudioDeviceMonitor {
private var address = AudioObjectPropertyAddress(mSelector:kAudioHardwarePropertyDevices,mScope:kAudioObjectPropertyScopeGlobal,mElement:kAudioObjectPropertyElementMain)
private var block: AudioObjectPropertyListenerBlock?
init(_ changed: @escaping @MainActor () -> Void) {
let listener: AudioObjectPropertyListenerBlock = { _,_ in Task { @MainActor in changed() } }
block = listener;AudioObjectAddPropertyListenerBlock(AudioObjectID(kAudioObjectSystemObject),&address,.main,listener)
}
deinit { if let block { AudioObjectRemovePropertyListenerBlock(AudioObjectID(kAudioObjectSystemObject),&address,.main,block) } }
}
struct DJAudioDevicesPane: View {
@ObservedObject var system: DJSystem
@State private var draft = DJAudioRouting()
var body: some View {
VStack(alignment:.leading,spacing:26) {
PreferencesSection("Audio Routing") {
VStack(spacing:22) {
HStack { Text("Mixer Mode:").frame(width:130,alignment:.trailing);Picker("Mixer Mode",selection:$draft.mode) { Text("Internal").tag("Internal");Text("External").tag("External") }.pickerStyle(.radioGroup).horizontalRadioGroupLayout().labelsHidden();Spacer() }
if draft.mode == "Internal" { row("Main Output",route:$draft.main,icon:"hifispeaker.fill") }
else { row("Deck A",route:$draft.deckA,icon:"a.circle");row("Deck B",route:$draft.deckB,icon:"b.circle") }
row("Pre-Cueing",route:$draft.cue,icon:"headphones",optional:true)
HStack { Text("Cue Volume:").frame(width:130,alignment:.trailing);Slider(value:$system.cueLevel,in:0...1).frame(maxWidth:320);Spacer() }
}.padding(.vertical,22)
}
Text(draft.mode == "Internal" ? "Internal mixes both decks through the main output. Pre-cueing can use channels 3–4 on the same four-channel sound card." : "External sends each deck to its own stereo pair. The software crossfader is bypassed; mix the outputs on your external mixer.").font(.callout).foregroundStyle(.secondary)
if system.devices.isEmpty { Text("No audio outputs reported by macOS. Reconnect the interface and refresh.").foregroundStyle(.orange) }
HStack {
Button("Refresh Devices") { system.refreshDevices() }
Spacer()
Button("Revert") { draft = system.routing }.disabled(draft == system.routing)
Button("Apply") { system.applyRouting(draft) }.buttonStyle(.borderedProminent).disabled(draft == system.routing)
}
Text("Booth, microphone and recording input are not available. Karaoke has its own output settings.").font(.caption).foregroundStyle(.secondary)
}.onAppear { system.refreshDevices();draft = system.routing }
}
private func row(_ label: String,route: Binding<DJAudioRoute>,icon: String,optional: Bool = false) -> some View {
let device = system.devices.first { route.wrappedValue.uid.isEmpty ? $0.id == DJAudioDevice.defaultOutput() : $0.uid == route.wrappedValue.uid }
let pairs = Array(stride(from:0,to:max(0,(device?.outputChannels ?? 2)-1),by:2))
return HStack(spacing:12) {
Text(label+":").frame(width:130,alignment:.trailing)
Picker(label,selection:Binding(get:{route.wrappedValue.uid},set:{route.wrappedValue = .init(uid:$0,firstChannel:0)})) {
Text(optional ? "None" : "System default").tag("")
ForEach(system.devices) { Text($0.name).tag($0.uid) }
if !route.wrappedValue.uid.isEmpty,device == nil { Text("Disconnected device").tag(route.wrappedValue.uid) }
}.labelsHidden().frame(maxWidth:340)
Image(systemName:icon).foregroundStyle(.secondary).frame(width:24)
Picker("\(label) channels",selection:route.firstChannel) {
ForEach(pairs,id:\.self) { Text("ch \($0+1)–\($0+2)").tag($0) }
}.labelsHidden().frame(width:110).disabled(optional && route.wrappedValue.uid.isEmpty)
}
}
}