• 熱門專題

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

作者:  發布日期:2016-12-26 20:23:28
Tag標簽:音頻  音樂  
  • 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

    該圖很好的說明了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主要用于指定要播放的音頻文件,如下所示:
    init方法
    讀取文件操作,使用NSFileHandle類,audioInUseLock,是一個NSLock*類型,用于在AudioQueue通知我們有空的緩沖區可以使用時做標記。

    我們在用戶點擊play按鈕的時候,初始化該播放器,并調用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中定義的屬性如下所示:
    LocalAudioPlayer的相關屬性

    讀取并開始解析音頻

    我們使用AudioFileStream來解析音頻信息,在用戶調用play方法之后,首先調用AudioFileStreamOpen,打開AudioFileStream,如下所示:
    打開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。

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

    解析音頻信息

    如果解析到音頻信息,那么將會調用之前指定的回調函數,如下所示:
    解析音頻信息1
    解析音頻信息2

    每個相關的屬性都可以調用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。之后便是音頻數據幀的解析。

    解析音頻幀

    音頻信息解析完畢之后,就應該解析音頻數據幀了,代碼如下所示:

    解析音頻數據幀1
    在這里,我們利用之前設置的inClientData,將該回調函數,由C語言形式改為Objc的形式,解析到音頻數據之后的處理代碼如下所示:
    解析音頻數據幀2

    解析到音頻數據之后,我們穴ky"http://www.xslszxw.com/qq/" target="_blank" class="keylink">qq9q8r9vt3QtMjrtb1BdWRpb1F1ZXVlQnVmZmVy1tCjrMrXz8ijrLjDu9i197qvyv21xNSt0M3I58/Cy/nKvqO6PC9wPgo8cHJlIGNsYXNzPQ=="brush:java;"> 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啟動或者是終止的時候,會調用該函數:
    設置監聽器

    該回調函數,如下所示:
    AudioQueue啟動或終止回調

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

    不過在實際使用過程中,我發現數據通ky"http://www.xslszxw.com/qq/" target="_blank" class="keylink">qq/1bXEyrG68kF1ZGlvUXVldWWyorK7u+HW97av1tXWuaOsvLSyu7vh1ve2r7X308O4w7vYtfejrMv50tTO0s/ro6zTprjDysfSqs7Sw8fX1Ly6u/HIobW9tbHHsLKlt8W1xL34tsijrLWxsqW3xc3qsc+1xMqxuvK199PDQXVkaW9RdWV1ZVN0b3DW1da5sqW3xbDJo6zV4rj2zsrM4sH018W0/dLUuvPU2bzM0PjR0L6/oaM8L3A+CjxoMyBpZD0="結束">結束

    終于完成了全部的代碼,用戶只要點擊play,即可以聽到《遙遠的她》這首美妙的音樂了。由于界面上只有一個play按鈕,不放演示圖了,戴上耳機,靜靜的享受自己的成果與這美妙的音樂。。

About IT165 - 廣告服務 - 隱私聲明 - 版權申明 - 免責條款 - 網站地圖 - 網友投稿 - 聯系方式
本站內容來自于互聯網,僅供用于網絡技術學習,學習中請遵循相關法律法規
彩票联盟网站饶平县| 富源县|