AudioQueue学习笔记和实战

AudioQueue理论学习

AudioQueue是iOS提供的又一套实现音频播放和录制的框架,怎么说呢,使用起来其实也不比AduioUnit方便很多,特别是在buffer的管理上,新手理解起来还是有点费劲的,在被她折磨了一周后,现在终于把她征服了,这里做一下总结。

首先看一下官方给的AudioQueue工作流程图

这里给出的是播放本地路径下的音频文件,流程总结如下:

  1. 读取音频文件,在音频文件的回调中给buffers填充数据
  2. 将填充满的buffers给AudioQueue播放
  3. AudioQueue播放完一个Buffer后,把这个buffer还给AudioQueue的回调继续填充
  4. 循环2和3直到音频播放完

主要的api:

AudioQueueNewOutput
1
OSStatus AudioQueueNewOutput(const AudioStreamBasicDescription *inFormat, AudioQueueOutputCallback inCallbackProc, void *inUserData, CFRunLoopRef inCallbackRunLoop, CFStringRef inCallbackRunLoopMode, UInt32 inFlags, AudioQueueRef _Nullable *outAQ);

该方法用于创建一个用于输出音频的AudioQueue

參数及返回说明例如以下:

  1. inFormat:该參数指明了即将播放的音频的数据格式
  2. inCallbackProc:该回调用于当AudioQueue已使用完一个缓冲区时通知用户,用户能够继续填充音频数据
  3. inUserData:由用户传入的数据指针,用于传递给回调函数
  4. inCallbackRunLoop:指明回调事件发生在哪个RunLoop之中,假设传递NULL,表示在AudioQueue所在的线程上运行该回调事件,普通情况下,传递NULL就可以。
  5. inCallbackRunLoopMode:指明回调事件发生的RunLoop的模式,传递NULL相当于kCFRunLoopCommonModes,通常情况下传递NULL就可以
  6. outAQ:该AudioQueue的引用实例,
AudioQueueOutput_Callback
1
void AudioQueueOutput_Callback(void *inClientData,AudioQueueRef inAQ,AudioQueueBufferRef inBuffer)

这个是AudioQueue的回调函数,会将已经播放完的buffer还回来。

AudioQueueAllocateBuffer
1
OSStatus AudioQueueAllocateBuffer(AudioQueueRef inAQ, UInt32 inBufferByteSize, AudioQueueBufferRef _Nullable *outBuffer);

该方法的作用是为存放音频数据的缓冲区开辟空间

參数及返回说明例如以下:

  1. inAQ:AudioQueue的引用实例
  2. inBufferByteSize:须要开辟的缓冲区的大小
  3. outBuffer:开辟的缓冲区的引用实例
AudioQueueEnqueueBuffer
1
OSStatus AudioQueueEnqueueBuffer(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, UInt32 inNumPacketDescs, const AudioStreamPacketDescription *inPacketDescs);

该方法用于将已经填充数据的AudioQueueBuffer入队到AudioQueue

參数及返回说明例如以下:

  1. inAQ:AudioQueue的引用实例
  2. inBuffer:须要入队的缓冲区实例
  3. inNumPacketDescs:缓冲区中共存在有多少帧音频数据
  4. inPacketDescs:缓冲区中每一帧的相关信息。用户须要指明当中每一帧在缓冲区中数据的偏移值,通过字段mStartOffset来指定
控制相关
1
2
3
4
5
6
OSStatus AudioQueueStart(AudioQueueRef inAQ, const AudioTimeStamp *inStartTime);
OSStatus AudioQueuePause(AudioQueueRef inAQ);
OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);
OSStatus AudioQueueFlush(AudioQueueRef inAQ);
OSStatus AudioQueueReset(AudioQueueRef inAQ);
OSStatus AudioQueueDispose(AudioQueueRef inAQ, Boolean inImmediate);

AudioFileStream

数据的相关内容都和它相关,所以还是很重要的,其实AudioQueue使用起来比较简单,复杂的部分都在这个数据的处理上了。。。

