broken changes

This commit is contained in:
Mark Gregory
2026-11-05 21:10:04 +00:00
parent 83a97ff3fd
commit 78c4411fa6
6 changed files with 218 additions and 21 deletions
+16 -9
View File
@@ -165,6 +165,7 @@ struct DJAudioDevice: Identifiable, Equatable {
@Published var cueEditor: String?
@Published var cueDraft: Double = 0
@Published var cueEditing = false
@Published var loopEditor: String?
@Published var cueLevel: Double = 0.6 { didSet { preview.volume = cueLevel } }
@Published private(set) var previewDeck: String?
@Published var devices = [DJAudioDevice]()
@@ -261,9 +262,7 @@ struct DJAudioDevice: Identifiable, Equatable {
if previewDeck == letter { stopPreview() }
try deck.load(song)
let route = routing.route(letter)
if !route.uid.isEmpty || route.firstChannel != 0 || routing.mode == "External" {
do { try deck.player.routeOutput(try device(route.uid),firstChannel:route.firstChannel) } catch { deck.unload();throw error }
}
do { try deck.player.routeOutput(try device(route.uid),firstChannel:route.firstChannel) } catch { deck.unload();throw error }
if settings.resetEQOnLoad { deck.player.resetTone() }
if settings.autoPlayOnLoad && !automated { deck.player.toggle() }
status = "\(song.title) ready on \(letter) · set or tap BPM for tempo sync and beat loops"
@@ -287,14 +286,18 @@ struct DJAudioDevice: Identifiable, Equatable {
let previous = routing
perform {
try next.validate(devices:devices,defaultID:DJAudioDevice.defaultOutput())
automix.manualTakeover();stopPreview()
automix.manualTakeover()
let previewActive = previewDeck != nil
do {
for deck in [a,b] { let route = next.route(deck.letter);try deck.player.routeOutput(try device(route.uid),firstChannel:route.firstChannel) }
if previewActive,!next.cue.uid.isEmpty { try preview.routeOutput(try device(next.cue.uid),firstChannel:next.cue.firstChannel) }
try JSONEncoder().encode(next).write(to:root.appendingPathComponent("DJAudioRouting.json"),options:.atomic)
routing = next;masterUID = next.main.uid;cueUID = next.cue.uid;applyMix()
if next.cue.uid.isEmpty { stopPreview() }
status = "Audio routing applied · \(next.mode) mixer"
} catch {
for deck in [a,b] { let route = previous.route(deck.letter);if let output = try? device(route.uid) { try? deck.player.routeOutput(output,firstChannel:route.firstChannel) } }
if previewActive,!previous.cue.uid.isEmpty,let output = try? device(previous.cue.uid) { try? preview.routeOutput(output,firstChannel:previous.cue.firstChannel) }
throw error
}
}
@@ -734,6 +737,7 @@ struct DJConsole: View {
Button("Replace & Load",role:.destructive) { system.confirmReplacement(request) }
} message: { request in Text("Deck \(request.deck) is playing. Replacing it with \(request.title) stops the current track. Continue?") }
.sheet(isPresented:$system.cueEditing) { DJCueEditor(system:system) }
.sheet(isPresented:Binding(get:{system.loopEditor != nil},set:{if !$0 { system.loopEditor = nil }})) { let deck = system.channel(system.loopEditor ?? "A");DJLoopEditor(player:deck.player,letter:deck.letter) }
.sheet(isPresented:$settingsModal) { StudioSettingsDialog(model:model,scope:"DJ") }
.sheet(isPresented:$routing) { StudioSettingsDialog(model:model,scope:"DJ",category:"Devices") }
.sheet(isPresented:$mapping) { StudioSettingsDialog(model:model,scope:"DJ",category:"MIDI") }
@@ -940,6 +944,7 @@ private struct DJPlatter: View {
}
}
private struct DJWaveform: View {
@State private var frozenStart: Double?
@ObservedObject var player: Player
let color: Color
var bpm: Double?
@@ -959,7 +964,7 @@ private struct DJWaveform: View {
context.fill(Path(CGRect(x:x,y:0,width:1.5,height:size.height)),with:.color(.white))
}.background(.black.opacity(0.3),in:RoundedRectangle(cornerRadius:3))
.overlay { if player.waveformLoading { ProgressView().controlSize(.mini) } else if player.waveformUnavailable { Text("Waveform unavailable").font(.caption2).foregroundStyle(.secondary) } }
.contentShape(Rectangle()).gesture(DragGesture(minimumDistance:0).onChanged { value in player.seek(Double(value.location.x/max(1,geometry.size.width))*player.duration) })
.modifier(DJLoopWaveInteraction(player:player,start:0,span:player.duration,width:geometry.size.width,frozenStart:$frozenStart))
}.accessibilityLabel("Track waveform and position").accessibilityValue(DJStyle.stamp(player.time)).accessibilityAdjustableAction { player.jog($0 == .increment ? 1 : -1) }
}
}
@@ -1207,7 +1212,7 @@ private struct DJConsoleTransport: View {
HStack(spacing:6) {
if deck.letter == "B" { Spacer(minLength:0) }
ForEach(0..<4) { index in Button("\(index+1)") { system.hotCue(index,deck:deck) }.foregroundStyle(player.hotCues[index] == nil ? Color.secondary : DJStyle.color(deck.letter)).contextMenu { Button("Clear hot cue") { player.hotCue(index,clear:true) } } }
Button("Cue \(DJStyle.stamp(player.cuePosition)) ✎") { system.editCue(deck) };Button("IN") { player.markLoopIn() };Button("OUT") { player.markLoopOut() }
Button("Cue \(DJStyle.stamp(player.cuePosition)) ✎") { system.editCue(deck) };Button("IN") { player.markLoopIn() };Button("OUT") { player.markLoopOut() };Button("Loop ✎") { system.loopEditor = deck.letter }
Menu { Button("Stop") { player.stopDeck() };Button("Exit loop") { player.clearLoop() };Button("Nudge back") { player.jog(-0.05) };Button("Nudge forward") { player.jog(0.05) } } label: { Image(systemName:"ellipsis") }
if deck.letter == "A" { Spacer(minLength:0) }
}.controlSize(.mini)
@@ -1221,12 +1226,12 @@ private struct DJCentreWaveform: View {
let color: Color
private var bpm: Double? { deck.bpmConfirmed ? deck.bpm : nil }
var settings = DJPreferences()
@State private var scrubStart: Double?
@State private var frozenStart: Double?
var body: some View {
GeometryReader { geometry in
Canvas { context,size in
guard !player.waveform.isEmpty,player.duration > 0 else { return }
let span = settings.waveformSeconds,start = player.time-span/2,count = max(1,Int(size.width/2))
let span = settings.waveformSeconds,start = frozenStart ?? (player.time-span/2),count = max(1,Int(size.width/2))
for second in 0...8 {
let x = Double(second)/8*size.width
context.fill(Path(CGRect(x:x,y:0,width:0.5,height:size.height)),with:.color(.white.opacity(settings.showTimeGrid ? 0.15 : 0)))
@@ -1246,7 +1251,7 @@ private struct DJCentreWaveform: View {
DJWaveMarkers.draw(context:&context,size:size,start:start,span:span,bpm:bpm,loop:player.loopEnabled ? player.loopIn.flatMap { a in player.loopOut.map { a...$0 } } : nil)
context.fill(Path(CGRect(x:size.width/2,y:0,width:1.5,height:size.height)),with:.color(.red))
}.overlay(alignment:.topLeading) { Text(letter+" · \(Int(settings.waveformSeconds)) s").font(.system(size:9,weight:.semibold)).foregroundStyle(color).padding(5) }
.contentShape(Rectangle()).gesture(DragGesture(minimumDistance:0).onChanged { value in if scrubStart == nil { scrubStart = player.time-settings.waveformSeconds/2 };player.seek((scrubStart ?? 0)+Double(value.location.x/max(1,geometry.size.width))*settings.waveformSeconds) }.onEnded { _ in scrubStart = nil })
.modifier(DJLoopWaveInteraction(player:player,start:player.time-settings.waveformSeconds/2,span:settings.waveformSeconds,width:geometry.size.width,frozenStart:$frozenStart))
}.accessibilityLabel("Deck \(letter) scrolling waveform").accessibilityValue(DJStyle.stamp(player.time))
}
}
@@ -1620,6 +1625,8 @@ enum DJWaveMarkers {
let x = (time-start)/span*size.width
if x >= 0,x <= size.width {
context.fill(Path(CGRect(x:x,y:0,width:2,height:size.height)),with:.color(.green))
context.fill(Path(CGRect(x:x-3,y:size.height/2-7,width:7,height:14)),with:.color(.green))
context.fill(Path(CGRect(x:x,y:size.height/2-5,width:1,height:10)),with:.color(.black))
context.draw(Text(label).font(.system(size:8,weight:.bold)).foregroundColor(.green),at:CGPoint(x:min(size.width-12,max(12,x)),y:7))
}
}