broken changes

This commit is contained in:
Mark Gregory
2026-11-05 17:21:18 +00:00
parent 8cffb40f17
commit 83a97ff3fd
5 changed files with 72 additions and 39 deletions
+18 -12
View File
@@ -20,6 +20,16 @@ struct DJAudioRouting: Codable, Equatable {
if mode == "Internal",device.outputChannels >= 4 { routing.cue = .init(uid:device.uid,firstChannel:2) }
return routing
}
func changingMode(_ mode: String,preferred: DJAudioDevice?) -> Self {
var next = self;next.mode = mode
if mode == "External" {
if next.deckA == DJAudioRoute(),next.deckB == DJAudioRoute(),let preferred {
next.deckA = .init(uid:preferred.uid,firstChannel:0);next.deckB = .init(uid:preferred.uid,firstChannel:2)
}
if !next.cue.uid.isEmpty,[next.deckA,next.deckB].contains(next.cue) { next.cue = .init() }
}
return next
}
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
@@ -67,7 +77,7 @@ struct DJAudioDevicesPane: View {
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() }
HStack { Text("Mixer Mode:").frame(width:130,alignment:.trailing);Picker("Mixer Mode",selection:Binding(get:{draft.mode},set:{draft = draft.changingMode($0,preferred:recommended)})) { 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)
@@ -76,7 +86,7 @@ struct DJAudioDevicesPane: View {
}
if let recommended {
Button("Use \(recommended.name)") { draft = .controllerPreset(recommended,mode:draft.mode) }
.help("Set up the controller’s stereo pairs, then Apply")
.help("Use the controller’s stereo pairs immediately")
}
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) }
@@ -89,7 +99,7 @@ struct DJAudioDevicesPane: View {
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) }
} else { Text(device.outputChannels == 0 ? "Input only" : "Mono output").foregroundStyle(.secondary) }
}
}.font(.caption).padding(.vertical,3)
}
@@ -101,18 +111,14 @@ struct DJAudioDevicesPane: View {
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 || validation != nil)
if validation != nil { Button("Revert invalid changes") { draft = system.routing } }
Text("Changes apply immediately").font(.caption).foregroundStyle(.secondary)
}
}.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() }
}
.onChange(of:draft) { next in
if next != system.routing,validation == nil { system.applyRouting(next) }
}
.task {
while !Task.isCancelled {
@@ -147,7 +153,7 @@ struct DJAudioChannelPicker: NSViewRepresentable {
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
button.setAccessibilityLabel(label);button.identifier = NSUserInterfaceItemIdentifier(label);return button
}
func updateNSView(_ button: NSPopUpButton,context: Context) {
context.coordinator.value = $value
+1
View File
@@ -52,6 +52,7 @@ import UniformTypeIdentifiers
func manualTakeover() { if enabled { stop("Automix off · manual control") } }
func start(runTimer: Bool = true) {
guard let system,!enabled else { return }
guard !system.importing else { message = "Wait for the current DJ import to finish";return }
guard system.routing.mode == "Internal" else { message = "Automix requires Internal mixer mode";return }
let playing = [system.a,system.b].filter{$0.player.playing}
guard playing.count <= 1 else { message = "Pause one deck before starting Automix";return }
+1 -1
View File
@@ -663,7 +663,7 @@ struct StudioSettingsDialog: View {
else if category == "Devices" { KaraokeDevicesPreferences(model:model) }
else { StudioSettingsPane(showHeading:false,section:category,model:model).id(category) }
Divider()
HStack { Text(scope == "DJ" && category == "Devices" ? "DJ routing · Use Apply to save device changes" : scope+" settings · Changes save automatically").font(.caption).foregroundStyle(.secondary);Spacer();Button("Done") { dismiss() }.keyboardShortcut(.defaultAction) }.padding(.horizontal,24).padding(.vertical,16)
HStack { Text(scope == "DJ" && category == "Devices" ? "DJ routing · Changes apply immediately" : scope+" settings · Changes save automatically").font(.caption).foregroundStyle(.secondary);Spacer();Button("Done") { dismiss() }.keyboardShortcut(.defaultAction) }.padding(.horizontal,24).padding(.vertical,16)
}.frame(width:850,height:700).background(Color(nsColor:.windowBackgroundColor))
.onChange(of:scope) { _ in model.dj.midi.learning = nil;if !categories.contains(category) { category = "General" } }
.onChange(of:category) { _ in model.dj.midi.learning = nil }
+5 -18
View File
@@ -773,24 +773,11 @@ struct LibraryView: View {
}
}
private var workspaceLayout: some View {
VStack(spacing:0) {
HStack {
Spacer()
Picker("Workspace",selection:$workspaceTab) {
Label("Karaoke",systemImage:"music.mic").tag("Karaoke")
Label("DJ Studio",systemImage:"opticaldisc").tag("DJ Studio")
}.pickerStyle(.segmented).frame(width:340).accessibilityIdentifier("workspace.tabs")
Spacer()
}.frame(height:43).background(.bar)
Divider()
// Keep both workspaces mounted: playback, selection and MIDI stay alive between tabs.
ZStack {
libraryWorkspace.opacity(workspaceTab == "Karaoke" ? 1 : 0)
.allowsHitTesting(workspaceTab == "Karaoke").accessibilityHidden(workspaceTab != "Karaoke")
DJConsole(model:model,system:model.dj,expanded:true).opacity(workspaceTab == "DJ Studio" ? 1 : 0)
.allowsHitTesting(workspaceTab == "DJ Studio").accessibilityHidden(workspaceTab != "DJ Studio")
}.frame(maxWidth:.infinity,maxHeight:.infinity)
}
TabView(selection:$workspaceTab) {
libraryWorkspace.tabItem { Label("Karaoke",systemImage:"music.mic") }.tag("Karaoke")
DJConsole(model:model,system:model.dj,expanded:true)
.tabItem { Label("DJ Studio",systemImage:"opticaldisc") }.tag("DJ Studio")
}.accessibilityIdentifier("workspace.tabs")
}
private var workspace: some View {
workspaceLayout