broken changes
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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..<count {
|
||||
let index = min(values.count-1,i*values.count/count),height = max(1,Double(values[index])*size.height)
|
||||
context.fill(Path(CGRect(x:Double(i)*size.width/Double(count),y:(size.height-height)/2,width:1.4,height:height)),with:.color(color.opacity(Double(i)/Double(count) < player.time/max(0.001,player.duration) ? 0.4 : 0.9)))
|
||||
context.fill(Path(CGRect(x:Double(i)*size.width/Double(count),y:(size.height-height)/2,width:1.4,height:height)),with:.color(color.opacity(Double(i)/Double(count) < displayTime/max(0.001,player.duration) ? 0.4 : 0.9)))
|
||||
}
|
||||
DJWaveMarkers.draw(context:&context,size:size,start:0,span:player.duration,bpm:bpm,loop:player.loopEnabled ? player.loopIn.flatMap { a in player.loopOut.map { a...$0 } } : nil)
|
||||
|
||||
let x = player.time/max(0.001,player.duration)*size.width
|
||||
let x = displayTime/max(0.001,player.duration)*size.width
|
||||
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) } }
|
||||
.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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
private struct DJMappingSheet: View {
|
||||
@@ -1224,10 +1230,12 @@ private struct DJCentreWaveform: View {
|
||||
var settings = DJPreferences()
|
||||
@State private var frozenStart: 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
|
||||
guard !player.waveform.isEmpty,player.duration > 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..<count {
|
||||
let seconds = start+Double(i)/Double(count)*span
|
||||
guard seconds >= 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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user