Skip to content

Commit

Permalink
avoid costly re-sampling during resizing
Browse files Browse the repository at this point in the history
and do a low-quality re-scale instead; this is just for illustration for #104
  • Loading branch information
dmrschmidt committed Oct 10, 2024
1 parent 4c56578 commit c2e5261
Showing 1 changed file with 39 additions and 9 deletions.
48 changes: 39 additions & 9 deletions Sources/DSWaveformImageViews/SwiftUI/WaveformView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public struct WaveformView<Content: View>: View {
private let content: (WaveformShape) -> Content

@State private var samples: [Float] = []
@State private var rescaleTimer: Timer?
@State private var currentSize: CGSize = .zero

/**
Creates a new WaveformView which displays a waveform for the audio at `audioURL`.
Expand Down Expand Up @@ -39,26 +41,54 @@ public struct WaveformView<Content: View>: View {
public var body: some View {
GeometryReader { geometry in
content(WaveformShape(samples: samples, configuration: configuration, renderer: renderer))
.scaleEffect(x: scaleDuringResize(for: geometry), y: 1, anchor: .trailing)
.onAppear {
guard samples.isEmpty else { return }
update(size: geometry.size, url: audioURL, configuration: configuration)
}
.modifier(OnChange(of: geometry.size, action: { newValue in update(size: newValue, url: audioURL, configuration: configuration) }))
.modifier(OnChange(of: geometry.size, action: { newValue in update(size: newValue, url: audioURL, configuration: configuration, delayed: true) }))
.modifier(OnChange(of: audioURL, action: { newValue in update(size: geometry.size, url: audioURL, configuration: configuration) }))
.modifier(OnChange(of: configuration, action: { newValue in update(size: geometry.size, url: audioURL, configuration: newValue) }))
}
}

private func update(size: CGSize, url: URL, configuration: Waveform.Configuration) {
Task(priority: priority) {
do {
let samplesNeeded = Int(size.width * configuration.scale)
let samples = try await WaveformAnalyzer().samples(fromAudioAt: url, count: samplesNeeded)
await MainActor.run { self.samples = samples }
} catch {
assertionFailure(error.localizedDescription)
private func update(size: CGSize, url: URL, configuration: Waveform.Configuration, delayed: Bool = false) {
rescaleTimer?.invalidate()

let updateTask: @Sendable (Timer?) -> Void = { _ in
Task(priority: .userInitiated) {
do {
let samplesNeeded = Int(size.width * configuration.scale)
let samples = try await WaveformAnalyzer().samples(fromAudioAt: url, count: samplesNeeded)

await MainActor.run {
self.currentSize = size
self.samples = samples
}
} catch {
assertionFailure(error.localizedDescription)
}
}
}

if delayed {
rescaleTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false, block: updateTask)
RunLoop.main.add(rescaleTimer!, forMode: .common)
} else {
updateTask(nil)
}
}

/*
* During resizing, we only visually scale the shape to make it look more seamless,
* before we re-calculate the pixel-perfect re-sampled waveform, which is costly.
* Due to the complex way we need to render the actual waveform based on samples
* available and size to occupy, the re-scaling currently only supports enlarging.
* If we resize to a smaller size, the waveform simply overflows.
*/
private func scaleDuringResize(for geometry: GeometryProxy) -> CGFloat {
guard currentSize != .zero else { return 1 }
return max(geometry.size.width / currentSize.width, 1)
}
}

Expand Down

0 comments on commit c2e5261

Please sign in to comment.