From d71c823ee695d8d80c890c36f1bc1a7117f8ca49 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Thu, 5 Nov 2026 22:31:53 +0000 Subject: [PATCH] broken changes --- KJamsClone/DJLoopEditing.swift | 55 ++++++++++++++++++++++++++-------- KJamsClone/DJWorkstation.swift | 33 ++++++++++++++------ KJamsClone/Player.swift | 9 ++++++ 3 files changed, 75 insertions(+), 22 deletions(-) diff --git a/KJamsClone/DJLoopEditing.swift b/KJamsClone/DJLoopEditing.swift index f266b28..5e1dd93 100644 --- a/KJamsClone/DJLoopEditing.swift +++ b/KJamsClone/DJLoopEditing.swift @@ -45,21 +45,12 @@ struct DJLoopWaveInteraction: ViewModifier { let span: Double let width: Double @Binding var frozenStart: Double? - @State private var drag: DJLoopDrag? func body(content: Content) -> some View { content.contentShape(Rectangle()) - .gesture(DragGesture(minimumDistance:0).onChanged { value in - guard player.canJog,span > 0 else { return } - if drag == nil { - let loop = player.loopEnabled ? player.loopIn.flatMap { a in player.loopOut.map { a...$0 } } : nil - drag = DJLoopDrag(x:value.startLocation.x,viewport:start,span:span,width:width,loop:loop,create:NSEvent.modifierFlags.contains(.shift)) - frozenStart = start - } - guard let drag else { return } - if let range = drag.range(x:value.location.x,duration:player.duration) { player.defineLoop(start:range.lowerBound,end:range.upperBound) } - else if drag.mode == .seek { player.seek(drag.viewport+value.location.x*drag.secondsPerPixel) } - }.onEnded { _ in drag = nil;frozenStart = nil }) - .onChange(of:player.currentSongID) { _ in drag = nil;frozenStart = nil } + .overlay { + DJWaveMouseSurface(player:player,start:start,span:span,width:width,frozenStart:$frozenStart) + } + .onChange(of:player.currentSongID) { _ in frozenStart = nil } .onContinuousHover { phase in switch phase { case .active(let point): @@ -78,6 +69,44 @@ struct DJLoopWaveInteraction: ViewModifier { } } +// Native tracking survives the nested hosting views used by the deck drop zones. +struct DJWaveMouseSurface: NSViewRepresentable { + let player: Player + let start: Double + let span: Double + let width: Double + @Binding var frozenStart: Double? + func makeNSView(context: Context) -> DJWaveMouseView { DJWaveMouseView() } + func updateNSView(_ view: DJWaveMouseView,context: Context) { + view.player = player;view.viewport = start;view.span = span;view.waveWidth = width + view.freeze = { frozenStart = $0 } + } +} +final class DJWaveMouseView: NSView { + weak var player: Player? + var viewport = 0.0,span = 0.0,waveWidth = 1.0 + var freeze: ((Double?) -> Void)? + private var drag: DJLoopDrag? + override var isFlipped: Bool { true } + override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true } + override func mouseDown(with event: NSEvent) { + guard let player,player.canJog,span > 0 else { return } + let point = convert(event.locationInWindow,from:nil) + let loop = player.loopEnabled ? player.loopIn.flatMap { a in player.loopOut.map { a...$0 } } : nil + drag = DJLoopDrag(x:point.x,viewport:viewport,span:span,width:waveWidth,loop:loop,create:event.modifierFlags.contains(.shift)) + freeze?(viewport);updateDrag(point.x) + } + override func mouseDragged(with event: NSEvent) { updateDrag(convert(event.locationInWindow,from:nil).x) } + override func mouseUp(with event: NSEvent) { + updateDrag(convert(event.locationInWindow,from:nil).x);drag = nil;freeze?(nil) + } + private func updateDrag(_ x: Double) { + guard let player,let drag else { return } + if let range = drag.range(x:x,duration:player.duration) { player.defineLoop(start:range.lowerBound,end:range.upperBound) } + else if drag.mode == .seek { player.seek(drag.viewport+x*drag.secondsPerPixel) } + } +} + struct DJLoopEditor: View { @ObservedObject var player: Player let letter: String diff --git a/KJamsClone/DJWorkstation.swift b/KJamsClone/DJWorkstation.swift index 0d6331e..185817e 100644 --- a/KJamsClone/DJWorkstation.swift +++ b/KJamsClone/DJWorkstation.swift @@ -918,6 +918,8 @@ private struct DJPlatter: View { var markerEnabled = true @State private var lastAngle: Double? var body: some View { + TimelineView(.animation(minimumInterval:1.0/60,paused:!player.playing)) { _ in + let displayTime = player.displayTime GeometryReader { geometry in ZStack { Circle().fill(Color.black.opacity(0.55)).overlay { Circle().stroke(color.opacity(0.4),lineWidth:3) } @@ -929,7 +931,7 @@ private struct DJPlatter: View { Rectangle().fill(color).frame(width:5,height:26).offset(y:-geometry.size.height*0.37) Circle().fill(.white).frame(width:5,height:5).offset(y:-geometry.size.height*0.45) } - }.rotationEffect(.degrees(player.time*200)) + }.rotationEffect(.degrees(displayTime*200)) }.contentShape(Circle()) .gesture(DragGesture(minimumDistance:2).onChanged { drag in let angle = atan2(Double(drag.location.y-geometry.size.height/2),Double(drag.location.x-geometry.size.width/2)) @@ -937,6 +939,7 @@ private struct DJPlatter: View { lastAngle = angle }.onEnded{_ in lastAngle = nil}) }.accessibilityLabel("Deck \(letter) jog platter").accessibilityValue(player.playing ? "Rotating" : "Paused").help("Drag around the platter to jog (2 seconds per turn). Rotation follows source playback time.") + } } } private struct DJWaveform: View { @@ -945,6 +948,8 @@ private struct DJWaveform: View { let color: Color var bpm: Double? var body: some View { + TimelineView(.animation(minimumInterval:1.0/60,paused:!player.playing)) { _ in + let displayTime = player.displayTime GeometryReader { geometry in Canvas { context,size in let values = player.waveform @@ -952,16 +957,17 @@ private struct DJWaveform: View { let count = max(1,Int(size.width/2)) for i in 0.. 0 else { return } - let span = settings.waveformSeconds,start = frozenStart ?? (player.time-span/2),count = max(1,Int(size.width/2)) + let span = settings.waveformSeconds,start = frozenStart ?? (displayTime-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))) @@ -1235,8 +1243,10 @@ private struct DJCentreWaveform: View { for i in 0..= 0,seconds < player.duration else { continue } - let index = min(player.waveform.count-1,Int(seconds/player.duration*Double(player.waveform.count))) - let height = max(1,Double(player.waveform[index])*size.height*0.85) + let sample = min(Double(player.waveform.count-1),seconds/player.duration*Double(player.waveform.count)) + let index = Int(sample),next = min(player.waveform.count-1,index+1),fraction = sample-Double(index) + let amplitude = Double(player.waveform[index])*(1-fraction)+Double(player.waveform[next])*fraction + let height = max(1,amplitude*size.height*0.85) var shade = color if settings.spectralWaveforms,player.waveformBands.indices.contains(index) { let band = player.waveformBands[index],sum = max(0.0001,band.low+band.mid+band.high) @@ -1245,10 +1255,12 @@ private struct DJCentreWaveform: View { context.fill(Path(CGRect(x:Double(i)*size.width/Double(count),y:(size.height-height)/2,width:1.5,height:height)),with:.color(shade)) } 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)) + let playhead = (displayTime-start)/span*size.width + context.fill(Path(CGRect(x:playhead,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) } - .modifier(DJLoopWaveInteraction(player:player,start:player.time-settings.waveformSeconds/2,span:settings.waveformSeconds,width:geometry.size.width,frozenStart:$frozenStart)) + .modifier(DJLoopWaveInteraction(player:player,start:displayTime-settings.waveformSeconds/2,span:settings.waveformSeconds,width:geometry.size.width,frozenStart:$frozenStart)) }.accessibilityLabel("Deck \(letter) scrolling waveform").accessibilityValue(DJStyle.stamp(player.time)) + } } } @@ -1607,7 +1619,10 @@ enum DJWaveMarkers { if let bpm { for beat in beats(start:start,span:span,bpm:bpm,width:size.width) { let x = (Double(beat)*60/bpm-start)/span*size.width - context.fill(Path(CGRect(x:x,y:0,width:beat % 4 == 0 ? 1.2 : 0.6,height:size.height)),with:.color(.white.opacity(beat % 4 == 0 ? 0.5 : 0.22))) + context.fill(Path(CGRect(x:x,y:0,width:beat % 4 == 0 ? 1.2 : 0.6,height:size.height)),with:.color(beat % 4 == 0 ? .yellow.opacity(0.85) : .white.opacity(0.4))) + if beat % 4 == 0,size.height >= 45 { + context.draw(Text("\(beat/4+1)").font(.system(size:9,weight:.semibold)).foregroundColor(.yellow),at:CGPoint(x:x+5,y:9),anchor:.leading) + } } } if let loop { diff --git a/KJamsClone/Player.swift b/KJamsClone/Player.swift index 1cc4a61..c90d8db 100644 --- a/KJamsClone/Player.swift +++ b/KJamsClone/Player.swift @@ -23,6 +23,15 @@ import CoreAudio @Published var loopEnabled = false var isShowScreen: Bool { slideImage != nil } var canJog: Bool { currentSongID != nil && duration > 0 } + /// Read the audio clock for display frames without publishing the entire player at 60 Hz. + var displayTime: Double { + guard playing,node.isPlaying,let file,let render = node.lastRenderTime, + let current = node.playerTime(forNodeTime:render) else { return time } + let now = mach_absolute_time() + let elapsed = render.isHostTimeValid && now >= render.hostTime ? min(0.1,AVAudioTime.seconds(forHostTime:now-render.hostTime)) : 0 + let position = anchor+Double(current.sampleTime)/file.processingFormat.sampleRate+elapsed*tempo + return min(duration,max(0,position)) + } func stopDeck() { guard canJog else { return } pauseDeck();loopEnabled = false;seek(0)