上兩篇我介紹了如何用AudioFile和AudioFileStream解析音頻格式信息凉蜂,分離音頻幀膨更,我們終于來(lái)到了播放環(huán)節(jié)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是系統(tǒng)推薦實(shí)現(xiàn)播放和錄音的工具幢尚,簡(jiǎn)單和開銷較小
支持PCM數(shù)據(jù)茫负,iOS/MacOSX平臺(tái)支持的壓縮格式(MP3蕉鸳、AAC等),或者其他解碼器生成的PCM數(shù)據(jù)
AudioQueue的原理
AudioQueue顧名思義就是音頻隊(duì)列,是因?yàn)樵诶锩嬗幸惶拙彌_隊(duì)列(Buffer Queue)的機(jī)制潮尝。在AudioQueue啟動(dòng)之后需要通過(guò)AudioQueueAllocateBuffer
生成若干個(gè)AudioQueueBufferRef
結(jié)構(gòu)榕吼,這些Buffer將用來(lái)存儲(chǔ)即將要播放的音頻數(shù)據(jù),并且這些Buffer是受生成它們的AudioQueue實(shí)例管理的勉失,內(nèi)存空間也已經(jīng)分配好羹蚣,當(dāng)AudioQueue被Dispose時(shí)這些Buffer也會(huì)隨之被銷毀。
當(dāng)有音頻數(shù)據(jù)需要被播放時(shí)首先需要被memcpy到AudioQueueBufferRef的mAudioData中(mAudioData所指向的內(nèi)存已經(jīng)被分配乱凿,之前AudioQueueAllocateBuffer所做的工作)顽素,并給mAudioDataByteSize字段賦值傳入的數(shù)據(jù)大小。完成之后需要調(diào)用AudioQueueEnqueueBuffer
把存有音頻數(shù)據(jù)的Buffer插入到AudioQueue內(nèi)置的Buffer隊(duì)列中徒蟆。在Buffer隊(duì)列中有buffer存在的情況下調(diào)用AudioQueueStart
胁出,AudioQueue就按照Enqueue的順序逐個(gè)使用Buffer隊(duì)列中的buffer進(jìn)行播放,每當(dāng)一個(gè)Buffer使用完畢之后就會(huì)從Buffer隊(duì)列中被移除并且使用者指定的Runloop上出發(fā)一個(gè)回調(diào)來(lái)告訴使用者段审,某個(gè)AudioQueueBufferRef對(duì)象已經(jīng)使用完成全蝶,你可以繼續(xù)重用這個(gè)對(duì)象來(lái)存儲(chǔ)后面的音頻數(shù)據(jù),實(shí)現(xiàn)復(fù)用寺枉。
過(guò)程如圖所示
由圖來(lái)看我們可以總結(jié)一下工作原理
- AudioQueue先創(chuàng)建數(shù)量一般為3的buffer裸诽,并往里面裝填音頻數(shù)據(jù)(即我們前面說(shuō)過(guò)的PCM數(shù)據(jù)或者音頻幀),當(dāng)有一個(gè)buffer裝填好數(shù)據(jù)型凳,則會(huì)被放入BufferQueue隊(duì)列中
- 當(dāng)BufferQueue隊(duì)列存在buffer時(shí),我們調(diào)用AudioQueue開始播放嘱函,AudioQueue會(huì)使用BufferQueue中第一個(gè)buffer開始播放
- 當(dāng)?shù)谝粋€(gè)buffer播放完畢之后甘畅,AudioQueue將會(huì)返回之前播放的buffer提供重復(fù)使用并播放下一個(gè)buffer,并調(diào)用回調(diào)函數(shù)開始往空的buffer里面裝填音頻數(shù)據(jù)往弓,當(dāng)裝填好之后繼續(xù)插入BufferQueue之中疏唾,循環(huán)播放每個(gè)buffer
可以看到,AudioQueue其實(shí)就是一個(gè)生產(chǎn)者消費(fèi)者問(wèn)題函似。生產(chǎn)者是AudioFile或者AudioFileStream槐脏,生產(chǎn)音頻數(shù)據(jù),放入BufferQueue進(jìn)行消費(fèi)撇寞;AudioQueue作為消費(fèi)者顿天,從BufferQueue取出buffer進(jìn)行消費(fèi)。所以我們也會(huì)涉及到多線程同步蔑担、信號(hào)量使用和死鎖的避免
接下來(lái)我們來(lái)創(chuàng)建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));
我們看第一個(gè)方法
第一個(gè)參數(shù)牌废,表示需要播放的音頻數(shù)據(jù)格式類型,是一個(gè)AudioStreamBasicDescription對(duì)象啤握,是AudioFileStream或者AudioFile解析出來(lái)的數(shù)據(jù)格式信息鸟缕;
第二個(gè)參數(shù),AudioQueueOutputCallback是buffer被使用之后的回調(diào)
第三個(gè)參數(shù),上下文對(duì)象
第四個(gè)參數(shù)懂从,inCallbackRunLoop為AudioQueueOutputCallback需要在哪個(gè)Runloop上被回調(diào)授段,如果傳入NULL的話就會(huì)在AudioQueue的內(nèi)部Runloop中被回調(diào),所以一般傳NULL
第五個(gè)參數(shù)番甩,inCallbackRunLoopMode為Runloop模式侵贵,如果傳入NULL就相當(dāng)于kCFRunLoopCommonModes,所以傳NULL也就好了
第六個(gè)參數(shù)对室,ioFlags是保留字段模燥,目前沒(méi)用,傳0
第七個(gè)參數(shù)掩宜,返回生成的AudioQueue實(shí)例
返回值用來(lái)判斷是否成功創(chuàng)建了
第二個(gè)方法就是把Runloop換成了dispatch_queue,其余一樣
Buffer相關(guān)的方法
1.創(chuàng)建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);
第一個(gè)方法傳入AudioQueue實(shí)例和Buffer大小蔫骂,傳出Buffer實(shí)例
第二個(gè)方法可以指定生成的Buffer中PacketDescriptions的個(gè)數(shù)
2.銷毀buffer
OSStatus AudioQueueFreeBuffer(AudioQueueRef inAQ,AudioQueueBufferRef inBuffer);
這個(gè)方法一般只在銷毀某個(gè)特定buffer時(shí)才會(huì)用到,且這個(gè)方法只能在AudioQueue不處理數(shù)據(jù)時(shí)才能使用牺汤。
3.插入buffer
OSStatus AudioQueueEnqueueBuffer(AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
UInt32 inNumPacketDescs,
const AudioStreamPacketDescription * inPacketDescs);
第一個(gè)參數(shù)辽旋,傳入AudioQueue實(shí)例
第二個(gè)參數(shù),傳入要插入的buffer
第三個(gè)參數(shù)檐迟,插入的buffer中packet的數(shù)量
第四個(gè)參數(shù)补胚,插入的buffer中packet數(shù)組
后面兩個(gè)參數(shù)根據(jù)需要時(shí)插入,一般是在播放VBR的時(shí)候使用追迟,但是我們之前即使是CBR數(shù)據(jù)AudioFileStream或者AudioFile也會(huì)給出PacketDescription溶其,總而言之就是有就傳入,沒(méi)有就給NULL
接下來(lái)終于到了播放環(huán)節(jié)了
1.開始播放
OSStatus AudioQueueStart(AudioQueueRef inAQ,const AudioTimeStamp * inStartTime);
第二個(gè)參數(shù)是用來(lái)控制播放時(shí)間的敦间,一般開始的時(shí)候傳NULL即可
2.解碼數(shù)據(jù)
OSStatus AudioQueuePrime(AudioQueueRef inAQ,
UInt32 inNumberOfFramesToPrepare,
UInt32 * outNumberOfFramesPrepared);
這個(gè)比較少用瓶逃,因?yàn)橹苯诱{(diào)用AudioQueueStart會(huì)自動(dòng)開始解碼。參數(shù)用來(lái)指定需要解碼幀數(shù)和實(shí)際完成解碼的幀數(shù)廓块;
3.暫停播放
OSStatus AudioQueuePause(AudioQueueRef inAQ);
方法一旦調(diào)用后播放會(huì)立即暫停厢绝,AudioQueueOutputCallback
也會(huì)立即暫停,這是就要關(guān)心線程的調(diào)度防止線程進(jìn)入無(wú)線等待
4.停止播放
OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);
第二個(gè)參數(shù)傳入true的話會(huì)立即停止播放(同步)带猴,如果傳入false的話AudioQueue會(huì)播放完已經(jīng)Enqueue的所有buffer再停止(異步)昔汉。
5.flush
OSStatus
AudioQueueFlush(AudioQueueRef inAQ);
調(diào)用后會(huì)播放完Enqueue的所有buffer后重置解碼器狀態(tài),以防止當(dāng)前的解碼器狀態(tài)影響到下一段音頻的解碼(比如切歌的時(shí)候)拴清。如果和AudioQueueStop(AQ,false)
一起使用并不會(huì)奇效靶病,因?yàn)镾top方法的false參數(shù)也會(huì)做同樣的事情
6.重置
OSStatus AudioQueueReset(AudioQueueRef inAQ);
重置AudioQueue會(huì)清楚已經(jīng)Enqueue的buffer并觸發(fā)AudioQueueOutputCallback,調(diào)用AudioQueueStop也會(huì)觸發(fā)該方法贷掖。這個(gè)方法一般在seek中使用嫡秕,用來(lái)清除殘余的buffer
7.獲取播放時(shí)間
OSStatus AudioQueueGetCurrentTime(AudioQueueRef inAQ,
AudioQueueTimelineRef inTimeline,
AudioTimeStamp * outTimeStamp,
Boolean * outTimelineDiscontinuity);
傳入的參數(shù)中,第一第四個(gè)參數(shù)是和AudioQueueTimeline相關(guān)的苹威,我們這里并沒(méi)有用到昆咽,傳入NULL。調(diào)用后返回AudioTimeStamp,從這個(gè)時(shí)間戳我們可以得出播放時(shí)間
AudioTimeStamp time = ...; //AudioQueueGetCurrentTime方法獲取
NSTimeInterval playedTime = time.mSampleTime / _format.mSampleRate;
這里有一點(diǎn)需要注意的是
1掷酗、第一個(gè)需要注意的是這個(gè)播放時(shí)間是指實(shí)際播放的時(shí)間调违。舉個(gè)??,開始播放8s后泻轰,用戶操作slider把播放進(jìn)度seek到了20s后播放了3s技肩,我們認(rèn)為的播放時(shí)間應(yīng)該是23s,可是GetCurrentTime方法中獲得的時(shí)間是11s浮声,即實(shí)際播放時(shí)間虚婿。所以每次seek時(shí)都必須保存seek的timingOffset:
AudioTimeStamp time = ...; //AudioQueueGetCurrentTime方法獲取
NSTimeInterval playedTime = time.mSampleTime / _format.mSampleRate; //seek時(shí)的播放時(shí)間
NSTimeInterval seekTime = ...; //需要seek到哪個(gè)時(shí)間
NSTimeInterval timingOffset = seekTime - playedTime;
seek之后的播放進(jìn)度需要根據(jù)timingOffset和playedTime計(jì)算
NSTimeInterval progress = timingOffset + playedTime;
2.第二個(gè)需要注意的是GetCurrentTime方法有時(shí)候會(huì)失敗,所以上次獲取的播放時(shí)間最好保存起來(lái)泳挥,如果遇到調(diào)用失敗然痊,就返回上次保存的結(jié)果。
銷毀AudioQueue
AudioQueueDispose(AudioQueueRef inAQ, Boolean inImmediate);
銷毀的同時(shí)會(huì)清除所有的buffer屉符,第二個(gè)參數(shù)的意義和用法和AudioQueueStop方法相同剧浸。
需要注意的一點(diǎn)是當(dāng)AudioQueueStart調(diào)用之后AudioQueue其實(shí)還沒(méi)有真正開始,期間會(huì)有一個(gè)短暫的間隙矗钟。如果在AudioQueueStart調(diào)用后到AudioQueue真正開始運(yùn)作前的這段時(shí)間內(nèi)調(diào)用AudioQueueDispose方法的話會(huì)導(dǎo)致卡死唆香。
要規(guī)避這種問(wèn)題的一種方法是做好線程的調(diào)度,保證Dispose方法調(diào)用一定是在每一個(gè)播放Runloop之后(即至少是一個(gè)buffer被成功播放之后)吨艇。另外一種是監(jiān)聽kAudioQueueProperty_IsRunning屬性躬它,這個(gè)屬性在AudioQueue真正運(yùn)作起來(lái)之后會(huì)變成1,停止后會(huì)變成0东涡,所以保證Start方法調(diào)用Dispose后一定在IsRunning為1是才能被調(diào)用虑凛。
到現(xiàn)在,一個(gè)基本音頻播放器就成型了软啼。下一篇章我會(huì)嘗試著用AudioFileStream和AudioQueue實(shí)現(xiàn)一個(gè)本地文件播放器。