iOS音頻播放 (五):AudioQueue 轉

來源:互聯網
上載者:User

iOS音頻播放 (五):AudioQueue 轉

 

前言

 

在第三篇和第四篇中介紹了如何用AudioStreamFileAudioFile解析音頻資料格式、分離音訊框架。下一步終於可以使用分離出來的音訊框架進行播放了,本片中將來講一講如何使用AudioQueue播放音頻資料。

AudioQueue介紹

AudioQueueAudioToolBox.framework中的一員,在官方文檔中Apple這樣描述AudioQueue的:

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.

在文檔中Apple推薦開發人員使用AudioQueue來實現app中的播放和錄音功能。這裡我們會針對播放功能進行介紹。

對於支援的資料格式,Apple這樣說:

Audio Queue Services lets you record and play audio in any of the following formats:* Linear PCM.* Any compressed format supported natively on the Apple platform you are developing for.* Any other format for which a user has an installed codec.

它支援PCM資料、iOS/MacOSX平台支援的壓縮格式(MP3、AAC等)、其他使用者可以自行提供解碼器的音頻資料(對於這一條,我的理解就是把音頻格式自行解碼成PCM資料後再給AudioQueue播放 )。

AudioQueue的工作模式

在使用AudioQueue之前首先必須理解其工作模式,它之所以這麼命名是因為在其內部有一套緩衝隊列(Buffer Queue)的機制。在AudioQueue啟動之後需要通過AudioQueueAllocateBuffer產生若干個AudioQueueBufferRef結構,這些Buffer將用來儲存即將要播放的音頻資料,並且這些Buffer是受產生他們的AudioQueue執行個體管理的,記憶體空間也已經被分配(按照Allocate方法的參數),當AudioQueue被Dispose時這些Buffer也會隨之被銷毀。

當有音頻資料需要被播放時首先需要被memcpy到AudioQueueBufferRef的mAudioData中(mAudioData所指向的記憶體已經被分配,之前AudioQueueAllocateBuffer所做的工作),並給mAudioDataByteSize欄位賦值傳入的資料大小。完成之後需要調用AudioQueueEnqueueBuffer把存有音頻資料的Buffer插入到AudioQueue內建的Buffer隊列中。在Buffer隊列中有buffer存在的情況下調用AudioQueueStart,此時AudioQueue就回按照Enqueue順序逐個使用Buffer隊列中的buffer進行播放,每當一個Buffer使用完畢之後就會從Buffer隊列中被移除並且在使用者指定的RunLoop上觸發一個回調來告訴使用者,某個AudioQueueBufferRef對象已經使用完成,你可以繼續重用這個對象來儲存後面的音頻資料。如此迴圈往複音頻資料就會被逐個播放直到結束。

官方文檔給出了一副圖來描述這一過程:

其中的callback按我的理解應該是指一個音頻資料裝填方法,該方法可以通過之前提到的資料使用後的回調來觸發。

AudioQueue playback

