Skip to content

Commit c2e5261

Browse files
committed
avoid costly re-sampling during resizing
and do a low-quality re-scale instead; this is just for illustration for #104
1 parent 4c56578 commit c2e5261

File tree

1 file changed

+39
-9
lines changed

1 file changed

+39
-9
lines changed

Sources/DSWaveformImageViews/SwiftUI/WaveformView.swift

+39-9
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public struct WaveformView<Content: View>: View {
1111
private let content: (WaveformShape) -> Content
1212

1313
@State private var samples: [Float] = []
14+
@State private var rescaleTimer: Timer?
15+
@State private var currentSize: CGSize = .zero
1416

1517
/**
1618
Creates a new WaveformView which displays a waveform for the audio at `audioURL`.
@@ -39,26 +41,54 @@ public struct WaveformView<Content: View>: View {
3941
public var body: some View {
4042
GeometryReader { geometry in
4143
content(WaveformShape(samples: samples, configuration: configuration, renderer: renderer))
44+
.scaleEffect(x: scaleDuringResize(for: geometry), y: 1, anchor: .trailing)
4245
.onAppear {
4346
guard samples.isEmpty else { return }
4447
update(size: geometry.size, url: audioURL, configuration: configuration)
4548
}
46-
.modifier(OnChange(of: geometry.size, action: { newValue in update(size: newValue, url: audioURL, configuration: configuration) }))
49+
.modifier(OnChange(of: geometry.size, action: { newValue in update(size: newValue, url: audioURL, configuration: configuration, delayed: true) }))
4750
.modifier(OnChange(of: audioURL, action: { newValue in update(size: geometry.size, url: audioURL, configuration: configuration) }))
4851
.modifier(OnChange(of: configuration, action: { newValue in update(size: geometry.size, url: audioURL, configuration: newValue) }))
4952
}
5053
}
5154

52-
private func update(size: CGSize, url: URL, configuration: Waveform.Configuration) {
53-
Task(priority: priority) {
54-
do {
55-
let samplesNeeded = Int(size.width * configuration.scale)
56-
let samples = try await WaveformAnalyzer().samples(fromAudioAt: url, count: samplesNeeded)
57-
await MainActor.run { self.samples = samples }
58-
} catch {
59-
assertionFailure(error.localizedDescription)
55+
private func update(size: CGSize, url: URL, configuration: Waveform.Configuration, delayed: Bool = false) {
56+
rescaleTimer?.invalidate()
57+
58+
let updateTask: @Sendable (Timer?) -> Void = { _ in
59+
Task(priority: .userInitiated) {
60+
do {
61+
let samplesNeeded = Int(size.width * configuration.scale)
62+
let samples = try await WaveformAnalyzer().samples(fromAudioAt: url, count: samplesNeeded)
63+
64+
await MainActor.run {
65+
self.currentSize = size
66+
self.samples = samples
67+
}
68+
} catch {
69+
assertionFailure(error.localizedDescription)
70+
}
6071
}
6172
}
73+
74+
if delayed {
75+
rescaleTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: false, block: updateTask)
76+
RunLoop.main.add(rescaleTimer!, forMode: .common)
77+
} else {
78+
updateTask(nil)
79+
}
80+
}
81+
82+
/*
83+
* During resizing, we only visually scale the shape to make it look more seamless,
84+
* before we re-calculate the pixel-perfect re-sampled waveform, which is costly.
85+
* Due to the complex way we need to render the actual waveform based on samples
86+
* available and size to occupy, the re-scaling currently only supports enlarging.
87+
* If we resize to a smaller size, the waveform simply overflows.
88+
*/
89+
private func scaleDuringResize(for geometry: GeometryProxy) -> CGFloat {
90+
guard currentSize != .zero else { return 1 }
91+
return max(geometry.size.width / currentSize.width, 1)
6292
}
6393
}
6494

0 commit comments

Comments
 (0)