iOS音频学习三之AudioQueue

上两篇我介绍了如何用AudioFile和AudioFileStream解析音频格式信息,分离音频帧,我们终于来到了播放环节AudioQueue

AudioQueue

AudioQueue也是AudioToolBox.framework中的一员,官方文档这么描述
Audio Queue Services provides a straightforward, low overhead way to record and play audio in iOS and Mac OS X. It is the recommended technology to use for adding basic recording or playback features to your iOS or Mac OS X application.
意思是AudioQueue是系统推荐实现播放和录音的工具,简单和开销较小
支持PCM数据,iOS/MacOSX平台支持的压缩格式(MP3、AAC等),或者其他解码器生成的PCM数据

AudioQueue的原理
AudioQueue顾名思义就是音频队列,是因为在里面有一套缓冲队列(Buffer Queue)的机制。在AudioQueue启动之后需要通过AudioQueueAllocateBuffer生成若干个AudioQueueBufferRef结构,这些Buffer将用来存储即将要播放的音频数据,并且这些Buffer是受生成它们的AudioQueue实例管理的,内存空间也已经分配好,当AudioQueue被Dispose时这些Buffer也会随之被销毁。
当有音频数据需要被播放时首先需要被memcpy到AudioQueueBufferRef的mAudioData中(mAudioData所指向的内存已经被分配,之前AudioQueueAllocateBuffer所做的工作),并给mAudioDataByteSize字段赋值传入的数据大小。完成之后需要调用AudioQueueEnqueueBuffer把存有音频数据的Buffer插入到AudioQueue内置的Buffer队列中。在Buffer队列中有buffer存在的情况下调用AudioQueueStart,AudioQueue就按照Enqueue的顺序逐个使用Buffer队列中的buffer进行播放,每当一个Buffer使用完毕之后就会从Buffer队列中被移除并且使用者指定的Runloop上出发一个回调来告诉使用者,某个AudioQueueBufferRef对象已经使用完成,你可以继续重用这个对象来存储后面的音频数据,实现复用。

过程如图所示


iOS音频学习三之AudioQueue_第1张图片
AudioQueue.png

由图来看我们可以总结一下工作原理

  • AudioQueue先创建数量一般为3的buffer,并往里面装填音频数据(即我们前面说过的PCM数据或者音频帧),当有一个buffer装填好数据,则会被放入BufferQueue队列中
  • 当BufferQueue队列存在buffer时,我们调用AudioQueue开始播放,AudioQueue会使用BufferQueue中第一个buffer开始播放
  • 当第一个buffer播放完毕之后,AudioQueue将会返回之前播放的buffer提供重复使用并播放下一个buffer,并调用回调函数开始往空的buffer里面装填音频数据,当装填好之后继续插入BufferQueue之中,循环播放每个buffer

可以看到,AudioQueue其实就是一个生产者消费者问题。生产者是AudioFile或者AudioFileStream,生产音频数据,放入BufferQueue进行消费;AudioQueue作为消费者,从BufferQueue取出buffer进行消费。所以我们也会涉及到多线程同步、信号量使用和死锁的避免

接下来我们来创建AudioQueue

extern OSStatus             
AudioQueueNewOutput(                const AudioStreamBasicDescription *inFormat,
                                    AudioQueueOutputCallback        inCallbackProc,
                                    void * __nullable               inUserData,
                                    CFRunLoopRef __nullable         inCallbackRunLoop,
                                    CFStringRef __nullable          inCallbackRunLoopMode,
                                    UInt32                          inFlags,
                                    AudioQueueRef __nullable * __nonnull outAQ)          __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
extern OSStatus             
AudioQueueNewOutputWithDispatchQueue(AudioQueueRef __nullable * __nonnull outAQ,
                                    const AudioStreamBasicDescription *inFormat,
                                    UInt32                          inFlags,
                                    dispatch_queue_t                inCallbackDispatchQueue,
                                    AudioQueueOutputCallbackBlock   inCallbackBlock)
                                        API_AVAILABLE(macos(10.6), ios(10.0), watchos(3.0), tvos(10.0));

我们看第一个方法
第一个参数,表示需要播放的音频数据格式类型,是一个AudioStreamBasicDescription对象,是AudioFileStream或者AudioFile解析出来的数据格式信息;
第二个参数,AudioQueueOutputCallback是buffer被使用之后的回调
第三个参数,上下文对象
第四个参数,inCallbackRunLoop为AudioQueueOutputCallback需要在哪个Runloop上被回调,如果传入NULL的话就会在AudioQueue的内部Runloop中被回调,所以一般传NULL
第五个参数,inCallbackRunLoopMode为Runloop模式,如果传入NULL就相当于kCFRunLoopCommonModes,所以传NULL也就好了
第六个参数,ioFlags是保留字段,目前没用,传0
第七个参数,返回生成的AudioQueue实例
返回值用来判断是否成功创建了
第二个方法就是把Runloop换成了dispatch_queue,其余一样

Buffer相关的方法

1.创建buffer

extern OSStatus
AudioQueueAllocateBuffer(           AudioQueueRef           inAQ,
                                    UInt32                  inBufferByteSize,
                                    AudioQueueBufferRef __nullable * __nonnull outBuffer)              __OSX_AVAILABLE_STARTING(__MAC_10_5,__IPHONE_2_0);
extern OSStatus
AudioQueueAllocateBufferWithPacketDescriptions(
                                    AudioQueueRef           inAQ,
                                    UInt32                  inBufferByteSize,
                                    UInt32                  inNumberPacketDescriptions,
                                    AudioQueueBufferRef __nullable * __nonnull outBuffer)              __OSX_AVAILABLE_STARTING(__MAC_10_6,__IPHONE_2_0);

