|
|
@@ -543,32 +543,87 @@ private struct PlaybackWaveformCanvas: View, Equatable {
|
|
|
)
|
|
|
|
|
|
guard barCount > 0, !waveformSamples.isEmpty else { return }
|
|
|
- for index in 0..<barCount {
|
|
|
- let sampleStart = index * waveformSamples.count / barCount
|
|
|
- let sampleEnd = max(
|
|
|
- sampleStart + 1,
|
|
|
- (index + 1) * waveformSamples.count / barCount
|
|
|
- )
|
|
|
- let upperBound = min(sampleEnd, waveformSamples.count)
|
|
|
- guard sampleStart < upperBound else { continue }
|
|
|
-
|
|
|
- let amplitude = waveformSamples[sampleStart..<upperBound].max() ?? 0
|
|
|
- let barHeight = max(CGFloat(max(amplitude, 0)) * size.height * 0.75, 1)
|
|
|
- let rect = CGRect(
|
|
|
- x: startX + CGFloat(index) * barSlot,
|
|
|
- y: centerY - barHeight / 2,
|
|
|
- width: barWidth,
|
|
|
- height: barHeight
|
|
|
+ let envelopes = WaveformMinMaxDownsampler.envelopes(
|
|
|
+ waveformSamples,
|
|
|
+ targetBucketCount: barCount
|
|
|
+ )
|
|
|
+ guard !envelopes.isEmpty else { return }
|
|
|
+ let bucketWidth = waveformWidth / CGFloat(envelopes.count)
|
|
|
+
|
|
|
+ for (index, envelope) in envelopes.enumerated() {
|
|
|
+ let maximumHeight = max(
|
|
|
+ CGFloat(max(envelope.maximum, 0)) * size.height * 0.75,
|
|
|
+ 1
|
|
|
)
|
|
|
+ let x = startX + CGFloat(index) * bucketWidth
|
|
|
context.fill(
|
|
|
- Path(rect),
|
|
|
- with: .color(Color.primary.opacity(0.85))
|
|
|
+ Path(CGRect(
|
|
|
+ x: x,
|
|
|
+ y: centerY - maximumHeight / 2,
|
|
|
+ width: min(barWidth, bucketWidth),
|
|
|
+ height: maximumHeight
|
|
|
+ )),
|
|
|
+ with: .color(Color.primary.opacity(0.58))
|
|
|
)
|
|
|
+
|
|
|
+ let minimumHeight = CGFloat(max(envelope.minimum, 0)) * size.height * 0.75
|
|
|
+ if minimumHeight >= 1 {
|
|
|
+ context.fill(
|
|
|
+ Path(CGRect(
|
|
|
+ x: x,
|
|
|
+ y: centerY - minimumHeight / 2,
|
|
|
+ width: min(barWidth, bucketWidth),
|
|
|
+ height: minimumHeight
|
|
|
+ )),
|
|
|
+ with: .color(Color.primary.opacity(0.9))
|
|
|
+ )
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+private struct WaveformEnvelope {
|
|
|
+ let minimum: Float
|
|
|
+ let maximum: Float
|
|
|
+}
|
|
|
+
|
|
|
+/// Reduces an arbitrarily long recording to at most one min/max envelope per
|
|
|
+/// drawable screen bucket. Peaks survive zooming and scrolling without
|
|
|
+/// creating one SwiftUI element (or one Canvas draw) per source sample.
|
|
|
+private enum WaveformMinMaxDownsampler {
|
|
|
+ static func envelopes(
|
|
|
+ _ samples: [Float],
|
|
|
+ targetBucketCount: Int
|
|
|
+ ) -> [WaveformEnvelope] {
|
|
|
+ guard !samples.isEmpty, targetBucketCount > 0 else { return [] }
|
|
|
+ let bucketCount = min(samples.count, targetBucketCount)
|
|
|
+ var result: [WaveformEnvelope] = []
|
|
|
+ result.reserveCapacity(bucketCount)
|
|
|
+
|
|
|
+ for bucket in 0..<bucketCount {
|
|
|
+ let lowerBound = bucket * samples.count / bucketCount
|
|
|
+ let upperBound = max(
|
|
|
+ lowerBound + 1,
|
|
|
+ (bucket + 1) * samples.count / bucketCount
|
|
|
+ )
|
|
|
+ var minimum = Float.greatestFiniteMagnitude
|
|
|
+ var maximum = -Float.greatestFiniteMagnitude
|
|
|
+ for sample in samples[lowerBound..<min(upperBound, samples.count)] {
|
|
|
+ minimum = min(minimum, sample)
|
|
|
+ maximum = max(maximum, sample)
|
|
|
+ }
|
|
|
+ result.append(
|
|
|
+ WaveformEnvelope(
|
|
|
+ minimum: minimum.isFinite ? minimum : 0,
|
|
|
+ maximum: maximum.isFinite ? maximum : 0
|
|
|
+ )
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return result
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
// MARK: - Preview
|
|
|
|
|
|
#Preview {
|