1. 程式人生 > >iOS音頻播放之AudioQueue(一):播放本地音樂

iOS音頻播放之AudioQueue(一):播放本地音樂

init方法 函數 完成 一起 utc getprop 應用 清洗 spl

  • AudioQueue簡單介紹
  • AudioStreamer說明
  • AudioQueue具體解釋
    • AudioQueue工作原理
    • AudioQueue主要接口
      • AudioQueueNewOutput
      • AudioQueueAllocateBuffer
      • AudioQueueEnqueueBuffer
      • AudioQueueStart Pause Stop Flush Reset Dispose
      • AudioQueueFreeBuffer
      • AudioQueueGetProperty AudioQueueSetProperty
  • 音頻播放LocalAudioPlayer
    • 播放器的初始化
    • 播放音頻
      • LocalAudioPlayer相關屬性
      • 讀取並開始解析音頻
      • 解析音頻信息
        • kAudioFileStreamProperty_DataFormat
        • kAudioFileStreamProperty_FileFormat
        • kAudioFileStreamProperty_AudioDataByteCount
        • kAudioFileStreamProperty_BitRate
        • kAudioFileStreamProperty_DataOffset
        • kAudioFileStreamProperty_AudioDataPacketCount
        • kAudioFileStreamProperty_ReadyToProducePackets
      • 解析音頻幀
      • 播放音頻數據
      • 清理相關資源
  • 結束

iOS實現播放本地音樂,有非常多種方法。比如AVAudioPlayer,這些都能非常好的勝任。有人就奇怪了。為什麽要退而求其次。使用更復雜的AudioQueue來播放本地音樂呢?請繼續往下看

AudioQueue簡單介紹

AudioQueue,在蘋果的開發人員文檔上是這麽說的

"Audio Queue Services provides a straightforward, low overhead way to record and play audio in iOS and Mac OS X."

AudioQueue官方文檔

AudioQueue服務提供一種直接的,低開銷的方式以用於在iOS及Mac OS X上錄音和播放音樂。

使用AudioQueue播放音樂的長處就是開銷非常小並且支持流式播放(邊下邊播)。可是缺點就是開發難度大。所以有網絡音頻庫AudioStreamer,網上有非常多講AudioQueue的。可是有實例代碼說明的,實在是少之又少。正好公司項目有音頻需求,盡管項目中使用的並不是我自己寫的音頻播放功能,但事後還是想自己來研究一下,這個在我看來比較奇妙也比較有趣的AudioQueue。

AudioStreamer說明

iOS上一個比較有名的流媒體音頻播放庫是AudioStreamer,該庫即使用了AudioQueue。只是該音頻庫並不支持本地音樂播放,我感覺非常奇怪,為什麽作者不支持。並且在使用過程中,我發現該庫還是有點問題,盡管我對音頻方面的知識並不怎麽了解,也並不能與大師媲美並論。但我也希望通過自己的學習,終於完成一個相似AudioStreamer的網絡音樂庫,眼下或許僅僅是一個設想。無論最後自己有沒有那能力,起碼我以前也嘗試過。只是工作近期比較忙,加上自己知識的欠缺。不知何時才幹實現。本次就先來補上AudioStreamer沒有支持的,使用AudioQueue播放本地音樂。

AudioQueue具體解釋

AudioQueue工作原理

我從Apple的官方文檔上截下下面該圖:
技術分享圖片

該圖非常好的說明了AudioQueue的工作原理,例如以下說明:
1. 用戶調用對應的方法,將音頻數據從硬盤中讀入到AudioQueue的緩沖區中,並將緩沖區送入音頻隊列。


2. 用戶App通過AudioQueue提供的接口。告訴外放設備,緩沖區中已經有數據。能夠拿去播放。
3. 當一個緩沖區中的音頻數據播放完成之後,AudioQueue告訴用戶,當前有一個空的緩沖區能夠用來給你填充數據。


4. 反復以上步驟。直至數據播放完成。

到這裏,肯定有不少同學發現了,AudioQueue事實上就是生產者-消費者模型的典型應用。

AudioQueue主要接口

AudioQueueNewOutput

OSStatus AudioQueueNewOutput(const AudioStreamBasicDescription *inFormat, AudioQueueOutputCallback inCallbackProc, void *inUserData, CFRunLoopRef inCallbackRunLoop, CFStringRef inCallbackRunLoopMode, UInt32 inFlags, AudioQueueRef  _Nullable *outAQ);

該方法用於創建一個用於輸出音頻的AudioQueue