第一个方法传入AudioQueue实例和Buffer大小,传出Buffer实例
第二个方法可以指定生成的Buffer中PacketDescriptions的个数

2.销毁buffer

OSStatus AudioQueueFreeBuffer(AudioQueueRef inAQ,AudioQueueBufferRef inBuffer);

这个方法一般只在销毁某个特定buffer时才会用到,且这个方法只能在AudioQueue不处理数据时才能使用。

3.插入buffer

OSStatus AudioQueueEnqueueBuffer(AudioQueueRef inAQ,
                                 AudioQueueBufferRef inBuffer,
                                 UInt32 inNumPacketDescs,
                                 const AudioStreamPacketDescription * inPacketDescs);

第一个参数,传入AudioQueue实例
第二个参数,传入要插入的buffer
第三个参数,插入的buffer中packet的数量
第四个参数,插入的buffer中packet数组
后面两个参数根据需要时插入,一般是在播放VBR的时候使用,但是我们之前即使是CBR数据AudioFileStream或者AudioFile也会给出PacketDescription,总而言之就是有就传入,没有就给NULL

接下来终于到了播放环节了

1.开始播放

OSStatus AudioQueueStart(AudioQueueRef inAQ,const AudioTimeStamp * inStartTime);

第二个参数是用来控制播放时间的,一般开始的时候传NULL即可

2.解码数据

OSStatus AudioQueuePrime(AudioQueueRef inAQ,
                          UInt32 inNumberOfFramesToPrepare,
                          UInt32 * outNumberOfFramesPrepared);     

这个比较少用,因为直接调用AudioQueueStart会自动开始解码。参数用来指定需要解码帧数和实际完成解码的帧数;

3.暂停播放

OSStatus AudioQueuePause(AudioQueueRef inAQ);

方法一旦调用后播放会立即暂停,AudioQueueOutputCallback也会立即暂停,这是就要关心线程的调度防止线程进入无线等待

4.停止播放

OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);

第二个参数传入true的话会立即停止播放(同步),如果传入false的话AudioQueue会播放完已经Enqueue的所有buffer再停止(异步)。

5.flush

OSStatus
AudioQueueFlush(AudioQueueRef inAQ);

调用后会播放完Enqueue的所有buffer后重置解码器状态,以防止当前的解码器状态影响到下一段音频的解码(比如切歌的时候)。如果和AudioQueueStop(AQ,false)一起使用并不会奇效,因为Stop方法的false参数也会做同样的事情

6.重置

OSStatus AudioQueueReset(AudioQueueRef inAQ);

重置AudioQueue会清楚已经Enqueue的buffer并触发AudioQueueOutputCallback,调用AudioQueueStop也会触发该方法。这个方法一般在seek中使用,用来清除残余的buffer

7.获取播放时间

OSStatus AudioQueueGetCurrentTime(AudioQueueRef inAQ,
                                  AudioQueueTimelineRef inTimeline,
                                  AudioTimeStamp * outTimeStamp,
                                  Boolean * outTimelineDiscontinuity);

传入的参数中,第一第四个参数是和AudioQueueTimeline相关的,我们这里并没有用到,传入NULL。调用后返回AudioTimeStamp,从这个时间戳我们可以得出播放时间

AudioTimeStamp time = ...; //AudioQueueGetCurrentTime方法获取
NSTimeInterval playedTime = time.mSampleTime / _format.mSampleRate;

这里有一点需要注意的是
1、第一个需要注意的是这个播放时间是指实际播放的时间。举个,开始播放8s后,用户操作slider把播放进度seek到了20s后播放了3s,我们认为的播放时间应该是23s,可是GetCurrentTime方法中获得的时间是11s,即实际播放时间。所以每次seek时都必须保存seek的timingOffset:

AudioTimeStamp time = ...; //AudioQueueGetCurrentTime方法获取
NSTimeInterval playedTime = time.mSampleTime / _format.mSampleRate; //seek时的播放时间

NSTimeInterval seekTime = ...; //需要seek到哪个时间
NSTimeInterval timingOffset = seekTime - playedTime;

seek之后的播放进度需要根据timingOffset和playedTime计算

NSTimeInterval progress = timingOffset + playedTime;

2.第二个需要注意的是GetCurrentTime方法有时候会失败,所以上次获取的播放时间最好保存起来,如果遇到调用失败,就返回上次保存的结果。

销毁AudioQueue
AudioQueueDispose(AudioQueueRef inAQ,  Boolean inImmediate);

销毁的同时会清除所有的buffer,第二个参数的意义和用法和AudioQueueStop方法相同。
需要注意的一点是当AudioQueueStart调用之后AudioQueue其实还没有真正开始,期间会有一个短暂的间隙。如果在AudioQueueStart调用后到AudioQueue真正开始运作前的这段时间内调用AudioQueueDispose方法的话会导致卡死。
要规避这种问题的一种方法是做好线程的调度,保证Dispose方法调用一定是在每一个播放Runloop之后(即至少是一个buffer被成功播放之后)。另外一种是监听kAudioQueueProperty_IsRunning属性,这个属性在AudioQueue真正运作起来之后会变成1,停止后会变成0,所以保证Start方法调用Dispose后一定在IsRunning为1是才能被调用。

到现在,一个基本音频播放器就成型了。下一篇章我会尝试着用AudioFileStream和AudioQueue实现一个本地文件播放器。

你可能感兴趣的:(iOS音频学习三之AudioQueue)