AudioFileStreamOpen
1
2
3
4
5
6
7
8
9
10
11
12
13
// AudioFileStreamOpen的参数说明如下:
// 1. inClientData:用户指定的数据,用于传递给回调函数,这里我们指定(__bridge LocalAudioPlayer*)self
// 2. inPropertyListenerProc:当解析到一个音频信息时,将回调该方法
// 3. inPacketsProc:当解析到一个音频帧时,将回调该方法
// 4. inFileTypeHint:指明音频数据的格式,如果你不知道音频数据的格式,可以传0
// 5. outAudioFileStream:AudioFileStreamID实例,需保存供后续使用
AudioFileStreamOpen ( void * __nullable inClientData,
AudioFileStream_PropertyListenerProc inPropertyListenerProc,
AudioFileStream_PacketsProc inPacketsProc,
AudioFileTypeID inFileTypeHint,
AudioFileStreamID __nullable * __nonnull outAudioFileStream)

这个函数会创建一个AudioFileStreamID,之后所有的操作都是基于这个ID来的,然后还是创建2个回调 inPropertyListenerProc 和 inPacketsProc,这2个回调函数比较重要,下面详说。

AudioFileStreamParseBytes
1
2
3
4
5
6
7
8
9
10
11
12
// 参数的说明如下:
// 1. inAudioFileStream:AudioFileStreamID实例,由AudioFileStreamOpen打开
// 2. inDataByteSize:此次解析的数据字节大小
// 3. inData:此次解析的数据大小
// 4. inFlags:数据解析标志,其中只有一个值kAudioFileStreamParseFlag_Discontinuity = 1,表示解析的数据是否是不连续的,目前我们可以传0。
AudioFileStreamParseBytes(
AudioFileStreamID inAudioFileStream,
UInt32 inDataByteSize,
const void * __nullable inData,
AudioFileStreamParseFlags inFlags)

只有对数据进行了解析,才会进到上面的2个回调函数里面。

AudioFileStreamPropertyListenerProc
1
2
3
4
void AudioFileStreamPropertyListenerProc(void *inClientData,
AudioFileStreamID inAudioFileStream,
AudioFileStreamPropertyID inPropertyID,
UInt32 *ioFlags)

在这个回调中,你可以拿到你想要的音频相关信息,比如音频结构(AudioStreamBasicDescription),码率(BitRate),MagicCookie等等,通过这些信息,你还可以计算其他数据,比如音频总时长。

这里分享下音频时长的2种计算方式:

  • 总时长 = 总帧数*单帧的时长

    单帧的时长 = 单帧的采样个数*每帧的时长

    每帧的时长 = 1/采样率

采样率:单位时间内的采样个数

  • 总时长 = 文件总的字节数/码率

码率:单位时间内的文件字节数

AudioFileStreamPacketsProc
1
2
3
4
5
void AudioFileStreamPacketsProc(void *inClientData,
UInt32 inNumberBytes,
UInt32 inNumberPackets,
const void *inInputData,
AudioStreamPacketDescription *inPacketDescriptions)

在这个回调中,你能够拿到每一个packet的数据,然后数据的填充都在这里完成。

实战

这里只讲几个比较重要的细节,其他的可以参考demo中的代码。

  1. AudioQueueNewOutput在创建的时候有2个runloop相关的参数,这里直接传NULL就行,不要取当前的runloop和model
  2. AudioQueueOutput_Callback里面在标记可使用的buffer时要加锁,不然音频无法正常播放
  3. 记得设置AVAudioSession的category
  4. 读取音频数据时使用while循环,比使用计时器优雅
  5. kAudioFileStreamProperty_DataFormat这个属性是必须要获取到的,在创建AudioQueue的时候需要传入
  6. 填装数据的时候要判断对当前buffer的可用填装空间,如果装不下了就别再装啦。。。
  7. AudioQueueEnqueueBuffer给AudioQueue塞完数据后,需要判断下一个buffer是否可用,不可用的话得一直等着,知道可用为止。

demo

参考文档