參數及返回說明例如以下:
1. inFormat:該參數指明了即將播放的音頻的數據格式
2. inCallbackProc:該回調用於當AudioQueue已使用完一個緩沖區時通知用戶,用戶能夠繼續填充音頻數據
3. inUserData:由用戶傳入的數據指針,用於傳遞給回調函數
4. inCallbackRunLoop:指明回調事件發生在哪個RunLoop之中,假設傳遞NULL,表示在AudioQueue所在的線程上運行該回調事件,普通情況下,傳遞NULL就可以。
5. inCallbackRunLoopMode:指明回調事件發生的RunLoop的模式,傳遞NULL相當於kCFRunLoopCommonModes,通常情況下傳遞NULL就可以
6. outAQ:該AudioQueue的引用實例,

返回OSStatus,假設值為noErr。則表示沒有錯誤。AudioQueue創建成功。

AudioQueueAllocateBuffer

OSStatus AudioQueueAllocateBuffer(AudioQueueRef inAQ, UInt32 inBufferByteSize, AudioQueueBufferRef  _Nullable *outBuffer);

該方法的作用是為存放音頻數據的緩沖區開辟空間

參數及返回說明例如以下:
1. inAQ:AudioQueue的引用實例
2. inBufferByteSize:須要開辟的緩沖區的大小
3. outBuffer:開辟的緩沖區的引用實例

返回OSStatus,假設值為noErr,則表示緩沖區開辟成功。

AudioQueueEnqueueBuffer

OSStatus AudioQueueEnqueueBuffer(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, UInt32 inNumPacketDescs, const AudioStreamPacketDescription *inPacketDescs);

該方法用於將已經填充數據的AudioQueueBuffer入隊到AudioQueue

參數及返回說明例如以下:
1. inAQ:AudioQueue的引用實例
2. inBuffer:須要入隊的緩沖區實例
3. inNumPacketDescs:緩沖區中共存在有多少幀音頻數據
4. inPacketDescs:緩沖區中每一幀的相關信息。用戶須要指明當中每一幀在緩沖區中數據的偏移值,通過字段mStartOffset來指定

返回OSStatus。假設值為noErr,則表示緩沖區已經成功入隊。等待播放

AudioQueueStart Pause Stop Flush Reset Dispose

OSStatus AudioQueueStart(AudioQueueRef inAQ, const AudioTimeStamp *inStartTime);
OSStatus AudioQueuePause(AudioQueueRef inAQ);
OSStatus AudioQueueStop(AudioQueueRef inAQ, Boolean inImmediate);
OSStatus AudioQueueFlush(AudioQueueRef inAQ);
OSStatus AudioQueueReset(AudioQueueRef inAQ);
OSStatus AudioQueueDispose(AudioQueueRef inAQ, Boolean inImmediate);

顧名思義,前三個方法用於音頻的播放,暫停及停止。

後兩個方法用於在最後清洗及重置音頻隊列,清洗確保隊列中的數據全然輸出。AudioQueuDispose用於清理AudioQueue所占資源。

參數及返回說明例如以下:
1. inAQ:AudioQueue的引用實例
2. inStartTime:指明要開始播放音頻的時間,假設要馬上開始。傳遞NULL
3. inImmediate:指明是否要馬上停止音頻播放,如是。傳遞true

返回OSStatus表示相關的操作是否成功運行。

AudioQueueFreeBuffer

OSStatus AudioQueueFreeBuffer(AudioQueueRef inAQ, AudioQueueBufferRef inBuffer);

該方法用於在播放結束時。釋放清理緩沖區時使用

AudioQueueGetProperty AudioQueueSetProperty

OSStatus AudioQueueGetProperty(AudioQueueRef inAQ, AudioQueuePropertyID inID, void *outData, UInt32 *ioDataSize);
OSStatus AudioQueueSetProperty(AudioQueueRef inAQ, AudioQueuePropertyID inID, const void *inData, UInt32 inDataSize);

此GET/SET方法,用於設置獲取AudioQueue的相關屬性,請參看AudioQueue.h頭文件裏的相關說明。

音頻播放(LocalAudioPlayer)

使用AudioQueue播放音樂,一般須要配合AudioFileStream一起,AudioFileStream負責解析音頻數據。AudioQueue負責播放解析到的音頻數據。

此次僅實現最主要的本地音頻播放功能。旨在為以後打下基礎,不處理不論什麽相關的狀態(如暫停、停止、SEEK),錯誤等。

播放方式相似流式播放,僅僅是音頻數據來源於本地文件而非網絡。需經過下面幾個步驟:
1. 持續不斷的從文件裏讀取部分數據,直到數據所有讀取結束
2. 將文件裏讀出的數據,交給AudioFileStream進行數據解析
3. 創建AudioQueue,當數據放入到AudioQueueBuffer中
4. 將緩沖區放到到AudioQueue中,開始播放音頻
5. 播放音頻結束,清理相關資源

播放器的初始化

