broken changes

This commit is contained in:
Mark Gregory
2026-11-05 17:06:40 +00:00
parent 9da6d2fdcd
commit 8cffb40f17
3 changed files with 169 additions and 35 deletions
+86 -8
View File
@@ -12,6 +12,14 @@ struct DJAudioRouting: Codable, Equatable {
var deckB = DJAudioRoute()
var cue = DJAudioRoute()
func route(_ letter: String) -> DJAudioRoute { mode == "External" ? (letter == "A" ? deckA : deckB) : main }
static func controllerPreset(_ device: DJAudioDevice,mode: String) -> Self {
var routing = Self();routing.mode = mode
routing.main = .init(uid:device.uid,firstChannel:0)
routing.deckA = routing.main
routing.deckB = .init(uid:device.uid,firstChannel:device.outputChannels >= 4 ? 2 : 0)
if mode == "Internal",device.outputChannels >= 4 { routing.cue = .init(uid:device.uid,firstChannel:2) }
return routing
}
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
@@ -48,8 +56,15 @@ struct DJAudioRouting: Codable, Equatable {
struct DJAudioDevicesPane: View {
@ObservedObject var system: DJSystem
@State private var draft = DJAudioRouting()
private var recommended: DJAudioDevice? { system.devices.first { $0.name.localizedCaseInsensitiveContains("MixTrack") && $0.outputChannels >= 4 } ?? system.devices.first{$0.outputChannels >= 4} }
private var validation: String? {
do { try draft.validate(devices:system.devices,defaultID:DJAudioDevice.defaultOutput());return nil }
catch { return error.localizedDescription }
}
var body: some View {
VStack(alignment:.leading,spacing:26) {
VStack(spacing:16) {
ScrollView {
VStack(alignment:.leading,spacing:22) {
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() }
@@ -59,20 +74,56 @@ struct DJAudioDevicesPane: View {
HStack { Text("Cue Volume:").frame(width:130,alignment:.trailing);Slider(value:$system.cueLevel,in:0...1).frame(maxWidth:320);Spacer() }
}.padding(.vertical,22)
}
if let recommended {
Button("Use \(recommended.name)") { draft = .controllerPreset(recommended,mode:draft.mode) }
.help("Set up the controller’s stereo pairs, then Apply")
}
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) }
if let validation { Text(validation).font(.callout).foregroundStyle(.orange) }
PreferencesSection("Detected Audio Devices") {
ForEach(system.detectedDevices) { device in
HStack(alignment:.top) {
Text(device.name).frame(maxWidth:.infinity,alignment:.leading)
VStack(alignment:.trailing,spacing:3) {
Text("\(device.outputChannels) out · \(device.inputChannels) in")
if device.outputChannels >= 2 {
Text(device.stereoPairs.map { "ch \($0+1)–\($0+2)" }.joined(separator:", ")).foregroundStyle(.secondary)
} else { Text("Input only").foregroundStyle(.secondary) }
}
}.font(.caption).padding(.vertical,3)
}
}
Text("Booth, microphone and recording input are not available. Karaoke has its own output settings.").font(.caption).foregroundStyle(.secondary)
}
}
Divider()
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)
Button("Apply") { system.applyRouting(draft) }.buttonStyle(.borderedProminent).disabled(draft == system.routing || validation != nil)
}
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 }
}.onAppear {
system.refreshDevices();draft = system.routing
if draft == DJAudioRouting(),let recommended { draft = .controllerPreset(recommended,mode:"Internal") }
}
.onChange(of:draft.mode) { mode in
if mode == "External",draft.deckA == DJAudioRoute(),draft.deckB == DJAudioRoute(),let recommended {
draft.deckA = .init(uid:recommended.uid,firstChannel:0);draft.deckB = .init(uid:recommended.uid,firstChannel:2)
if draft.cue.uid == recommended.uid { draft.cue = .init() }
}
}
.task {
while !Task.isCancelled {
do { try await Task.sleep(nanoseconds:2_000_000_000) } catch { return }
system.refreshDevices()
}
}
}
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))
let pairs = device?.stereoPairs ?? [0]
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)})) {
@@ -81,9 +132,36 @@ struct DJAudioDevicesPane: View {
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)
DJAudioChannelPicker(value:route.firstChannel,pairs:pairs,label:label+" channels")
.frame(width:110,height:26).disabled(optional && route.wrappedValue.uid.isEmpty)
}
}
}
// Explicit native menus keep every reported channel pair visible and keyboard accessible.
struct DJAudioChannelPicker: NSViewRepresentable {
@Binding var value: Int
let pairs: [Int]
let label: String
func makeCoordinator() -> Coordinator { Coordinator(value:$value) }
func makeNSView(context: Context) -> NSPopUpButton {
let button = NSPopUpButton(frame:.zero,pullsDown:false)
button.target = context.coordinator;button.action = #selector(Coordinator.changed(_:))
button.setAccessibilityLabel(label);return button
}
func updateNSView(_ button: NSPopUpButton,context: Context) {
context.coordinator.value = $value
if button.itemArray.map(\.tag) != pairs {
button.removeAllItems()
for pair in pairs { button.addItem(withTitle:"ch \(pair+1)–\(pair+2)");button.lastItem?.tag = pair }
}
button.selectItem(withTag:value);button.isEnabled = context.environment.isEnabled
button.setAccessibilityLabel(label)
}
func sizeThatFits(_ proposal: ProposedViewSize,nsView: NSPopUpButton,context: Context) -> CGSize? { .init(width:proposal.width ?? 110,height:26) }
final class Coordinator: NSObject {
var value: Binding<Int>
init(value: Binding<Int>) { self.value = value }
@objc func changed(_ sender: NSPopUpButton) { if let selected = sender.selectedItem { value.wrappedValue = selected.tag } }
}
}