返回步驟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可以從AudioFileStream
和AudioFile
中擷取。比較有價值的參數有:
kAudioQueueParam_Volume
,它可以用來調節AudioQueue
的播放音量,注意這個音量是AudioQueue
的內部播放音量和系統音量相互獨立設定並且最後疊加生效。
kAudioQueueParam_VolumeRampTime
參數和Volume
參數配合使用可以實現音頻播放淡入淡出的效果;
kAudioQueueParam_PlayRate
參數可以調整播放速率;後記至此本片關於AudioQueue
的話題接結束了。使用上面提到的方法已經可以滿足大部分的播放需求,但AudioQueue
的功能遠不止如此,AudioQueueTimeline
、Offline Rendering
、AudioQueueProcessingTap
等功能我目前也尚未涉及和研究,未來也許還會有更多新的功能加入,學無止境啊。
另外由於AudioQueue
的相關內容無法單獨做Demo進行展示,於是我提前把後一篇內容的Demo(一個簡單的本地音頻播放器)先在這裡給出方便大家理解AudioQueue
。如果覺得上面提到某一部分的很難以的話理解歡迎在下面留言或者在微博上和我交流,除此之外還可以閱讀官方文檔(我一直覺得官方文檔是學習的最好途徑);
範例程式碼AudioStreamer和FreeStreamer都用到了AudioQueue。
下篇預告下一篇將講述如何利用之前講到的AudioSession
、AudioFileStream
和AudioQueue
實現一個簡單的本地檔案播放器。