播放器的init主要用於指定要播放的音頻文件,例如以下所看到的:
技術分享圖片
讀取文件操作,使用NSFileHandle類。audioInUseLock,是一個NSLock*類型,用於在AudioQueue通知我們有空的緩沖區能夠使用時做標記。

我們在用戶點擊playbutton的時候,初始化該播放器,並調用play方法進行播放
技術分享圖片

播放音頻

播放音頻將分為下面步驟:
1. 讀取並開始解析音頻
2. 解析音頻信息
3. 解析音頻幀
4. 播放音頻數據
5. 清理相關資源

我們先定義幾個宏,用於指定一些緩沖區的大小

#define kNumberOfBuffers 3              //AudioQueueBuffer數量。一般指明為3
#define kAQBufSize 128 * 1024           //每一個AudioQueueBuffer的大小
#define kAudioFileBufferSize 2048       //文件讀取數據的緩沖區大小
#define kMaxPacketDesc 512              //最大的AudioStreamPacketDescription個數

LocalAudioPlayer相關屬性

LocalAudioPlayer中定義的屬性例如以下所看到的:
技術分享圖片

讀取並開始解析音頻

我們使用AudioFileStream來解析音頻信息。在用戶調用play方法之後。首先調用AudioFileStreamOpen,打開AudioFileStream,例如以下所看到的:
技術分享圖片

extern OSStatus 
AudioFileStreamOpen (void *inClientData, AudioFileStream_PropertyListenerProc   inPropertyListenerProc, AudioFileStream_PacketsProc             inPacketsProc, AudioFileTypeID                          inFileTypeHint, AudioFileStreamID * outAudioFileStream);

AudioFileStreamOpen的參數說明例如以下:
1. inClientData:用戶指定的數據,用於傳遞給回調函數。這裏我們指定(__bridge LocalAudioPlayer*)self
2. inPropertyListenerProc:當解析到一個音頻信息時。將回調該方法
3. inPacketsProc:當解析到一個音頻幀時,將回調該方法
4. inFileTypeHint:指明音頻數據的格式。假設你不知道音頻數據的格式,能夠傳0
5. outAudioFileStream:AudioFileStreamID實例,需保存供興許使用

讀取到數據之後。調用AudioFileStreamParseBytes解析數據,其原型例如以下:

extern OSStatus
AudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream, UInt32 inDataByteSize, const void * inData, AudioFileStreamParseFlags inFlags);

參數的說明例如以下:
1. inAudioFileStream:AudioFileStreamID實例,由AudioFileStreamOpen打開
2. inDataByteSize:此次解析的數據字節大小
3. inData:此次解析的數據大小
4. inFlags:數據解析標誌。當中僅僅有一個值kAudioFileStreamParseFlag_Discontinuity = 1,表示解析的數據是否是不連續的,眼下我們能夠傳0。

當文件數據合部讀取結束的時候,此時便能夠關閉文件。

解析音頻信息

假設解析到音頻信息,那麽將會調用之前指定的回調函數,例如以下所看到的:
技術分享圖片
技術分享圖片

每一個相關的屬性都能夠調用AudioFileStreamGetProperty來獲取到對應的值,原型例如以下:

extern OSStatus
AudioFileStreamGetProperty(AudioFileStreamID inAudioFileStream, AudioFileStreamPropertyID inPropertyID, UInt32 *ioPropertyDataSize, void *                              outPropertyData);

參數說明:
1. inAudioFileStream:AudioFileStreamID實例,由AudioFileStreamOpen打開
2. inPropertyID:要獲取的屬性名稱。參見AudioFileStream.h
3. ioPropertyDataSize:指明該屬性的大小
4. outPropertyData:用於存放該屬性值的空間

kAudioFileStreamProperty_DataFormat

該屬性指明了音頻數據的格式信息,返回的數據是一個AudioStreamBasicDescription結構。需保存用於AudioQueue的使用

kAudioFileStreamProperty_FileFormat

該屬性指明了音頻數據的編碼格式,如MPEG等。

kAudioFileStreamProperty_AudioDataByteCount

該屬性可獲取到音頻數據的長度,可用於計算音頻時長,計算公式為:
時長 = (音頻數據字節大小 * 8) / 採樣率

kAudioFileStreamProperty_BitRate

該屬性可獲取到音頻的採樣率。可用於計算音頻時長

kAudioFileStreamProperty_DataOffset

該屬性指明了音頻數據在整個音頻文件裏的偏移量:
音頻文件總大小 = 偏移量 + 音頻數據字節大小

kAudioFileStreamProperty_AudioDataPacketCount

該屬性指明了音頻文件裏共同擁有多少幀

kAudioFileStreamProperty_ReadyToProducePackets