根據Apple提供的AudioQueue工作原理結合自己理解,可以得到其工作流程大致如下:

  1. 建立AudioQueue,建立一個自己的buffer數組BufferArray;
  2. 使用AudioQueueAllocateBuffer建立若干個AudioQueueBufferRef(一般2-3個即可),放入BufferArray;
  3. 有資料時從BufferArray取出一個buffer,memcpy資料後用AudioQueueEnqueueBuffer方法把buffer插入AudioQueue中;
  4. AudioQueue中存在Buffer後,調用AudioQueueStart播放。(具體等到填入多少buffer後再播放可以自己控制,只要能保證播放不間斷即可);
  5. AudioQueue播放音樂後消耗了某個buffer,在另一個線程回調並送出該buffer,把buffer放回BufferArray供下一次使用;
  6. 返回步驟3繼續迴圈直到播放結束

    從以上步驟其實不難看出,AudioQueue播放的過程其實就是一個典型的生產者消費者問題。生產者是AudioFileStream或者AudioFile,它們生產處音頻資料幀,放入到AudioQueue的buffer隊列中,直到buffer填滿後需要等待消費者消費;AudioQueue作為消費者,消費了buffer隊列中的資料,並且在另一個線程回調通知數據已經被消費生產者可以繼續生產。所以在實現AudioQueue播放音訊過程中必然會接觸到一些多線程同步、訊號量的使用、死結的避免等等問題。

    瞭解了工作流程之後再回頭來看AudioQueue的方法,其中大部分方法都非常好理解,部分需要稍加解釋。

    建立AudioQueue

    使用下列方法來產生AudioQueue的執行個體

    12345678910111213
    OSStatus AudioQueueNewOutput (const AudioStreamBasicDescription * inFormat,                              AudioQueueOutputCallback inCallbackProc,                              void * inUserData,                              CFRunLoopRef inCallbackRunLoop,                              CFStringRef inCallbackRunLoopMode,                              UInt32 inFlags,                              AudioQueueRef * outAQ);                              OSStatus AudioQueueNewOutputWithDispatchQueue(AudioQueueRef * outAQ,                                              const AudioStreamBasicDescription * inFormat,                                              UInt32 inFlags,                                              dispatch_queue_t inCallbackDispatchQueue,                                              AudioQueueOutputCallbackBlock inCallbackBlock);

    先來看第一個方法:

    第一個參數表示需要播放的音頻資料格式類型,是一個AudioStreamBasicDescription對象,是使用AudioFileStream或者AudioFile解析出來的資料格式資訊;

    第二個參數AudioQueueOutputCallback是某塊Buffer被使用之後的回調;

    第三個參數為內容物件;

    第四個參數inCallbackRunLoop為AudioQueueOutputCallback需要在的哪個RunLoop上被回調,如果傳入NULL的話就會再AudioQueue的內部RunLoop中被回調,所以一般傳NULL就可以了;

    第五個參數inCallbackRunLoopMode為RunLoop模式,如果傳入NULL就相當於kCFRunLoopCommonModes,也傳NULL就可以了;

    第六個參數inFlags是保留欄位,目前沒作用,傳0;

    第七個參數,返回產生的AudioQueue執行個體;

    傳回值用來判斷是否成功建立(OSStatus == noErr)。

    第二個方法就是把RunLoop替換成了一個dispatch queue,其餘參數同相同。

    Buffer相關的方法1. 建立Buffer
    12345678
    OSStatus AudioQueueAllocateBuffer(AudioQueueRef inAQ,                                  UInt32 inBufferByteSize,                                  AudioQueueBufferRef * outBuffer);                                  OSStatus AudioQueueAllocateBufferWithPacketDescriptions(AudioQueueRef inAQ,                                                        UInt32 inBufferByteSize,                                                        UInt32 inNumberPacketDescriptions,                                                        AudioQueueBufferRef * outBuffer);

    第一個方法傳入AudioQueue執行個體和Buffer大小,傳出的Buffer執行個體;

    第二個方法可以指定產生的Buffer中PacketDescriptions的個數;

    2. 銷毀Buffer
    1
    OSStatus AudioQueueFreeBuffer(AudioQueueRef inAQ,AudioQueueBufferRef inBuffer);

    注意這個方法一般只在需要銷毀特定某個buffer時才會被用到(因為dispose方法會自動銷毀所有buffer),並且這個方法只能在AudioQueue不在處理資料時才能使用。所以這個方法一般不太能用到。

    3. 插入Buffer
    1234
    OSStatus AudioQueueEnqueueBuffer(AudioQueueRef inAQ,                                 AudioQueueBufferRef inBuffer,                                 UInt32 inNumPacketDescs,                                 const AudioStreamPacketDescription * inPacketDescs);

    Enqueue方法一共有兩個,上面給出的是第一個方法,第二個方法AudioQueueEnqueueBufferWithParameters可以對Enqueue的buffer進行更多額外的操作,第二個方法我也沒有細細研究,一般來說用第一個方法就能滿足需求了,這裡我也就只針對第一個方法進行說明:

    這個Enqueue方法需要傳入AudioQueue執行個體和需要Enqueue的Buffer,對於有inNumPacketDescs和inPacketDescs則需要根據需要選擇傳入,文檔上說這兩個參數主要是在播放VBR資料時使用,但之前我們提到過即便是CBR資料AudioFileStream或者AudioFile也會給出PacketDescription所以不能如此一概而論。簡單的來說就是有就傳PacketDescription沒有就給NULL,不必管是不是VBR。

    播放控制1.開始播放
    1
    OSStatus AudioQueueStart(AudioQueueRef inAQ,const AudioTimeStamp * inStartTime);

    第二個參數可以用來控制播放開始的時間,一般情況下直接開始播放傳入NULL即可。

    2.解碼資料
    123
    OSStatus AudioQueuePrime(AudioQueueRef inAQ,                          UInt32 inNumberOfFramesToPrepare,                          UInt32 * outNumberOfFramesPrepared);                                    

    這個方法並不常用,因為直接調用AudioQueueStart會自動開始解碼(如果需要的話)。參數的作用是用來指定需要解碼幀數和實際完成解碼的幀數;

    3.暫停播放
    1
    OSStatus AudioQueuePause(AudioQueueRef inAQ);

    需要注意的是這個方法一旦調用後播放就會立即暫停,這就意味著AudioQueueOutputCallback回調也會暫停,這時需要特別關注線程的調度以防止線程陷入無限等待。

    4.停止播放
    1
    OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);

    第二個參數如果傳入true的話會立即停止播放(同步),如果傳入false的話AudioQueue會播放完已經Enqueue的所有buffer後再停止(非同步)。使用時注意根據需要傳入適合的參數。

    5.Flush
    12
    OSStatusAudioQueueFlush(AudioQueueRef inAQ);

    調用後會播放完Enqueu的所有buffer後重設解碼器狀態,以防止當前的解碼器狀態影響到下一段音訊解碼(比如切換播放的歌曲時)。如果和AudioQueueStop(AQ,false)一起使用並不會起效,因為Stop方法的false參數也會做同樣的事情。

    6.重設
    1
    OSStatus AudioQueueReset(AudioQueueRef inAQ);

    重設AudioQueue會清除所有已經Enqueue的buffer,並觸發AudioQueueOutputCallback,調用AudioQueueStop方法時同樣會觸發該方法。這個方法的直接調用一般在seek時使用,用來清除殘留的buffer(seek時還有一種做法是先AudioQueueStop,等seek完成後重新start)。

    7.擷取播放時間
    1234
    OSStatus AudioQueueGetCurrentTime(AudioQueueRef inAQ,                                  AudioQueueTimelineRef inTimeline,                                  AudioTimeStamp * outTimeStamp,                                  Boolean * outTimelineDiscontinuity);

    傳入的參數中,第一、第四個參數是和AudioQueueTimeline相關的我們這裡並沒有用到,傳入NULL。調用後的返回AudioTimeStamp,從這個timestap結構可以得出播放時間,計算方法如下:

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

    在使用這個時間擷取方法時有兩點必須注意:

    1、 第一個需要注意的時這個播放時間是指實際播放的時間和一般理解上的播放進度是有區別的。舉個例子,開始播放8秒後使用者操作slider把播放進度seek到了第20秒之後又播放了3秒鐘,此時通常意義上播放時間應該是23秒,即播放進度;而用GetCurrentTime方法中獲得的時間為11秒,即實際播放時間。所以每次seek時都必須儲存seek的timingOffset:

    12345
    AudioTimeStamp time = ...; //AudioQueueGetCurrentTime方法擷取NSTimeInterval playedTime = time.mSampleTime / _format.mSampleRate; //seek時的播放時間NSTimeInterval seekTime = ...; //需要seek到哪個時間NSTimeInterval timingOffset = seekTime - playedTime;

    seek後的播放進度需要根據timingOffset和playedTime計算:

    1
    NSTimeInterval progress = timingOffset + playedTime;

    2、 第二個需要注意的是GetCurrentTime方法有時候會失敗,所以上次擷取的播放時間最好儲存起來,如果遇到調用失敗,就返回上次儲存的結果。

    銷毀AudioQueue
    1
    AudioQueueDispose(AudioQueueRef inAQ,  Boolean inImmediate);

    銷毀的同時會清除其中所有的buffer,第二個參數的意義和用法與AudioQueueStop方法相同。

    這個方法使用時需要注意當AudioQueueStart調用之後AudioQueue其實還沒有真正開始,期間會有一個短暫的間隙。如果在AudioQueueStart調用後到AudioQueue真正開始運作前的這段時間內調用AudioQueueDispose方法的話會導致程式卡死。這個問題是我在使用AudioStreamer時發現的,在iOS 6必現(iOS 7我倒是沒有測試過,當時發現問題時iOS 7還沒發布),起因是由於AudioStreamer會在音頻EOF時就進入Cleanup環節,Cleanup環節會flush所有資料然後調用Dispose,那麼當音頻檔案中資料非常少時就有可能出現AudioQueueStart調用之時就已經EOF進入Cleanup,此時就會出現上述問題。

    要規避這個問題第一種方法是做好線程的調度,保證Dispose方法調用一定是在每一個播放RunLoop之後(即至少是一個buffer被成功播放之後)。第二種方法是監聽kAudioQueueProperty_IsRunning屬性,這個屬性在AudioQueue真正運作起來之後會變成1,停止後會變成0,所以需要保證Start方法調用後Dispose方法一定要在IsRunning為1時才能被調用。

    屬性和參數

    和其他的AudioToolBox類一樣,AudioToolBox有很多參數和屬性可以設定、擷取、監聽。以下是相關的方法,這裡就不再一一贅述:

    123456789101112
    //參數相關方法AudioQueueGetParameterAudioQueueSetParameter//屬性相關方法AudioQueueGetPropertySizeAudioQueueGetPropertyAudioQueueSetProperty//監聽屬性變化相關方法AudioQueueAddPropertyListenerAudioQueueRemovePropertyListener

    屬性和參數的列表:

    12345678910111213141516171819202122232425262728293031323334
    //屬性列表enum { // typedef UInt32 AudioQueuePropertyID    kAudioQueueProperty_IsRunning               = 'aqrn',       // value is UInt32    kAudioQueueDeviceProperty_SampleRate        = 'aqsr',       // value is Float64    kAudioQueueDeviceProperty_NumberChannels    = 'aqdc',       // value is UInt32    kAudioQueueProperty_CurrentDevice           = 'aqcd',       // value is CFStringRef    kAudioQueueProperty_MagicCookie             = 'aqmc',       // value is void*    kAudioQueueProperty_MaximumOutputPacketSize = 'xops',       // value is UInt32    kAudioQueueProperty_StreamDescription       = 'aqft',       // value is AudioStreamBasicDescription    kAudioQueueProperty_ChannelLayout           = 'aqcl',       // value is AudioChannelLayout    kAudioQueueProperty_EnableLevelMetering     = 'aqme',       // value is UInt32    kAudioQueueProperty_CurrentLevelMeter       = 'aqmv',       // value is array of AudioQueueLevelMeterState, 1 per channel    kAudioQueueProperty_CurrentLevelMeterDB     = 'aqmd',       // value is array of AudioQueueLevelMeterState, 1 per channel    kAudioQueueProperty_DecodeBufferSizeFrames  = 'dcbf',       // value is UInt32    kAudioQueueProperty_ConverterError          = 'qcve',       // value is UInt32    kAudioQueueProperty_EnableTimePitch         = 'q_tp',       // value is UInt32, 0/1    kAudioQueueProperty_TimePitchAlgorithm      = 'qtpa',       // value is UInt32. See values below.    kAudioQueueProperty_TimePitchBypass         = 'qtpb',       // value is UInt32, 1=bypassed};//參數列表enum    // typedef UInt32 AudioQueueParameterID;{    kAudioQueueParam_Volume         = 1,    kAudioQueueParam_PlayRate       = 2,    kAudioQueueParam_Pitch          = 3,    kAudioQueueParam_VolumeRampTime = 4,    kAudioQueueParam_Pan            = 13};

    其中比較有價值的屬性有:

    • kAudioQueueProperty_IsRunning監聽它可以知道當前AudioQueue是否在運行,這個參數的作用在講到AudioQueueDispose時已經提到過。
    • kAudioQueueProperty_MagicCookie部分音頻格式需要設定magicCookie,這個cookie可以從AudioFileStreamAudioFile中擷取。

      比較有價值的參數有:

      • kAudioQueueParam_Volume,它可以用來調節AudioQueue的播放音量,注意這個音量是AudioQueue的內部播放音量和系統音量相互獨立設定並且最後疊加生效。
      • kAudioQueueParam_VolumeRampTime參數和Volume參數配合使用可以實現音頻播放淡入淡出的效果;
      • kAudioQueueParam_PlayRate參數可以調整播放速率;後記

        至此本片關於AudioQueue的話題接結束了。使用上面提到的方法已經可以滿足大部分的播放需求,但AudioQueue的功能遠不止如此,AudioQueueTimelineOffline RenderingAudioQueueProcessingTap等功能我目前也尚未涉及和研究,未來也許還會有更多新的功能加入,學無止境啊。

        另外由於AudioQueue的相關內容無法單獨做Demo進行展示,於是我提前把後一篇內容的Demo(一個簡單的本地音頻播放器)先在這裡給出方便大家理解AudioQueue。如果覺得上面提到某一部分的很難以的話理解歡迎在下面留言或者在微博上和我交流,除此之外還可以閱讀官方文檔(我一直覺得官方文檔是學習的最好途徑);

        範例程式碼

        AudioStreamer和FreeStreamer都用到了AudioQueue。

        下篇預告

        下一篇將講述如何利用之前講到的AudioSessionAudioFileStreamAudioQueue實現一個簡單的本地檔案播放器。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.