import Accelerate import AVFoundation import Foundation /// Generates waveform data from audio files for visualization. struct WaveformGenerator { struct WaveformSample: Codable { let min: Float let max: Float } /// Generate downsampled waveform from an audio file. static func generateWaveform(for track: Track, targetSampleCount: Int = 300) async throws -> [WaveformSample] { let url = track.fileURL let file = try AVAudioFile(forReading: url) guard let buffer = AVAudioPCMBuffer(pcmFormat: file.processingFormat, frameCapacity: AVAudioFrameCount(file.length)) else { return [] } try file.read(into: buffer) guard let floatData = buffer.floatChannelData else { return [] } let frameCount = Int(buffer.frameLength) let channelCount = Int(buffer.format.channelCount) // Mix to mono var monoSamples = [Float](repeating: 0, count: frameCount) for ch in 0.. 1 { var divisor = Float(channelCount) vDSP_vsdiv(monoSamples, 1, &divisor, &monoSamples, 1, vDSP_Length(frameCount)) } // Downsample to target count let samplesPerBucket = max(1, frameCount / targetSampleCount) var waveform: [WaveformSample] = [] waveform.reserveCapacity(targetSampleCount) for i in 0.. 0 else { continue } var minVal: Float = 0 var maxVal: Float = 0 monoSamples.withUnsafeBufferPointer { ptr in let base = ptr.baseAddress! + start vDSP_minv(base, 1, &minVal, count) vDSP_maxv(base, 1, &maxVal, count) } waveform.append(WaveformSample(min: minVal, max: maxVal)) } // Cache on the track if let encoded = try? JSONEncoder().encode(waveform) { track.waveformData = encoded } return waveform } /// Decode cached waveform data. static func decodeCachedWaveform(from data: Data) -> [WaveformSample]? { try? JSONDecoder().decode([WaveformSample].self, from: data) } }