import Accelerate import AVFoundation public class WaveformExtractor { public private(set) var audioFile: AVAudioFile? private var result: FlutterResult var flutterChannel: FlutterMethodChannel private var waveformData = Array() var progress: Float = 0.0 var channelCount: Int = 1 private var currentProgress: Float = 0.0 private let abortWaveformDataQueue = DispatchQueue( label: "WaveformExtractor", attributes: .concurrent ) private var _abortGetWaveformData: Bool = false public var abortGetWaveformData: Bool { get { _abortGetWaveformData } set { abortWaveformDataQueue.async(flags: .barrier) { self._abortGetWaveformData = newValue } } } public init(url: URL, flutterResult: @escaping FlutterResult, channel: FlutterMethodChannel) throws { result = flutterResult self.flutterChannel = channel do { audioFile = try AVAudioFile(forReading: url) } catch { audioFile = nil result(FlutterError(code: Constants.audioWaveforms, message: error.localizedDescription, details: "Couldn't initialise AVAudioFile from \(url.absoluteString)")) } } deinit { audioFile = nil } public func extractWaveform( samplesPerPixel: Int?, offset: Int? = 0, length: UInt? = nil, playerKey: String, onExtractionComplete: ([Float]?) -> Void ) async -> Void { guard let audioFile = audioFile else { return } /// Prevent division by zero, + minimum resolution let samplesPerPixel = max(1, samplesPerPixel ?? 100) let currentFrame = audioFile.framePosition let totalFrames = AVAudioFrameCount(audioFile.length) var framesPerBuffer = totalFrames / AVAudioFrameCount(samplesPerPixel) guard let rmsBuffer = AVAudioPCMBuffer( pcmFormat: audioFile.processingFormat, frameCapacity: framesPerBuffer ) else { return } let channelCount = Int(audioFile.processingFormat.channelCount) let waveformStorage = WaveformStorage( channelCount: channelCount, size: samplesPerPixel ) let startIndex = max( 0, offset ?? Int(currentFrame / Int64(framesPerBuffer)) ) let endIndex = min( samplesPerPixel, startIndex + (length.map { Int($0) } ?? samplesPerPixel) ) if startIndex > endIndex { sendErrorToFlutter( message: "Offset is larger than total length.", details: "Please select less number of samples" ) return } var startFrame: AVAudioFramePosition = offset == nil ? currentFrame : Int64(startIndex * Int(framesPerBuffer)) for i in startIndex.. totalFrames { framesPerBuffer = totalFrames - AVAudioFrameCount(startFrame) if framesPerBuffer <= 0 { break } } } audioFile.framePosition = currentFrame let waveformData = await waveformStorage.getData() let data = getChannelMean(data: waveformData) onExtractionComplete(data); } func getChannelMean(data: FloatChannelData) -> [Float] { var resultWaveform = [Float]() if channelCount == 2, !data[0].isEmpty, !data[1].isEmpty { resultWaveform = zip(data[0], data[1]).map { ($0 + $1) / 2 } } else if !data[0].isEmpty { resultWaveform = data[0] } else if !data[1].isEmpty { resultWaveform = data[1] } else { sendErrorToFlutter( message: "Cannot get waveform mean", details: "Both audio channels are null" ) } return resultWaveform } public func cancel() { abortGetWaveformData = true } private func sendWaveformDataToFlutter( waveformStorage: WaveformStorage, progress: Float, playerKey: String ) async { let waveformData = await waveformStorage.getData() let meanData = getChannelMean(data: waveformData) DispatchQueue.main.async { self.flutterChannel.invokeMethod( Constants.onCurrentExtractedWaveformData, arguments: [ Constants.waveformData: meanData, Constants.progress: progress, Constants.playerKey: playerKey ] ) } } private func sendErrorToFlutter(message: String, details: String? = nil) { DispatchQueue.main.async { self.result( FlutterError( code: Constants.audioWaveforms, message: message, details: details ) ) } } } actor WaveformStorage { private var data: [[Float]] init(channelCount: Int, size: Int) { data = Array(repeating: [Float](repeating: 0, count: size), count: channelCount) } func update(channel: Int, index: Int, value: Float) { data[channel][index] = value } func getData() -> [[Float]] { return data } }