Le forme d'onda sonore (waveform) offrono un riscontro visivo straordinario nei player audio e nelle app di messaggistica vocale. Renderizzare centinaia di barre di campionamento senza cali di frame rate sui display ProMotion a 120 Hz richiede l'uso efficiente di SwiftUI.Canvas o shader Metal personalizzati per evitare sovraccarichi sul main thread.
1. Estrazione Asincrona delle Ampiezze da File Audio
Utilizziamo AVAssetReader per estrarre i campioni PCM in blocchi normalizzati tra 0.0 e 1.0:
import AVFoundation
public final class WaveformExtractor {
public static func extractSamples(from url: URL, targetCount: Int = 80) async -> [Float] {
guard let file = try? AVAudioFile(forReading: url),
let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length)) else {
return Array(repeating: 0.2, count: targetCount)
}
try? file.read(into: buffer)
guard let channelData = buffer.floatChannelData?[0] else { return [] }
let frameCount = Int(buffer.frameLength)
let blockSize = max(1, frameCount / targetCount)
var samples: [Float] = []
for i in 0..
2. Rendering a 120 FPS con SwiftUI Canvas
L'uso di Canvas garantisce un rendering hardware accelerato tramite Metal senza allocare decine di visualizzazioni Rectangle separate:
import SwiftUI
struct InteractiveWaveformView: View {
let samples: [Float]
@Binding var progress: Double // da 0.0 a 1.0
var body: some View {
Canvas { context, size in
let barWidth = size.width / CGFloat(samples.count)
let spacing: CGFloat = 2.0
for (index, sample) in samples.enumerated() {
let x = CGFloat(index) * barWidth + spacing / 2
let barHeight = size.height * CGFloat(sample)
let y = (size.height - barHeight) / 2
let rect = CGRect(x: x, y: y, width: barWidth - spacing, height: barHeight)
let isPlayed = Double(index) / Double(samples.count) <= progress
let color = isPlayed ? Color.orange : Color.gray.opacity(0.3)
context.fill(Path(roundedRect: rect, cornerRadius: 2), with: .color(color))
}
}
.frame(height: 48)
.gesture(
DragGesture(minimumDistance: 0)
.onChanged { value in
let newProgress = max(0.0, min(value.location.x / 300.0, 1.0))
progress = newProgress
UIImpactFeedbackGenerator(style: .light).impactOccurred()
}
)
}
}
Cerchi un'interfaccia audio interattiva o uno scrubber custom per iOS?
Realizziamo visualizzatori audio, equalizzatori spettrometrici e interfacce grafiche Metal a 120 FPS per iPhone e iPad.