iOS音頻學(xué)習(xí)三之AudioQueue

上兩篇我介紹了如何用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ò)程如圖所示


AudioQueue.png

由圖來(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è)本地文件播放器。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末延柠,一起剝皮案震驚了整個(gè)濱河市祸挪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贞间,老刑警劉巖贿条,帶你破解...
    沈念sama閱讀 222,729評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異增热,居然都是意外死亡整以,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,226評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門峻仇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)公黑,“玉大人,你說(shuō)我怎么就攤上這事》惭粒” “怎么了人断?”我有些...
    開封第一講書人閱讀 169,461評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)朝蜘。 經(jīng)常有香客問(wèn)我恶迈,道長(zhǎng),這世上最難降的妖魔是什么谱醇? 我笑而不...
    開封第一講書人閱讀 60,135評(píng)論 1 300
  • 正文 為了忘掉前任暇仲,我火速辦了婚禮,結(jié)果婚禮上副渴,老公的妹妹穿的比我還像新娘奈附。我一直安慰自己,他們只是感情好佳晶,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,130評(píng)論 6 398
  • 文/花漫 我一把揭開白布桅狠。 她就那樣靜靜地躺著,像睡著了一般轿秧。 火紅的嫁衣襯著肌膚如雪中跌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,736評(píng)論 1 312
  • 那天菇篡,我揣著相機(jī)與錄音漩符,去河邊找鬼。 笑死驱还,一個(gè)胖子當(dāng)著我的面吹牛嗜暴,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播议蟆,決...
    沈念sama閱讀 41,179評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼闷沥,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了咐容?” 一聲冷哼從身側(cè)響起舆逃,我...
    開封第一講書人閱讀 40,124評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎戳粒,沒(méi)想到半個(gè)月后路狮,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,657評(píng)論 1 320
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蔚约,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,723評(píng)論 3 342
  • 正文 我和宋清朗相戀三年奄妨,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片苹祟。...
    茶點(diǎn)故事閱讀 40,872評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡砸抛,死狀恐怖评雌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情锰悼,我是刑警寧澤柳骄,帶...
    沈念sama閱讀 36,533評(píng)論 5 351
  • 正文 年R本政府宣布,位于F島的核電站箕般,受9級(jí)特大地震影響耐薯,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜丝里,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,213評(píng)論 3 336
  • 文/蒙蒙 一曲初、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧杯聚,春花似錦臼婆、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,700評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至傀广,卻和暖如春颁独,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伪冰。 一陣腳步聲響...
    開封第一講書人閱讀 33,819評(píng)論 1 274
  • 我被黑心中介騙來(lái)泰國(guó)打工誓酒, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人贮聂。 一個(gè)月前我還...
    沈念sama閱讀 49,304評(píng)論 3 379
  • 正文 我出身青樓靠柑,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親吓懈。 傳聞我的和親對(duì)象是個(gè)殘疾皇子歼冰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,876評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容