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

來源:互聯網
上載者:User

標籤:連續   行資料   cookie   tao   ide   get   rtti   ash   start   

  • 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方法進行播放

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="初始化播放器執行個體" title="">

播放音頻

播放音頻將分為下面步驟:
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,例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="開啟AudioFileStream" title="">

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 OSStatusAudioFileStreamParseBytes(AudioFileStreamID inAudioFileStream, UInt32 inDataByteSize, const void * inData, AudioFileStreamParseFlags inFlags);

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

當檔案資料合部讀取結束的時候,此時便能夠關閉檔案。

解析音頻資訊

假設解析到音頻資訊,那麼將會調用之前指定的回呼函數,例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="解析音頻資訊-1" title="">

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

extern OSStatusAudioFileStreamGetProperty(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的形式,解析到音頻資料之後的處理代碼例如以下所看到的:

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="解析音頻資料幀-2" title="">

解析到音頻資料之後,我們要將資料寫入到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指明了該幀資料的大小。

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

watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvY2Fpcm8xMjM=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast" alt="建立音頻隊列" title="">

之後我們遍曆每一幀。擷取每一幀的位移位置及資料大小,假設當前幀的資料大小,已經無法存放到當前的緩衝區當中,此時,我們應該將當前的緩衝區入隊。改動當前使用的緩衝區為下一個。並重設相關的資料填充資訊及已包括的資料幀資訊。

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(一):播放本地音樂

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.