twonly-app-dependencies/audio_waveforms/ios/Classes/WaveformExtractor.swift
2026-08-22 20:30:14 +02:00

209 lines
6.7 KiB
Swift

import Accelerate
import AVFoundation
public class WaveformExtractor {
public private(set) var audioFile: AVAudioFile?
private var result: FlutterResult
var flutterChannel: FlutterMethodChannel
private var waveformData = Array<Float>()
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..<endIndex {
if abortGetWaveformData {
audioFile.framePosition = currentFrame
abortGetWaveformData = false
return
}
do {
audioFile.framePosition = startFrame
try audioFile.read(into: rmsBuffer, frameCount: framesPerBuffer)
} catch {
sendErrorToFlutter(
message: "Couldn't read buffer. \(error.localizedDescription)"
)
return
}
guard let floatData = rmsBuffer.floatChannelData else { return }
for channel in 0..<channelCount {
/// Calculating RMS(Root mean square)
var rmsValue: Float = 0.0
vDSP_rmsqv(
floatData[channel], 1, &rmsValue,
vDSP_Length(rmsBuffer.frameLength)
)
await waveformStorage.update(
channel: channel, index: i, value: rmsValue
)
}
let progress = Float(i - startIndex + 1) / Float(endIndex - startIndex)
await sendWaveformDataToFlutter(
waveformStorage: waveformStorage,
progress: progress,
playerKey: playerKey
)
startFrame += AVAudioFramePosition(framesPerBuffer)
if startFrame + AVAudioFramePosition(framesPerBuffer) > 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
}
}