Swift 音频 DIY ,Audio Queue Services 搞缓冲,AVAudioEngine 加声效
播放网络音频,可以先下载好,得到音频文件,简单了
使用 AVAudioPlayer 播放,就完
苹果封装下,AVAudioPlayer 处理本地文件,很方便
直接拿到一个文件地址 url,播放
简单机械的理解:
便于音频的传输,一般使用音频压缩文件,mp3 等。文件压的体积小,好传输
声卡是播放 PCM 缓冲的
苹果帮开发把压缩格式,转换为未压缩的原始文件 PCM, 还帮开发做播放音频的资源调度,从 PCM 文件中拿出一段段的缓冲 buffer,交给声卡消费掉
( 实际不会分两步,过程当然是并行的 )
现在手动
本文介绍,直接搞音频流媒体
接收到网络上的音频数据包,就去播放。
分四步:
1,网络的音频文件 >> 下载到本地的音频 data
下载音频文件的二进制数据
URLSession 建立 task, 去获取网络文件
拿到一个数据包 Data,就处理一个
本例子中,一个数据包 Data,对应一个音频包 packet, 对应一个音频缓冲 buffer
这一步,比较容易,
建个 URLSessionDataTask ,去下载
要做的,都在网络代理方法里
extension Downloader: URLSessionDataDelegate {
// 开始下载,拿到文件的总体积
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) {
totalBytesCount = response.expectedContentLength
completionHandler(.allow)
}
// 接收数据
public func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
// 更新,下载到本地的数据总量
totalBytesReceived += Int64(data.count)
// 算进度
progress = Float(totalBytesReceived) / Float(totalBytesCount)
// data 教给代理,去解析为音频数据包
delegate?.download(self, didReceiveData: data, progress: progress)
}
// 下载完成了
public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
state = .completed
delegate?.download(self, completedWithError: error)
}
}
音频基础了解先:
音频文件,分为封装格式(文件格式),和编码格式
音频数据的三个层级,buffer, packet, frame
数据缓冲 buffer , 装音频包 packet,
音频包 packet,装音频帧 frame
音频按编码格式,一般分为可变码率 ,和固定码率
固定码率 CBR, 平均采样,对应原始文件,pcm ( 未压缩文件 )
可变码率 VBR,对应压缩文件,例如: mp3
Core Audio 支持 VBR,一般通过可变帧率格式 VFR
VFR 是指:每个包 packet 的体积相等,
包 packet 里面的帧 frame 的数量不一, 帧 frame 含有的音频数据有大有小
Core Audio 中数据描述
固定码率用 ASBD 描述,AudioStreamBasicDescription
ASBD 的描述, 就是指一些配置信息,包含通道数、采样率、位深...
可变码率中 VFR,用 ASPD 描述,AudioStreamPacketDescription
压缩音频数据中 VFR,对应 ASPD
每一个包 Packet,都有其 ASPD
ASPD 里面有,包 packet 的位置信息 mStartOffset
包 packet 的帧 frame 的个数,mVariableFramesInPacket
2,音频 data >> 音频包 Packet
拿 Audio Queue Services ,处理上一步获取的音频二进制数据 data,解析为音频数据包 packet
2.1 建立音频的处理通道, 注册解析回调方法
public init() throws {
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
// 创建一个活跃的音频文件流解析器,创建解析器 ID
guard AudioFileStreamOpen(context, ParserPropertyChangeCallback, ParserPacketCallback, kAudioFileMP3Type, &streamID) == noErr else {
throw ParserError.streamCouldNotOpen
}
}
2.2 传递数据进来,开始解析
public func parse(data: Data) throws {
let streamID = self.streamID!
let count = data.count
_ = try data.withUnsafeBytes({ (rawBufferPointer) in
let bufferPointer = rawBufferPointer.bindMemory(to: UInt8.self)
if let address = bufferPointer.baseAddress{
// 把音频数据,传给解析器
// streamID, 指定解析器
let result = AudioFileStreamParseBytes(streamID, UInt32(count), address, [])
guard result == noErr else {
throw ParserError.failedToParseBytes(result)
}
}
})
}
2.3 音频信息解析先
func ParserPropertyChangeCallback(_ context: UnsafeMutableRawPointer, _ streamID: AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID, _ flags: UnsafeMutablePointer<AudioFileStreamPropertyFlags>) {
let parser = Unmanaged<Parser>.fromOpaque(context).takeUnretainedValue()
// 关心什么信息,取什么
switch propertyID {
case kAudioFileStreamProperty_DataFormat:
// 拿数据格式
var format = AudioStreamBasicDescription()
GetPropertyValue(&format, streamID, propertyID)
parser.dataFormat = AVAudioFormat(streamDescription: &format)
case kAudioFileStreamProperty_AudioDataPacketCount:
// 音频流文件,分离出来的音频数据中,的包 packet 个数
GetPropertyValue(&parser.packetCount, streamID, propertyID)
default:
()
}
}
// 套路就是,先拿内存大小 propSize, 再拿关心的属性的值 value
func GetPropertyValue<T>(_ value: inout T, _ streamID: AudioFileStreamID, _ propertyID: AudioFileStreamPropertyID) {
var propSize: UInt32 = 0
guard AudioFileStreamGetPropertyInfo(streamID, propertyID, &propSize, nil) == noErr else {
return
}
guard AudioFileStreamGetProperty(streamID, propertyID, &propSize, &value) == noErr else {
return
}
}
2.4 解析回调,处理数据
func ParserPacketCallback(_ context: UnsafeMutableRawPointer, _ byteCount: UInt32, _ packetCount: UInt32, _ data: UnsafeRawPointer, _ packetDescriptions: UnsafeMutablePointer<AudioStreamPacketDescription>) {
// 拿回了 self ( parser )
let parser = Unmanaged<Parser>.fromOpaque(context).takeUnretainedValue()
let packetDescriptionsOrNil: UnsafeMutablePointer<AudioStreamPacketDescription>? = packetDescriptions
// ASPD 存在,就是压缩的音频包
// 未压缩的 pcm, 使用 ASBD
let isCompressed = packetDescriptionsOrNil != nil
guard let dataFormat = parser.dataFormat else {
return
}
// 拿到了数据,遍历,
// 存储进去 parser.packets, 也就是 self.packets
if isCompressed {
for i in 0 ..< Int(packetCount) {
// 压缩音频数据,每一个包对应 ASPD, 逐个计算
let packetDescription = packetDescriptions[i]
let packetStart = Int(packetDescription.mStartOffset)
let packetSize = Int(packetDescription.mDataByteSize)
let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)
parser.packets.append((packetData, packetDescription))
}
} else {
// 原始音频数据 pcm,文件统一配置,计算比较简单
let format = dataFormat.streamDescription.pointee
let bytesPerPacket = Int(format.mBytesPerPacket)
for i in 0 ..< Int(packetCount) {
let packetStart = i * bytesPerPacket
let packetSize = bytesPerPacket
let packetData = Data(bytes: data.advanced(by: packetStart), count: packetSize)
parser.packets.append((packetData, nil))
}
}
}
3,音频包 packet >> 音频缓冲 buffer
public required init(parser: Parsing, readFormat: AVAudioFormat) throws {
// 从之前负责解析的,拿音频数据
self.parser = parser
guard let dataFormat = parser.dataFormat else {
throw ReaderError.parserMissingDataFormat
}
let sourceFormat = dataFormat.streamDescription
let commonFormat = readFormat.streamDescription
// 创建音频格式转换器 converter
// 通过指定输入格式,和输出格式
// 输入格式是上一步解析出来的,从 paser 里面拿
// 输出格式,开发指定的
let result = AudioConverterNew(sourceFormat, commonFormat, &converter)
guard result == noErr else {
throw ReaderError.unableToCreateConverter(result)
}
self.readFormat = readFormat
}
开发指定的输出格式
public var readFormat: AVAudioFormat {
return AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 44100, channels: 2, interleaved: false)!
}
// 位深,采用 Float32
// 采样率 44100 Hz, 标准 CD 音质
// 分左右声道
上一步解析出音频包 packet 后,进入读取音频缓冲 buffer 的阶段
public func read(_ frames: AVAudioFrameCount) throws -> AVAudioPCMBuffer {
let framesPerPacket = readFormat.streamDescription.pointee.mFramesPerPacket
var packets = frames / framesPerPacket
// 创建空白的、指定格式和容量的,音频缓冲 AVAudioPCMBuffer
guard let buffer = AVAudioPCMBuffer(pcmFormat: readFormat, frameCapacity: frames) else {
throw ReaderError.failedToCreatePCMBuffer
}
buffer.frameLength = frames
// 把解析出的音频包 packet, 转换成 AVAudioPCMBuffer,这样 AVAudioEngine 可以拿来播放
try queue.sync {
let context = unsafeBitCast(self, to: UnsafeMutableRawPointer.self)
// 通过设置好的转换器 converter,使用回调方法 ReaderConverterCallback,填充创建的 buffer 的数据 buffer.mutableAudioBufferList
let status = AudioConverterFillComplexBuffer(converter!, ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil)
guard status == noErr else {
switch status {
case ReaderMissingSourceFormatError:
throw ReaderError.parserMissingDataFormat
case ReaderReachedEndOfDataError:
throw ReaderError.reachedEndOfFile
case ReaderNotEnoughDataError:
throw ReaderError.notEnoughData
default:
throw ReaderError.converterFailed(status)
}
}
}
return buffer
}
- AudioConverterFillComplexBuffer 的使用姿势:
AudioConverterFillComplexBuffer(格式转换器,回调函数,自定义参数指针,包的个数指针,接收转换后数据的指针,接收 ASPD 的指针)
AudioConverterFillComplexBuffer(converter!, ReaderConverterCallback, context, &packets, buffer.mutableAudioBufferList, nil)
- AudioConverterFillComplexBuffer 的回调函数 ReaderConverterCallback, 的使用姿势:
回调函数(格式转换器, 包的个数指针,接收转换后数据的指针, 接收 ASPD 的指针, 自定义参数指针 )
可看出,传递给 AudioConverterFillComplexBuffer 的 6 个参数,
除了其回调参数本身,其他 5 个参数,其回调参数都有用到
转换 buffer 的回调函数,之前创建了空白的音频缓冲 buffer,现在往 buffer 里面,填入数据
func ReaderConverterCallback(_ converter: AudioConverterRef,
_ packetCount: UnsafeMutablePointer<UInt32>,
_ ioData: UnsafeMutablePointer<AudioBufferList>,
_ outPacketDescriptions: UnsafeMutablePointer<UnsafeMutablePointer<AudioStreamPacketDescription>?>?,
_ context: UnsafeMutableRawPointer?) -> OSStatus {
// 还原出 self ( reader )
let reader = Unmanaged<Reader>.fromOpaque(context!).takeUnretainedValue()
// 确保输入格式可用
guard let sourceFormat = reader.parser.dataFormat else {
return ReaderMissingSourceFormatError
}
// 这个类 Reader, 里面记录了一个播放到的位置 currentPacket,
// 播放相对位置,就是一个 offset
// 判断播放到包尾的情况
// 播放到包尾,根据下载解析情况,分两种情况
// 1, 下载解析完成,播放到了结尾
// 2, 下载没完成,解析好了的,都播放完了
// (仅此两种状况,因为解析的时间,远比不上下载的时间。下载完成 = 解析完成 )
let packetIndex = Int(reader.currentPacket)
let packets = reader.parser.packets
let isEndOfData = packetIndex >= packets.count - 1
if isEndOfData {
if reader.parser.isParsingComplete {
packetCount.pointee = 0
return ReaderReachedEndOfDataError
} else {
return ReaderNotEnoughDataError
}
}
// 之前的设置,一次只处理一个包 packet 的音频数据
let packet = packets[packetIndex]
var data = packet.0
let dataCount = data.count
ioData.pointee.mNumberBuffers = 1
// 音频数据拷贝过来:先分配内存,再拷贝地址的数据
ioData.pointee.mBuffers.mData = UnsafeMutableRawPointer.allocate(byteCount: dataCount, alignment: 0)
_ = data.withUnsafeMutableBytes { (rawMutableBufferPointer) in
let bufferPointer = rawMutableBufferPointer.bindMemory(to: UInt8.self)
if let address = bufferPointer.baseAddress{
memcpy((ioData.pointee.mBuffers.mData?.assumingMemoryBound(to: UInt8.self))!, address, dataCount)
}
}
ioData.pointee.mBuffers.mDataByteSize = UInt32(dataCount)
// 处理压缩文件 MP3, AAC 的 ASPD
let sourceFormatDescription = sourceFormat.streamDescription.pointee
if sourceFormatDescription.mFormatID != kAudioFormatLinearPCM {
if outPacketDescriptions?.pointee == nil {
outPacketDescriptions?.pointee = UnsafeMutablePointer<AudioStreamPacketDescription>.allocate(capacity: 1)
}
outPacketDescriptions?.pointee?.pointee.mDataByteSize = UInt32(dataCount)
outPacketDescriptions?.pointee?.pointee.mStartOffset = 0
outPacketDescriptions?.pointee?.pointee.mVariableFramesInPacket = 0
}
packetCount.pointee = 1
// 更新播放到的位置 currentPacket
reader.currentPacket = reader.currentPacket + 1
return noErr;
}
4, 使用 AVAudioEngine, 播放与实时音效处理
AVAudioEngine 可以做实时的音效处理,用 Effect Unit 加效果
4.1 播放先
设置 AudioEngine,添加节点,连接节点
func setupAudioEngine(){
// 添加节点
attachNodes()
// 连接节点
connectNodes()
// 准备 AudioEngine
engine.prepare()
// AVAudioEngine 的数据流,采用推 push 模型
// 使用计时器,每隔 0.1 秒左右,调度播放资源
let interval = 1 / (readFormat.sampleRate / Double(readBufferSize))
let timer = Timer(timeInterval: interval / 2, repeats: true) {
[weak self] _ in
guard self?.state != .stopped else {
return
}
// 分配缓冲 buffer, 调度播放资源
self?.scheduleNextBuffer()
self?.handleTimeUpdate()
self?.notifyTimeUpdated()
}
RunLoop.current.add(timer, forMode: .common)
}
// 添加播放节点
open func attachNodes() {
engine.attach(playerNode)
}
// 播放节点,连通到输出
open func connectNodes() {
engine.connect(playerNode, to: engine.mainMixerNode, format: readFormat)
}
调度播放资源,将数据 ( 上一步创建的音频缓冲 buffer )交给 AudioEngine 的播放节点 playerNode
func scheduleNextBuffer(){
guard let reader = reader else {
return
}
// 通过状态记录,管理播放
// 播放状态,就是一个开关
guard !isFileSchedulingComplete || repeats else {
return
}
do {
// 拿到,上一步创建音频缓冲 buffer
let nextScheduledBuffer = try reader.read(readBufferSize)
// playerNode 播放消费掉
playerNode.scheduleBuffer(nextScheduledBuffer)
} catch ReaderError.reachedEndOfFile {
isFileSchedulingComplete = true
} catch { }
}
开启播放
public func play() {
// 没播放,才开启
guard !playerNode.isPlaying else {
return
}
if !engine.isRunning {
do {
try engine.start()
} catch { }
}
// 提升用户体验,播放前,先静音
let lastVolume = volumeRampTargetValue ?? volume
volume = 0
// 播放节点播放
playerNode.play()
// 250 毫秒后,正常音量播放
swellVolume(to: lastVolume)
// 更新播放状态
state = .playing
}
4.2 音效后
添加实时的音高、播放速度效果
// 使用 AVAudioUnitTimePitch 单元,调节播放速度和音高效果
let timePitchNode = AVAudioUnitTimePitch()
override func attachNodes() {
// 添加播放节点
super.attachNodes()
// 添加音效节点
engine.attach(timePitchNode)
}
// 相当于在播放节点和输出节点中间,插入音效节点
override func connectNodes() {
engine.connect(playerNode, to: timePitchNode, format: readFormat)
engine.connect(timePitchNode, to: engine.mainMixerNode, format: readFormat)
}
补充细节
5,计算出歌曲的时长, duration
先拿到包的个数,
下载的数据,解析完成后,加出来的
1 首 2:34 秒的 mp3, 可分为 5925 个包
public var totalPacketCount: AVAudioPacketCount? {
guard let _ = dataFormat else {
return nil
}
// 本例子,走的是 AVAudioPacketCount(packets.count)
// 2.4 的解析回调 ParserPacketCallback 中,
// 拿到步骤 1 下载的数据后,就解析,添加数据到 packets
return max(AVAudioPacketCount(packetCount), AVAudioPacketCount(packets.count))
}
去拿音频帧 frame 的总数
public var totalFrameCount: AVAudioFrameCount? {
guard let framesPerPacket = dataFormat?.streamDescription.pointee.mFramesPerPacket else {
return nil
}
guard let totalPacketCount = totalPacketCount else {
return nil
}
// 上一步包的总数 X 每个包里有几个帧
return AVAudioFrameCount(totalPacketCount) * AVAudioFrameCount(framesPerPacket)
}
计算出音频持续时间
public var duration: TimeInterval? {
guard let sampleRate = dataFormat?.sampleRate else {
return nil
}
guard let totalFrameCount = totalFrameCount else {
return nil
}
// 上一步的音频帧 frame 的总数 / 采样率
return TimeInterval(totalFrameCount) / TimeInterval(sampleRate)
}
6,调节播放的当前位置
6.1 音频管理者 streamer 里面
public func seek(to time: TimeInterval) throws {
// 有了 parser 的音频包,和 reader 的音频缓冲,才可播放
guard let parser = parser, let reader = reader else {
return
}
// 拿时间,先算出音频帧的相对位置
// 拿音频帧的相对位置,算出音频包的相对位置
guard let frameOffset = parser.frameOffset(forTime: time),
let packetOffset = parser.packetOffset(forFrame: frameOffset) else {
return
}
// 更新当前状态
currentTimeOffset = time
isFileSchedulingComplete = false
// 记录当前状态,一会恢复
let isPlaying = playerNode.isPlaying
let lastVolume = volumeRampTargetValue ?? volume
// 优化体验,避免杂声,播放先停下来
playerNode.stop()
volume = 0
// 更新 reader 里面的播放资源位置
do {
try reader.seek(packetOffset)
} catch {
return
}
// 刚才记录当前状态,恢复
if isPlaying {
playerNode.play()
}
// 更新 UI
delegate?.streamer(self, updatedCurrentTime: time)
// 恢复原来的音量
swellVolume(to: lastVolume)
}
算出当前时间的,帧偏移
public func frameOffset(forTime time: TimeInterval) -> AVAudioFramePosition? {
guard let _ = dataFormat?.streamDescription.pointee,
let frameCount = totalFrameCount,
let duration = duration else {
return nil
}
// 拿当前时间 / 音频总时长,算出比值
let ratio = time / duration
return AVAudioFramePosition(Double(frameCount) * ratio)
}
算出当前帧,对应的包的位置
public func packetOffset(forFrame frame: AVAudioFramePosition) -> AVAudioPacketCount? {
guard let framesPerPacket = dataFormat?.streamDescription.pointee.mFramesPerPacket else {
return nil
}
// 当前是第多少帧 / 一个包里面有几个帧
return AVAudioPacketCount(frame) / AVAudioPacketCount(framesPerPacket)
}
6.2 音频资源调度 reader 里面
public func seek(_ packet: AVAudioPacketCount) throws {
queue.sync {
// 更改位置偏移
currentPacket = packet
}
}
记录的位置 currentPacket,这样起作用
步骤三的回调 ReaderConverterCallback,
// ...
// 本例子中,一个音频包 packet, 对应一个音频缓冲 buffer
let packet = packets[packetIndex]
var data = packet.0
// ...
_ = data.withUnsafeMutableBytes { (rawMutableBufferPointer) in // ...
}
// ...
7 UI 用户体验提升,手动拖拽播放时刻的场景
分三个事件处理:手指按下,手指拖动,手指抬起
// 手指按下, 屏蔽刷新播放进度的代理方法
@IBAction func progressSliderTouchedDown(_ sender: UISlider) {
isSeeking = true
}
// 手指拖动, 屏蔽了刷新播放进度的代理方法,采用手势对应的 UI
@IBAction func progressSliderValueChanged(_ sender: UISlider) {
let currentTime = TimeInterval(progressSlider.value)
currentTimeLabel.text = currentTime.toMMSS()
}
// 手指抬起, 恢复刷新播放进度的代理方法,这个时候才调度播放的资源
@IBAction func progressSliderTouchedUp(_ sender: UISlider) {
seek(sender)
isSeeking = false
}
相关代理方法,根据播放进度,更新当前事件和进度条的 UI
func streamer(_ streamer: Streaming, updatedCurrentTime currentTime: TimeInterval) {
if !isSeeking {
progressSlider.value = Float(currentTime)
currentTimeLabel.text = currentTime.toMMSS()
}
}
8 单曲循环模式
步骤 4 播放中,分发播放资源,是走计时器的
管理下里面的两个方法的逻辑,就好
( 调度音频缓冲,和播放完了改状态 )
let timer = Timer(timeInterval: interval / 2, repeats: true) {
[weak self] _ in
// ...
self?.scheduleNextBuffer()
self?.handleTimeUpdate()
// ...
}
调度音频缓冲 buffer,
func scheduleNextBuffer(){
guard let reader = reader else {
return
}
// 如果重复 repeats,就继续播放,不用管播放完了一遍没有
guard !isFileSchedulingComplete || repeats else {
return
}
// ... 下面是,播放节点播放资源
}
根据播放情况,处理相关状态
func handleTimeUpdate(){
guard let currentTime = currentTime, let duration = duration else {
return
}
// 当前播放的时间,过了音频时长,就认为播放完了一遍,去暂停
if currentTime >= duration {
try? seek(to: 0)
// 如果重复,别暂停
if !repeats{
pause()
}
}
}
代码见 github
实例分析:RxSwift 速成的三个原则