iOS AVAudioEngine 录制音频文件和播放音频文件

AVAudioEngine 是 AVFoundation 的其中一部分,AVAudioEngine 封装了底层的 Audio Unit,并提供了 Swift / Objective-C API,本文讲解如何实现录制音频文件(可以混合一路背景音乐文件)和播放音频文件。
完整代码和参考:
- AudioEngineRecorder
- AudioEnginePlayer
- syedhali / AudioStreamer
- AVAudioEngine Tutorial for iOS: Getting Started
AVAudioEngine
iOS 中的音频处理框架如下,Audio Unit 位于 AudioToolbox 中,提供 C API,iOS 8 后已经弃用,AVAudioEngine 位于 AVFoundation 中,提供 Swift / Objective-C API:
AVAudioEngine 封装了底层的 Audio Unit,所以很多概念是相同的,这里主要说一下,首先,Audio Unit 是拉的模式,而 AVAudioEngine 是推的模式。
其次,AVAudioEngine 中已经有 inputNode、outputNode 和 mainMixerNode 用于输入、输出和混合,所以不需要再单独创建节点,只需要将其和相应节点连接起来。
var inputNode: AVAudioInputNode { get }
var outputNode: AVAudioOutputNode { get }
var mainMixerNode: AVAudioMixerNode { get }
AVAudioEngine 录制音频文件
第一步,创建 AVAudioEngine:
private let engine = AVAudioEngine()
第二步,添加 AVAudioNode:
private let filePlayer = AVAudioPlayerNode()
private let rateEffect = AVAudioUnitTimePitch()
engine.attach(filePlayer)
engine.attach(rateEffect)
第三步,连接 AVAudioNode:
engine.connect(engine.inputNode, to: engine.mainMixerNode, fromBus: 0,
toBus: 0, format: format)
if let bgmFile = bgmFile {
engine.connect(filePlayer, to: rateEffect, format: format)
engine.connect(rateEffect, to: engine.mainMixerNode, fromBus: 0,
toBus: 1, format: bgmFile.processingFormat)
}
第四步,准备 AVAudioEngine:
engine.prepare()
第五步,安装 Tap:
Tap 就是水龙头,比喻从数据流中安装一个水龙头来获取音频数据,从而可以写到文件或者传递给外部回调,类似于 Audio Unit 中的 Render Callback,注意拉的模式和推的模式导致使用方法是不同的:
private func installTap() {
let outputFormat = engine.mainMixerNode.outputFormat(forBus: 0)
engine.mainMixerNode.installTap(onBus: 0, bufferSize: 4096, format: outputFormat) { [weak self] buffer, when in
guard let self = self else { return }
if let outputFile = self.outputFile {
do {
try outputFile.write(from: buffer)
} catch let error {
DDLogError("Could not write buffer data to file \(error.localizedDescription)")
}
} else if let delegate = self.delegate, let converter = self.converter {
let inputCallback: AVAudioConverterInputBlock = { inNumPackets, outStatus in
outStatus.pointee = AVAudioConverterInputStatus.haveData
return buffer
}
let convertedBuffer = AVAudioPCMBuffer(pcmFormat: self.converterFormat,
frameCapacity: AVAudioFrameCount(self.converterFormat.sampleRate) * buffer.frameLength / AVAudioFrameCount(buffer.format.sampleRate))!
var error: NSError? = nil
let statusCode = converter.convert(to: convertedBuffer, error: &error, withInputFrom: inputCallback)
if statusCode == .error {
DDLogError("AVAudioConverter failed \(error?.localizedDescription ?? "")")
exit(1)
}
delegate.audioRecorder(self, receive: convertedBuffer.audioBufferList.pointee.mBuffers)
}
}
}
第六步,启动 AVAudioEngine:
try engine.start()
第七步,停止 AVAudioEngine:
engine.stop()
AVAudioEngine 播放音频文件
通过 AVAudioEngine 来播放音频文件和录制音频文件的步骤并没有什么太大不同,唯一值得关注的是如何设置 AVAudioPlayerNode 需要的音频文件,前面的录制音频文件也用到此功能来混合一路背景音乐文件:
class AudioEnginePlayer {
private let fileURL: URL
private var file: AVAudioFile?
private let filePlayer = AVAudioPlayerNode()
private func setupFilePlayer() {
file = try? AVAudioFile(forReading: fileURL)
}
private func scheduleAudioFile() {
guard let file = file else { return }
filePlayer.scheduleFile(file, at: nil) { [weak self] in
self?.scheduleAudioFile()
}
}
}