你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發綜合 >> iOS音頻播放 (五):AudioQueue 轉

iOS音頻播放 (五):AudioQueue 轉

編輯:IOS開發綜合

 

前言

 

在第三篇和第四篇中介紹了如何用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按我的理解應該是指一個音頻數據裝填方法,該方法可以通過之前提到的數據使用後的回調來觸發。

AudioQueueAudioQueue 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的實例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    
    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

    1
    2
    3
    4
    5
    6
    7
    8
    
    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

    1
    2
    3
    4
    
    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.解碼數據

    1
    2
    3
    
    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

    1
    2
    
    OSStatus
    AudioQueueFlush(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.獲取播放時間

    1
    2
    3
    4
    
    OSStatus AudioQueueGetCurrentTime(AudioQueueRef inAQ,
                                      AudioQueueTimelineRef inTimeline,
                                      AudioTimeStamp * outTimeStamp,
                                      Boolean * outTimelineDiscontinuity);
    

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

    1
    2
    
    AudioTimeStamp time = ...; //AudioQueueGetCurrentTime方法獲取
    NSTimeInterval playedTime = time.mSampleTime / _format.mSampleRate;
    

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

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

    1
    2
    3
    4
    5
    
    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有很多參數和屬性可以設置、獲取、監聽。以下是相關的方法,這裡就不再一一贅述:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    
    //參數相關方法
    AudioQueueGetParameter
    AudioQueueSetParameter
    
    //屬性相關方法
    AudioQueueGetPropertySize
    AudioQueueGetProperty
    AudioQueueSetProperty
    
    //監聽屬性變化相關方法
    AudioQueueAddPropertyListener
    AudioQueueRemovePropertyListener
    

    屬性和參數的列表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    
    //屬性列表
    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實現一個簡單的本地文件播放器。

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved