Rendering di Forme d'Onda (Waveform) Interattive a 120 FPS in SwiftUI

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.