在第三篇和第四篇中介紹了如何用AudioStreamFile
和AudioFile
解析音頻數據格式、分離音頻幀。下一步終於可以使用分離出來的音頻幀進行播放了,本片中將來講一講如何使用AudioQueue
播放音頻數據。
AudioQueue
是AudioToolBox.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
之前首先必須理解其工作模式,它之所以這麼命名是因為在其內部有一套緩沖隊列(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
工作原理結合自己理解,可以得到其工作流程大致如下:
AudioQueue
,創建一個自己的buffer數組BufferArray;AudioQueueAllocateBuffer
創建若干個AudioQueueBufferRef
(一般2-3個即可),放入BufferArray;AudioQueueEnqueueBuffer
方法把buffer插入AudioQueue
中;AudioQueue
中存在Buffer後,調用AudioQueueStart
播放。(具體等到填入多少buffer後再播放可以自己控制,只要能保證播放不間斷即可);AudioQueue
播放音樂後消耗了某個buffer,在另一個線程回調並送出該buffer,把buffer放回BufferArray供下一次使用;從以上步驟其實不難看出,AudioQueue
播放的過程其實就是一個典型的生產者消費者問題。生產者是AudioFileStream
或者AudioFile
,它們生產處音頻數據幀,放入到AudioQueue
的buffer隊列中,直到buffer填滿後需要等待消費者消費;AudioQueue
作為消費者,消費了buffer隊列中的數據,並且在另一個線程回調通知數據已經被消費生產者可以繼續生產。所以在實現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,其余參數同相同。
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的個數;
1
OSStatus AudioQueueFreeBuffer(AudioQueueRef inAQ,AudioQueueBufferRef inBuffer);
注意這個方法一般只在需要銷毀特定某個buffer時才會被用到(因為dispose方法會自動銷毀所有buffer),並且這個方法只能在AudioQueue不在處理數據時
才能使用。所以這個方法一般不太能用到。
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
OSStatus AudioQueueStart(AudioQueueRef inAQ,const AudioTimeStamp * inStartTime);
第二個參數可以用來控制播放開始的時間,一般情況下直接開始播放傳入NULL即可。
1 2 3
OSStatus AudioQueuePrime(AudioQueueRef inAQ,
UInt32 inNumberOfFramesToPrepare,
UInt32 * outNumberOfFramesPrepared);
這個方法並不常用,因為直接調用AudioQueueStart
會自動開始解碼(如果需要的話)。參數的作用是用來指定需要解碼幀數和實際完成解碼的幀數;
1
OSStatus AudioQueuePause(AudioQueueRef inAQ);
需要注意的是這個方法一旦調用後播放就會立即暫停,這就意味著AudioQueueOutputCallback
回調也會暫停,這時需要特別關注線程的調度以防止線程陷入無限等待。
1
OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);
第二個參數如果傳入true的話會立即停止播放(同步),如果傳入false的話AudioQueue
會播放完已經Enqueue的所有buffer後再停止(異步)。使用時注意根據需要傳入適合的參數。
1 2
OSStatus
AudioQueueFlush(AudioQueueRef inAQ);
調用後會播放完Enqueu的所有buffer後重置解碼器狀態,以防止當前的解碼器狀態影響到下一段音頻的解碼(比如切換播放的歌曲時)。如果和AudioQueueStop(AQ,false)
一起使用並不會起效,因為Stop方法的false參數也會做同樣的事情。
1
OSStatus AudioQueueReset(AudioQueueRef inAQ);
重置AudioQueue
會清除所有已經Enqueue的buffer,並觸發AudioQueueOutputCallback
,調用AudioQueueStop
方法時同樣會觸發該方法。這個方法的直接調用一般在seek時使用,用來清除殘留的buffer(seek時還有一種做法是先AudioQueueStop
,等seek完成後重新start)。
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
方法有時候會失敗,所以上次獲取的播放時間最好保存起來,如果遇到調用失敗,就返回上次保存的結果。
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可以從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
實現一個簡單的本地文件播放器。