該屬性告訴我們,已經解析到完整的音頻幀數據,準備產生音頻幀。之後會調用到另外一個回調函數。我們在這裏創建音頻隊列AudioQueue,假設音頻數據中有Magic Cookie Data,則先調用AudioFileStreamGetPropertyInfo,獲取該數據是否可寫,假設可寫再取出該屬性值,並寫入到AudioQueue。之後便是音頻數據幀的解析。

解析音頻幀

音頻信息解析完成之後,就應該解析音頻數據幀了,代碼例如以下所看到的:

技術分享圖片
在這裏。我們利用之前設置的inClientData,將該回調函數,由C語言形式改為Objc的形式,解析到音頻數據之後的處理代碼例如以下所看到的:
技術分享圖片

解析到音頻數據之後,我們要將數據寫入到AudioQueueBuffer中,首先,該回調函數的原型例如以下所看到的:

typedef void (*AudioFileStream_PacketsProc)(void *              inClientData, UInt32 inNumberBytes,UInt32       inNumberPackets, const void *inInputData,                           AudioStreamPacketDescription *inPacketDescriptions);

參數說明:
1. inClientData:由AudioFileStreamOpen設置的用戶數據
2. inNumberBytes:音頻數據的字節數
3. inNumberPackets:解析到的音頻幀數
4. inInputData:包括這些音頻數據幀的數據
5. inPacketDescriptions:AudioStreamPacketDescription數組。當中包括mStartOffset,指明了該幀相關數據的起始位置。mDataByteSize指明了該幀數據的大小。

此時我們首先創建一個音頻隊列。用以播放音頻。並為每一個緩沖區分配空間,例如以下所看到的:
技術分享圖片

之後我們遍歷每一幀。獲取每一幀的偏移位置及數據大小,假設當前幀的數據大小,已經無法存放到當前的緩沖區當中,此時,我們應該將當前的緩沖區入隊。改動當前使用的緩沖區為下一個。並重置相關的數據填充信息及已包括的數據幀信息。

self.audioQueueCurrentBufferIndex = (++self.audioQueueCurrentBufferIndex) % kNumberOfBuffers;
self.audioPacketsFilled = 0;
self.audioDataBytesFilled = 0;

假設此時還沒有開始播放音樂,則能夠開始播放音樂

if(self.isPlaying == NO) {
    err = AudioQueueStart(audioQueueRef, NULL);
    self.isPlaying = YES;
}

若下一個指定的緩沖區已經在使用。即所有緩沖區已滿且入隊。則應該進行等待

while(inuse[self.audioQueueCurrentBufferIndex]);

最後,假設緩沖區空間還能存放一幀數據,我們就使用memcpy將數據拷貝到緩沖區中對應的位置上去,保存每一幀的相關信息,設置每一幀在緩沖區中的偏移量(mStartOffset),設置當前緩沖區已存方的數據大小。及已包括的幀量。

當一個緩沖區使用結束之後,AudioQueue將會調用之前由AudioQueueNewOutput設置的回調函數,例如以下所看到的:

技術分享圖片
在該回調中,我們遍歷每一個緩沖區,找到空緩沖區。將該緩沖區的標記改動為未使用。

播放音頻數據

在處理數據幀的時候。假設緩沖區已經滿(指緩沖區的空間不足以存放下一幀數據),則此時能夠開始播放音頻

if(self.isPlaying == NO) {
    err = AudioQueueStart(audioQueueRef, NULL);
    self.isPlaying = YES;
}

我們還能夠調用AudioQueuePause等相關方法來暫停和終止音頻播放。例如以下所看到的:

技術分享圖片

清理相關資源

終於。我們清理相關資源,在前面,我們利用了AudioQueueAddPropertyListener為kAudioQueueProperty_IsRunning屬性設置了一個監聽器,當AudioQueue啟動或者是終止的時候,會調用該函數:
技術分享圖片

該回調函數,例如以下所看到的:
技術分享圖片

在該方法中,我們使用AudioQueueReset重置播放隊列,調用AudioQueueFreeBuffer釋放緩沖區空間,釋放AudioQueue所有資源,關閉AudioFileStream。

只是在實際使用過程中,我發現數據為空的時候AudioQueue並不會主動終止,即不會主動調用該回調。所以我想,應該是要我們自己獲取到當前播放的進度,當播放完成的時候調用AudioQueueStop終止播放吧,這個問題留著待以後再繼續研究。

結束

終於完成了所有的代碼,用戶僅僅要點擊play,即能夠聽到《遙遠的她》這首美妙的音樂了。因為界面上僅僅有一個playbutton。不放演示圖了。戴上耳機。靜靜的享受自己的成果與這美妙的音樂。

iOS音頻播放之AudioQueue(一):播放本地音樂