performance updates
This commit is contained in:
@@ -1700,110 +1700,21 @@ private struct NowPlayingWaveform: View {
|
||||
@ObservedObject var player: Player
|
||||
@State private var mixer = false
|
||||
@State private var zoom: Double = 1
|
||||
@State private var hoverX: CGFloat?
|
||||
@State private var scrubStart: Double?
|
||||
@State private var resumeAfterScrub = false
|
||||
@State private var scrubSong: Int64?
|
||||
private var span: Double { max(0.001,player.duration/zoom) }
|
||||
private var windowStart: Double { scrubStart ?? min(max(0,player.time-span/2),max(0,player.duration-span)) }
|
||||
private var progress: Double { player.duration > 0 ? min(1,max(0,(player.time-windowStart)/span)) : 0 }
|
||||
@State private var bpm: Double?
|
||||
private func clock(_ value: Double) -> String { let n = max(0,Int(value));return String(format:"%d:%02d",n/60,n%60) }
|
||||
private func preciseClock(_ value: Double) -> String { String(format:"%d:%05.2f",max(0,Int(value))/60,max(0,value).truncatingRemainder(dividingBy:60)) }
|
||||
private func bandColor(_ index: Int) -> Color {
|
||||
guard player.waveformBands.indices.contains(index) else { return .cyan }
|
||||
let band = player.waveformBands[index]
|
||||
let largest = max(0.000001,max(band.low,max(band.mid,band.high)))
|
||||
let low = Double(band.low/largest),mid = Double(band.mid/largest),high = Double(band.high/largest)
|
||||
return Color(red:min(1,pow(low,1.2)+mid*0.12+high*0.6),green:min(1,low*0.08+pow(mid,1.5)*0.97+high*0.1),blue:min(1,low*0.07+mid*0.22+high))
|
||||
}
|
||||
var body: some View {
|
||||
VStack(alignment:.leading,spacing:5) {
|
||||
Text(player.duration > 0 ? player.title : "Ready to play").font(.system(size:13,weight:.semibold)).foregroundStyle(Color(white:0.94))
|
||||
.lineLimit(1).truncationMode(.middle).frame(maxWidth:.infinity,alignment:.leading).help(player.title)
|
||||
GeometryReader { geometry in
|
||||
Canvas { context,size in
|
||||
let top: CGFloat = 10,plotHeight = size.height-top,mid = top+plotHeight/2
|
||||
var centre = Path();centre.move(to:CGPoint(x:0,y:mid));centre.addLine(to:CGPoint(x:size.width,y:mid))
|
||||
context.stroke(centre,with:.color(.white.opacity(0.13)),lineWidth:0.5)
|
||||
if player.duration > 0 {
|
||||
let step = [0.1,0.25,0.5,1.0,2,5,10,15,30,60,120,300,600,1800,3600].first { $0 >= span/5 } ?? 3600
|
||||
let first = floor(windowStart/(step/4))*(step/4)
|
||||
for t in stride(from:first,through:min(player.duration,windowStart+span),by:step/4) {
|
||||
let x = size.width*(t-windowStart)/span
|
||||
if x < 0 { continue }
|
||||
let major = abs(t/step-(t/step).rounded()) < 0.001
|
||||
var tick = Path();tick.move(to:CGPoint(x:x,y:top));tick.addLine(to:CGPoint(x:x,y:size.height))
|
||||
context.stroke(tick,with:.color(major ? .yellow.opacity(0.65) : .white.opacity(0.16)),lineWidth:major ? 0.8 : 0.5)
|
||||
if major && x < size.width-25 {
|
||||
context.draw(Text(span < 10 ? preciseClock(t) : clock(t)).font(.system(size:8,design:.monospaced)).foregroundColor(.gray),at:CGPoint(x:x+3,y:3),anchor:.leading)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !player.waveform.isEmpty {
|
||||
let count = max(1,Int(size.width/2)),step = size.width/CGFloat(count)
|
||||
for bar in 0..<count {
|
||||
let total = player.waveform.count
|
||||
let left = windowStart+Double(bar)/Double(count)*span,right = windowStart+Double(bar+1)/Double(count)*span
|
||||
let start = min(total-1,max(0,Int(left/max(player.duration,0.001)*Double(total))))
|
||||
let end = min(total,max(start+1,Int(right/max(player.duration,0.001)*Double(total))))
|
||||
let level = player.waveform[start..<min(end,player.waveform.count)].max() ?? 0
|
||||
let height = max(0.7,CGFloat(level)*(plotHeight-3)),x = CGFloat(bar)*step
|
||||
let path = Path(CGRect(x:x,y:mid-height/2,width:max(0.7,step-0.7),height:height))
|
||||
context.fill(path,with:.color(bandColor(start).opacity(x <= size.width*progress ? 1 : 0.88)))
|
||||
}
|
||||
if let hoverX {
|
||||
let hx = min(size.width,max(0,hoverX))
|
||||
var hover = Path();hover.move(to:CGPoint(x:hx,y:top));hover.addLine(to:CGPoint(x:hx,y:size.height))
|
||||
context.stroke(hover,with:.color(.white.opacity(0.7)),style:StrokeStyle(lineWidth:1,dash:[2,2]))
|
||||
let time = windowStart+Double(hx/size.width)*span
|
||||
context.draw(Text(preciseClock(time)).font(.system(size:9,weight:.bold,design:.monospaced)).foregroundColor(.white),at:CGPoint(x:min(size.width-28,max(28,hx)),y:3))
|
||||
}
|
||||
} else if player.waveformLoading {
|
||||
context.draw(Text("Analysing waveform…").font(.system(size:9)).foregroundColor(.gray),at:CGPoint(x:size.width/2,y:mid))
|
||||
}
|
||||
if player.duration > 0 {
|
||||
let x = min(size.width-2,max(2,size.width*progress))
|
||||
context.fill(Path(CGRect(x:0,y:top,width:x,height:plotHeight)),with:.color(.black.opacity(0.18)))
|
||||
var head = Path();head.move(to:CGPoint(x:x,y:top));head.addLine(to:CGPoint(x:x,y:size.height))
|
||||
context.stroke(head,with:.color(.black.opacity(0.9)),lineWidth:6)
|
||||
context.stroke(head,with:.color(.cyan.opacity(0.3)),lineWidth:5)
|
||||
context.stroke(head,with:.color(.white),lineWidth:2)
|
||||
var pointer = Path();pointer.move(to:CGPoint(x:x-5,y:top));pointer.addLine(to:CGPoint(x:x+5,y:top));pointer.addLine(to:CGPoint(x:x,y:top+7));pointer.closeSubpath()
|
||||
context.fill(pointer,with:.color(.cyan))
|
||||
let labelX = min(size.width-30,max(30,x))
|
||||
context.fill(Path(roundedRect:CGRect(x:labelX-29,y:0,width:58,height:13),cornerRadius:3),with:.color(.cyan))
|
||||
context.draw(Text(preciseClock(player.time)).font(.system(size:9,weight:.bold,design:.monospaced)).foregroundColor(.black),at:CGPoint(x:labelX,y:6))
|
||||
}
|
||||
}.contentShape(Rectangle()).background(WaveformJogWheel(player:player))
|
||||
.onContinuousHover { phase in switch phase { case .active(let point):hoverX = point.x;case .ended:hoverX = nil } }
|
||||
.gesture(DragGesture(minimumDistance:0).onChanged { value in
|
||||
guard player.duration > 0,geometry.size.width > 0 else { return }
|
||||
if scrubStart == nil {
|
||||
scrubStart = windowStart;scrubSong = player.currentSongID;resumeAfterScrub = player.playing
|
||||
if player.playing { player.toggle() }
|
||||
}
|
||||
guard scrubSong == player.currentSongID else { return }
|
||||
hoverX = value.location.x
|
||||
let target = windowStart+Double(min(1,max(0,value.location.x/geometry.size.width)))*span
|
||||
if abs(target-player.time) > 0.02 { player.seek(target) }
|
||||
}.onEnded { value in
|
||||
if scrubStart == nil {
|
||||
if player.duration > 0 { player.seek(windowStart+Double(min(1,max(0,value.location.x/max(1,geometry.size.width))))*span) }
|
||||
return
|
||||
}
|
||||
let target = windowStart+Double(min(1,max(0,value.location.x/max(1,geometry.size.width))))*span
|
||||
if scrubSong == player.currentSongID {
|
||||
player.seek(target)
|
||||
if resumeAfterScrub && !player.playing && target < player.duration-0.01 { player.toggle() }
|
||||
}
|
||||
scrubStart = nil;resumeAfterScrub = false;scrubSong = nil
|
||||
}).help("Hover for exact time. Drag to scrub; playback resumes on release. Zoom for fine positioning.")
|
||||
.accessibilityLabel("Audio waveform").accessibilityValue("\(clock(player.time)) of \(clock(player.duration))")
|
||||
.accessibilityAdjustableAction { direction in if direction == .increment { player.seek(player.time+5) } else if direction == .decrement { player.seek(player.time-5) } }
|
||||
}.frame(height:60)
|
||||
DJWaveform(player:player,color:.cyan,bpm:bpm).frame(height:14)
|
||||
DJScrollingSpectrum(player:player,bpm:bpm,seconds:16/zoom)
|
||||
.frame(height:46).background(.black)
|
||||
.background(WaveformJogWheel(player:player))
|
||||
.accessibilityAdjustableAction { direction in player.seek(player.time+(direction == .increment ? 5 : -5)) }
|
||||
HStack(spacing:5) {
|
||||
Circle().fill(player.playing ? Color.green : Color.orange).frame(width:4,height:4)
|
||||
Text(player.duration == 0 ? "NO TRACK LOADED" : player.playing ? "PLAYING" : "PAUSED").tracking(1)
|
||||
if let bpm { Text(String(format:"· %.1f BPM~",bpm*player.tempo)).monospacedDigit() }
|
||||
if player.waveformUnavailable { Text("· Waveform unavailable").foregroundStyle(.secondary) }
|
||||
Spacer(minLength:0)
|
||||
Button { zoom = max(1,zoom/2) } label: { Image(systemName:"minus.magnifyingglass") }.buttonStyle(.plain).disabled(zoom <= 1).help("Zoom out")
|
||||
@@ -1822,6 +1733,13 @@ private struct NowPlayingWaveform: View {
|
||||
.background(Color(.sRGB,red:0.055,green:0.065,blue:0.08,opacity:1),in:RoundedRectangle(cornerRadius:10))
|
||||
.overlay(RoundedRectangle(cornerRadius:10).strokeBorder(.white.opacity(0.1),lineWidth:1))
|
||||
.environment(\.colorScheme,.dark)
|
||||
.task(id:player.currentSongID) {
|
||||
bpm = nil
|
||||
guard let source = player.sourceAudioURL,!player.isShowScreen else { return }
|
||||
let analysis = Task.detached(priority:.utility) { try? DJBPM.analyse(source,cancelled:{Task.isCancelled}) }
|
||||
let result = await withTaskCancellationHandler(operation:{ await analysis.value },onCancel:{ analysis.cancel() })
|
||||
if !Task.isCancelled { bpm = result }
|
||||
}
|
||||
}
|
||||
}
|
||||
private struct ToneKnob: View {
|
||||
|
||||
Reference in New Issue
Block a user