chua's picture
Add iOS ASR source and docs
64f7370 verified
Raw
History Blame Contribute Delete
2.95 kB
import AVFoundation
import Foundation
/// Converts arbitrary PCM input (any sample rate / channel count) to the
/// 16 kHz mono Float32 the models expect. Wraps AVAudioConverter with a
/// simple incremental API safe to call from one audio-callback thread.
public final class AudioResampler {
public static let targetRate = 16_000.0
private var converter: AVAudioConverter?
private var sourceFormat: AVAudioFormat?
private let targetFormat = AVAudioFormat(
commonFormat: .pcmFormatFloat32, sampleRate: targetRate,
channels: 1, interleaved: false)!
public init() {}
/// Converts one buffer. Reconfigures automatically if the input format
/// changes (e.g. route change to a Bluetooth mic).
public func convert(_ buffer: AVAudioPCMBuffer) throws -> [Float] {
let format = buffer.format
if format.sampleRate == Self.targetRate, format.channelCount == 1,
format.commonFormat == .pcmFormatFloat32, let ch = buffer.floatChannelData {
return Array(UnsafeBufferPointer(start: ch[0], count: Int(buffer.frameLength)))
}
if converter == nil || sourceFormat != format {
guard let c = AVAudioConverter(from: format, to: targetFormat) else {
throw SpeechError.audioFormatUnsupported(format.description)
}
converter = c
sourceFormat = format
}
let ratio = Self.targetRate / format.sampleRate
let capacity = AVAudioFrameCount(Double(buffer.frameLength) * ratio) + 16
guard let out = AVAudioPCMBuffer(pcmFormat: targetFormat, frameCapacity: capacity) else {
throw SpeechError.resamplingFailed("buffer allocation")
}
var err: NSError?
var served = false
converter!.convert(to: out, error: &err) { _, status in
if served { status.pointee = .noDataNow; return nil }
served = true
status.pointee = .haveData
return buffer
}
if let err { throw SpeechError.resamplingFailed(err.localizedDescription) }
guard let ch = out.floatChannelData else {
throw SpeechError.resamplingFailed("no channel data")
}
return Array(UnsafeBufferPointer(start: ch[0], count: Int(out.frameLength)))
}
/// Loads an audio file and returns 16 kHz mono samples.
public static func loadFile(_ url: URL) throws -> [Float] {
guard let file = try? AVAudioFile(forReading: url) else {
throw SpeechError.audioFormatUnsupported("cannot open \(url.lastPathComponent)")
}
guard let buf = AVAudioPCMBuffer(pcmFormat: file.processingFormat,
frameCapacity: AVAudioFrameCount(file.length)) else {
throw SpeechError.resamplingFailed("file buffer allocation")
}
try file.read(into: buf)
return try AudioResampler().convert(buf)
}
}