Audio Queue錄製 播放原理

閱讀前提:

  • C語言基礎
  • 音視頻基礎
  • Core Audio基本數據結構
  • Audio Session

Audio Queue Services是官方推薦的方式以一種直接的,低開銷的方式在iOS與Mac OS X中完成錄製與播放的操做.不像上層的API,它能夠經過回調拿到音頻幀數據,以完成更加精細的操做.html

使用場景:

比上層API而言,能夠直接獲取每一幀音頻數據,所以能夠對音頻幀作一些須要的處理. 可是沒法對聲音作一些更加精細的處理,如回聲消除,混音,降噪等等,若是須要作更底層的操做,須要使用Audio Unit.數組

Overview

Audio Queue Service是Core Audio的Audio Toolbox框架中的基於C語言的一套接口.緩存

Audio Queue Services是一套高級的API. 它不只能夠在無需瞭解硬件的基礎上使程序與音頻硬件(麥克風,揚聲器等)之間完成交互,也在無需瞭解編解碼器的原理狀況下讓咱們使用複雜的編解碼器.bash

同時,Audio Queue Services還提供了更加精細的定時控制以支持預約的播放與同步任務.可使用它同步多個音頻播放隊列或者音視頻間進行同步.cookie

支持如下格式網絡

  • 線性PCM
  • Apple提供的本機支持的任何壓縮格式
  • 用戶使用編解碼器生成的任何格式

注意: Audio Queue Services是一套純C的接口,因此基礎的C,C++須要有必定了解.數據結構

1. Audio Queues概述

在iOS, Mac OS X中audio queue是一個軟件層面的對象,能夠用來作錄製與播放操做.使用AudioQueueRef表明其數據結構.架構

做用app

  • 鏈接音頻硬件
  • 管理相關模塊內存
  • 使用編解碼器
  • 調解錄製與播放

1.1. Audio Queue架構

  • 一組音頻隊列數據,隊列中每一個結點都是音頻數據的臨時存儲庫.
  • 隊列中數據是嚴格按照順序排列
  • 回調函數

1.2. 錄製

若是要使用audio queue的錄製功能,經過AudioQueueNewInput建立錄音隊列.框架

1.record

錄製使用的audio queue的輸入端一般是當前設備鏈接的音頻設備,如內置的麥克風,或外置的帶麥克風功能的輸入設備.輸出端是咱們定義的回調函數.若是將音頻數據錄製成文件,能夠在回調函數中將從audio queue中取出的音頻數據寫入文件.固然錄製的音頻數據也能夠直接送給當前App以實現邊錄製邊播放的功能.

每一個audio queue,不論是用於錄製或播放,都至少有一個或多個音頻數據.全部的音頻數據被放在一個被稱爲音頻隊列buffer特殊的數據結構中,能夠理解成隊列中的結點.如上圖所示,指定數量的buffer按順序依次被放入音頻隊列中,它們最終也將在回調函數中按順序取出.

1.3. 播放

若是要使用audio queue的播放功能,經過AudioQueueNewOutput建立播放隊列對象.

2.play

播放使用的音頻隊列,回調函數在輸入端.該回調函數將從本地或其餘音頻數據源獲取到的數據交給音頻隊列中.當沒有數據裝入播放回調函數也會告訴音頻隊列中止播放.

用於播放的音頻隊列的輸出端則鏈接着音頻輸出硬件,如揚聲器或外接的具備揚聲器功能的音頻設備(如:耳機,音響等).

1.4. 音頻隊列數據

AudioQueueBuffer用於存放音頻隊列數據.

typedef struct AudioQueueBuffer {
    const UInt32   mAudioDataBytesCapacity;
    void *const    mAudioData;
    UInt32         mAudioDataByteSize;
    void           *mUserData;
} AudioQueueBuffer;
typedef AudioQueueBuffer *AudioQueueBufferRef;
複製代碼
  • mAudioData: 當前取出的隊列中存放的即時的音頻數據指針,它指向真正存放音頻數據的內存地址.
  • mAudioDataBytesCapacity: 當前音頻數據最大存儲空間
  • mAudioDataByteSize: 當前存儲的音頻數據實際的大小
  • mUserData: 開發者能夠存放一些自定義的數據

音頻隊列可使用任意數量的音頻數據結點,但通常建議用3個便可.由於若是太少則存取過於頻繁,太多則增長應用程序內存消耗,正常狀況下兩個便可,咱們可使用第三個當有延遲狀況出現時做爲補償數據.

由於Audio Queue的純C函數,內存須要咱們手動管理.

  • 初始化Audio Queue時使用AudioQueueAllocateBuffer分配內存
  • 回調函數中用完時使用AudioQueueDispose回收內存

經過內存管理,可使錄製播放更加穩定,同時優化App資源使用.

1.5. 音頻隊列與入隊操做

audio queue: 音頻隊列, 即Audio Queue Services的名字

audio queue buffer : 音頻隊列中存放一個或多個結點數據

  • 錄製過程

作錄製操做時,一個audio queue buffer將被從輸入設備(如:麥克風)採集的音頻數據填充.音頻隊列中剩餘的buffer按順序排列在當前填充數據的buffer以後,依次等待被填充數據.在輸出端,回調函數將按照指定時間間隔依次接收音頻隊列中按順序排列好的音頻數據.工做原理以下圖:

3.recording_process

圖一: 錄製開始,音頻隊列中填充須要的音頻數據.

圖二: 第一個buffer被填充,對調函數取出buffer 1並將其寫入文件,同時buffer2也被填充完數據.

圖三: 在第4步,回調函數將用完的buffer 1從新放回音頻隊列,隨後第五步回調函數再次取出音頻數據buffer2,最終將其寫入文件然後從新放回音頻隊列此後循環往復直到錄製中止.

  • 播放過程

作播放操做時,一個audio queue buffer須要交給輸出設備(如:揚聲器).剩餘的音頻數據也將按順序排列在當前取出播放的音頻數據以後,等待播放.回調函數將按順序取出音頻隊列中的數據交給揚聲器,隨後將用完的audio queue buffer從新放入音頻隊列.

4.playback_process

圖1: 應用程序啓動音頻播放隊列,每調用依次回調函數填充一個audio queue buffers,填充完後將其放入音頻隊列. 當應用程序調用AudioQueueStart當即開始播放.

圖2: 音頻隊列輸出第一個音頻數據

圖3: 用完的audio queue buffer從新放入音頻隊列.一旦播放了第一個音頻數據,音頻隊列會進入一個循環穩定的狀態,即開始播放下一個buffer2(第4步)而後調用回調函數準備填充數據(第5步),最後(第6步)buffer1從新被填充並裝入音頻隊列依次循環直到音頻隊列中止.

  • 控制播放的過程

Audio queue buffers始終按照入隊順序進行播放.然而可使用AudioQueueEnqueueBufferWithParameters函數作一些額外控制

a. 設置緩衝區精確的播放時間,用於同步

b. 能夠裁剪開始或結尾的audio queue buffer,這使咱們能夠作到開始或結尾的靜音效果.

c. 增長播放的聲音

後文播放章節中將具體介紹.

1.6. 回調函數

不管錄製仍是播放,一旦註冊好回調函數,它將頻繁的被調用.調用時間取決於咱們的設置.回調函數的一個重要職責是將用完的數據從新交給音頻隊列.使用AudioQueueEnqueueBuffer入隊.

1.6.1. 錄製的回調函數
AudioQueueInputCallback (
    void                               *inUserData,
    AudioQueueRef                      inAQ,
    AudioQueueBufferRef                inBuffer,
    const AudioTimeStamp               *inStartTime,
    UInt32                             inNumberPacketDescriptions,
    const AudioStreamPacketDescription *inPacketDescs
);
複製代碼

當輸入端採集到音頻數據時就會觸發回調,能夠從回調函數中取出裝有音頻數據的audio queue buffer.

  • inUserData: 自定義的數據,開發者能夠傳入一些咱們須要的數據供回調函數使用.注意:通常狀況下咱們須要將當前的OC類實例傳入,由於回調函數是純C語言,不能調用OC類中的屬性與方法,因此傳入OC實例以與本類中屬性方法交互.
  • inAQ: 調用回調函數的音頻隊列
  • inBuffer: 裝有音頻數據的audio queue buffer.
  • inStartTime: 當前音頻數據的時間戳.主要用於同步.
  • inNumberPacketDescriptions: 數據包描述參數.若是你正在錄製VBR格式,音頻隊列會提供此參數的值.若是錄製文件須要將其傳遞給AudioFileWritePackets函數.CBR格式不使用此參數.
  • inPacketDescs: 音頻數據中一組packet描述.若是是VBR格式數據,若是錄製文件須要將此值傳遞給AudioFileWritePackets函數
1.6.2. 播放的回調函數
AudioQueueOutputCallback (
    void                  *inUserData,
    AudioQueueRef         inAQ,
    AudioQueueBufferRef   inBuffer
);
複製代碼

在回調函數中將讀取音頻數據以用來播放

  • inUserData:自定義的數據,開發者能夠傳入一些咱們須要的數據供回調函數使用.注意:通常狀況下咱們須要將當前的OC類實例傳入,由於回調函數是純C語言,不能調用OC類中的屬性與方法,因此傳入OC實例以與本類中屬性方法交互.
  • inAQ:調用回調函數的音頻隊列
  • inBuffer:回調將要填充的數據。

若是應用程序正在播放VBR格式數據,這個回調函數須要經過AudioFileReadPackets獲取音頻數據包信息.而後,回調將數據包信息放入自定義數據結構中,以使其可用於播放音頻隊列。

1.7. 使用編解碼器

Audio Queue Services使音頻編解碼器用於轉換音頻數據格式.你的錄製或播放可使用編解碼器支持的任意格式.

每一個audio queue有一個本身的音頻數據格式,被封裝在AudioStreamBasicDescription中,經過mFormatID能夠指定音頻數據格式,audio queue會自動選擇適當編解碼器對其壓縮.開發者能夠指定採樣率,聲道數等等參數自定義音頻數據.

5.record_convert

如上圖,應用程序告訴音頻隊列使用指定格式開始錄製,音頻隊列在獲取到原生的PCM數據後使用編碼器將其轉換爲AAC類型數據,而後音頻隊列通知回調函數,將轉換好的數據放入audio queue buffer中傳給回調函數.最後,回調函數拿到轉換好的AAC數據進行使用.

6.play_convert

如上圖,應用程序告訴音頻隊列播放指定的格式(AAC)的文件,音頻隊列調用回調函數從音頻文件中讀取音頻數據,回調函數將原始格式的數據傳給音頻隊列.最後,音頻隊列使用合適的解碼器將音頻數據(PCM)交給揚聲器.

音頻隊列能夠利用任何編解碼器不管是系統自帶的仍是第三方安裝的(僅Mac OS)

1.7. 生命週期

音頻隊列在建立與銷燬間的活動範圍稱爲它的聲明週期.

  • Start (AudioQueueStart): 初始化
  • Prime (AudioQueuePrime): 僅用於播放,在調用AudioQueueStart前調用它確保當有可用的音頻數據時可以當即播放.
  • Stop (AudioQueueStop): 重置音頻隊列,中止播放與錄製.
  • Pause (AudioQueuePause): 暫停錄製,播放不會影響音頻隊列中已有的數據.調用AudioQueueStart恢復.
  • Flush (AudioQueueFlush): 在音頻隊列最後一個buffer入隊時調用,確保全部的音頻數據處理完畢.
  • Reset (AudioQueueReset): 調用後會當即靜音,音頻隊列移除全部數據而且重置編解碼器與DSP狀態.

AudioQueueStop能夠選擇以同步或異步的方式中止.

  • Synchronous: 當即中止,忽略隊列中的數據
  • Asynchronous: 當隊列中全部數據被取出用完後再中止.

1.8. 參數設置

音頻隊列有一個能夠調節的設置稱爲參數,每一個參數都有一個枚舉常量做爲其鍵,一個浮點型做爲其值,該值僅用於播放.

如下有兩種方式設置參數

  • 對於每一個audio queue, 使用AudioQueueSetParameter:當即改變
  • 對於每一個audio queue buffer,使用AudioQueueEnqueueBufferWithParameters,在入隊時進行設置,播放時,此類更改將生效。

使用kAudioQueueParam_Volume能夠調節播放音量(0.0~1.0)

2. 錄製

使用Audio Queue Services進行錄製,輸出端能夠是一個文件,網絡協議傳輸,拷貝給一個對象等等.這裏僅介紹輸出到文件.

流程

  • 自定義一個結構體去管理音頻格式,狀態,文件路徑等等...
  • 使用audio queue作錄製
  • 選擇須要的每一個音頻數據的大小,若是須要還能夠生成magic cookies(元數據信息).
  • 設置自定義音頻數據格式,指定文件路徑.
  • 建立audio queue,分配audio queue buffer內存,執行入隊操做.
  • 告訴audio queue開始錄製
  • 完成時中止audio queue而且回收audio queue buffer的內存.

2.1. 使用自定義結構體管理狀態信息

第一步是自定義一個結構體管理音頻格式及狀態信息.

static const int kNumberBuffers = 3;                            // 1
struct AQRecorderState {
    AudioStreamBasicDescription  mDataFormat;                   // 2
    AudioQueueRef                mQueue;                        // 3
    AudioQueueBufferRef          mBuffers[kNumberBuffers];      // 4
    AudioFileID                  mAudioFile;                    // 5
    UInt32                       bufferByteSize;                // 6
    SInt64                       mCurrentPacket;                // 7
    bool                         mIsRunning;                    // 8
};
複製代碼
  • kNumberBuffers: 使用多少個音頻隊列數據.
  • mDataFormat: 指定音頻數據格式
  • mQueue: 應用程序建立的錄製音頻隊列.
  • mBuffers: 音頻隊列中音頻數據指針的數組
  • mAudioFile: 錄製的文件
  • bufferByteSize: 當前錄製的文件的大小(單位是bytes)
  • mCurrentPacket: 要寫入當前錄製文件的音頻數據包的索引
  • mIsRunning: 當前音頻隊列是否正在運行.

2.2. 回調函數

static void HandleInputBuffer (
    void                                *aqData,             // 1
    AudioQueueRef                       inAQ,                // 2
    AudioQueueBufferRef                 inBuffer,            // 3
    const AudioTimeStamp                *inStartTime,        // 4
    UInt32                              inNumPackets,        // 5
    const AudioStreamPacketDescription  *inPacketDesc        // 6
)
複製代碼
  • aqData: 自定義的數據,開發者能夠傳入一些咱們須要的數據供回調函數使用.注意:通常狀況下咱們須要將當前的OC類實例傳入,由於回調函數是純C語言,不能調用OC類中的屬性與方法,因此傳入OC實例以與本類中屬性方法交互.
  • inAQ: 調用回調函數的音頻隊列
  • inBuffer: 裝有音頻數據的audio queue buffer.
  • inStartTime: 當前音頻數據的時間戳.主要用於同步.
  • inNumberPacketDescriptions: 數據包描述參數.若是你正在錄製VBR格式,音頻隊列會提供此參數的值.若是錄製文件須要將其傳遞給AudioFileWritePackets函數.CBR格式不使用此參數(值爲0).
  • inPacketDescs: 音頻數據中一組packet描述.若是是VBR格式數據,若是錄製文件須要將此值傳遞給AudioFileWritePackets函數

2.3. 將數據寫入本地文件

使用AudioFileWritePackets將數據寫入音頻文件.

AudioFileWritePackets (                     // 1
    pAqData->mAudioFile,                    // 2
    false,                                  // 3
    inBuffer->mAudioDataByteSize,           // 4
    inPacketDesc,                           // 5
    pAqData->mCurrentPacket,                // 6
    &inNumPackets,                          // 7
    inBuffer->mAudioData                    // 8
);

複製代碼
  • 1.將音頻數據寫入音頻文件
  • 2.要寫入的音頻文件
  • 3.使用false表示寫入文件時不該緩存數據
  • 4.被寫入文件的大小
  • 5.一組音頻數據包的描述,如2.2中介紹,若是是CBR設置爲NULL,若是是VBR須要設置回調函數中的inPacketDesc參數.
  • 6.當前寫入的數據包的索引
  • 7.輸入(錄製)時,要寫入的數據包數。輸出(播放)時,實際寫入的數據包數
  • 8.要寫入的音頻數據.

2.4. 入隊

當音頻數據在回調函數中用完後,須要從新放回音頻隊列以便存儲新的音頻數據

AudioQueueEnqueueBuffer (                    // 1
    pAqData->mQueue,                         // 2
    inBuffer,                                // 3
    0,                                       // 4
    NULL                                     // 5
);
複製代碼
  • 1.將音頻數據放入音頻隊列
  • 2.錄製的音頻隊列
  • 3.等待入隊的音頻數據
  • 4.音頻數據包的描述信息,設置爲0由於該參數不用於錄製.
  • 5.描述音頻隊列數據的數據包描述數組。設置爲NULL由於該參數不用於錄製.

2.5. 完整的錄製回調

static void HandleInputBuffer (
    void                                 *aqData,
    AudioQueueRef                        inAQ,
    AudioQueueBufferRef                  inBuffer,
    const AudioTimeStamp                 *inStartTime,
    UInt32                               inNumPackets,
    const AudioStreamPacketDescription   *inPacketDesc
) {
    AQRecorderState *pAqData = (AQRecorderState *) aqData;               // 1
 
    if (inNumPackets == 0 &&                                             // 2
          pAqData->mDataFormat.mBytesPerPacket != 0)
       inNumPackets =
           inBuffer->mAudioDataByteSize / pAqData->mDataFormat.mBytesPerPacket;
 
    if (AudioFileWritePackets (                                          // 3
            pAqData->mAudioFile,
            false,
            inBuffer->mAudioDataByteSize,
            inPacketDesc,
            pAqData->mCurrentPacket,
            &inNumPackets,
            inBuffer->mAudioData
        ) == noErr) {
            pAqData->mCurrentPacket += inNumPackets;                     // 4
    }
   if (pAqData->mIsRunning == 0)                                         // 5
      return;
 
    AudioQueueEnqueueBuffer (                                            // 6
        pAqData->mQueue,
        inBuffer,
        0,
        NULL
    );
}
複製代碼
  • 1.用於記錄音頻隊列一些信息的結構體,裏面包含當前錄製文件的信息,狀態等等參數.
  • 2.若是音頻數據是CBR數據,計算當前數據中包含多少個音頻數據包.對於VBR數據,能夠直接從回調函數中的inNumPackets參數獲取.
  • 3.將音頻數據寫入音頻文件
  • 4.若是成功的話,須要將音頻數據包索引累加,以便下次能夠繼續錄製
  • 5.若是audio queue已經中止則返回.
  • 6.使用完的音頻隊列數據從新裝入音頻隊列.

2.6. 獲取Audio Queue Buffer大小

void DeriveBufferSize (
    AudioQueueRef                audioQueue,                  // 1
    AudioStreamBasicDescription  &ASBDescription,             // 2
    Float64                      seconds,                     // 3
    UInt32                       *outBufferSize               // 4
) {
    static const int maxBufferSize = 0x50000;                 // 5
 
    int maxPacketSize = ASBDescription.mBytesPerPacket;       // 6
    if (maxPacketSize == 0) {                                 // 7
        UInt32 maxVBRPacketSize = sizeof(maxPacketSize);
        AudioQueueGetProperty (
                audioQueue,
                kAudioQueueProperty_MaximumOutputPacketSize,
                // in Mac OS X v10.5, instead use
                //   kAudioConverterPropertyMaximumOutputPacketSize
                &maxPacketSize,
                &maxVBRPacketSize
        );
    }
 
    Float64 numBytesForTime =
        ASBDescription.mSampleRate * maxPacketSize * seconds; // 8
    *outBufferSize =
    UInt32 (numBytesForTime < maxBufferSize ?
        numBytesForTime : maxBufferSize);                     // 9
}
複製代碼
  • 1.指定的音頻隊列
  • 2.音頻隊列配置信息
  • 3.音頻數據採集的間隔(能夠經過採樣率與間隔算出每一個採集數據的大小)
  • 4.經過該參數返回計算出的音頻數據的大小
  • 5.音頻隊列數據大小的上限,以字節爲單位。在此示例中,上限設置爲320 KB。這至關於採樣速率爲96 kHz的大約5秒的立體聲,24位音頻。
  • 6.對於CBR的數據,能夠從ASBD中獲取該值大小.若是是VBR數據,ASBD中取出得值爲0.
  • 7.對於VBR數據,須要手動估算一個最大值.
  • 8.獲取音頻數據大小(字節)
  • 9.若是須要,限制音頻數據最大值.

2.7. 爲音頻文件設置magin cookie

對於一些壓縮音頻數據格式,如AAC,MPEG 4 AAC等,必須包含音頻元數據.包含該元數據信息的數據結構稱爲magic cookies.當你錄製壓縮音頻數據格式的音頻文件時,必須從audio queue中獲取元數據並將其設置給音頻文件.

注意: 咱們在錄製前與中止錄製後兩個時間點都設置一次magin cookie,由於有的編碼器須要在中止錄製後更新magin cookie.

OSStatus SetMagicCookieForFile (
    AudioQueueRef inQueue,                                      // 1
    AudioFileID   inFile                                        // 2
) {
    OSStatus result = noErr;                                    // 3
    UInt32 cookieSize;                                          // 4
 
    if (
            AudioQueueGetPropertySize (                         // 5
                inQueue,
                kAudioQueueProperty_MagicCookie,
                &cookieSize
            ) == noErr
    ) {
        char* magicCookie =
            (char *) malloc (cookieSize);                       // 6
        if (
                AudioQueueGetProperty (                         // 7
                    inQueue,
                    kAudioQueueProperty_MagicCookie,
                    magicCookie,
                    &cookieSize
                ) == noErr
        )
            result =    AudioFileSetProperty (                  // 8
                            inFile,
                            kAudioFilePropertyMagicCookieData,
                            cookieSize,
                            magicCookie
                        );
        free (magicCookie);                                     // 9
    }
    return result;                                              // 10
}
複製代碼
  • 1.錄製的音頻隊列
  • 2.準備錄製的文件
  • 3.定義一個變量記錄設置是否成功
  • 4.定義一個變量記錄magic cookie的大小
  • 5.從audio queue中獲取magic cookie的大小.
  • 6.定義一個變量記錄magic cookie的內容併爲其分配須要的內存
  • 7.從audio queue中獲取magic cookie的內容
  • 8.將獲取到的magic cookie設置到文件中.
  • 9.釋放剛纔臨時保存的magic cookie變量
  • 10.返回設置的結果

2.8.設置錄製音頻的格式.

主要關注如下參數

  • 音頻格式(PCM,AAC...)
  • 採樣率(44.1kHz, 48kHz)
  • 聲道數(單聲道,雙聲道)
  • 採樣位數(16bits)
  • 每一個音頻數據包中的幀數(線性PCM一般是1幀,壓縮數據一般比較多)
  • 音頻文件類型(CAF, AIFF...)
AQRecorderState aqData;                                       // 1
 
aqData.mDataFormat.mFormatID         = kAudioFormatLinearPCM; // 2
aqData.mDataFormat.mSampleRate       = 44100.0;               // 3
aqData.mDataFormat.mChannelsPerFrame = 2;                     // 4
aqData.mDataFormat.mBitsPerChannel   = 16;                    // 5
aqData.mDataFormat.mBytesPerPacket   =                        // 6
   aqData.mDataFormat.mBytesPerFrame =
      aqData.mDataFormat.mChannelsPerFrame * sizeof (SInt16);
aqData.mDataFormat.mFramesPerPacket  = 1;                     // 7
 
AudioFileTypeID fileType             = kAudioFileAIFFType;    // 8
aqData.mDataFormat.mFormatFlags =                             // 9
    kLinearPCMFormatFlagIsBigEndian
    | kLinearPCMFormatFlagIsSignedInteger
    | kLinearPCMFormatFlagIsPacked;

複製代碼
  • 1.建立一個存放音頻狀態信息的結構體.(結構體名字自定義)
  • 2.指定音頻格式
  • 3.指定採樣率
  • 4.指定聲道數
  • 5.指定採樣位數
  • 6.指定每一個包中的字節數
  • 7.指定每一個包中的幀數
  • 8.指定文件類型
  • 9.指定文件類型所須要的標誌

2.9. 建立錄製的Audio Queue

AudioQueueNewInput (                              // 1
    &aqData.mDataFormat,                          // 2
    HandleInputBuffer,                            // 3
    &aqData,                                      // 4
    NULL,                                         // 5
    kCFRunLoopCommonModes,                        // 6
    0,                                            // 7
    &aqData.mQueue                                // 8
);
複製代碼
  • 1.建立一個錄製音頻隊列
  • 2.指定錄製的音頻格式
  • 3.指定回調函數
  • 4.可傳入自定義的數據結構,能夠是本類的實例,能夠是記錄音頻信息的結構體
  • 5.回調函數在哪一個循環中被調用.設置爲NULL爲默認值,即回調函數所在的線程由audio queue內部控制.
  • 6.回調函數運行循環模式一般使用kCFRunLoopCommonModes.
  • 7.保留值,只能爲0.
  • 8.輸出時新分配的音頻隊列.

2.10. 獲取完整的音頻格式.

當audio queue開始工做後,它可能會產生更多音頻格式信息比咱們初始化設置時,因此咱們須要對獲取到的音頻數據作一個檢查.

UInt32 dataFormatSize = sizeof (aqData.mDataFormat);       // 1
 
AudioQueueGetProperty (                                    // 2
    aqData.mQueue,                                         // 3
    kAudioQueueProperty_StreamDescription,                 // 4
    // in Mac OS X, instead use
    //    kAudioConverterCurrentInputStreamDescription
    &aqData.mDataFormat,                                   // 5
    &dataFormatSize                                        // 6
);
複製代碼
  • 1.查詢音頻數據格式
  • 2.獲取audio queue指定屬性的值
  • 3.查詢的音頻隊列
  • 4.音頻隊列數據格式的ID
  • 5.做爲輸出,輸出完整的音頻數據格式
  • 6.在輸入時,AudioStreamBasicDescription結構的預期大小。在輸出時,實際大小。您的錄製應用程序不須要使用此值。

2.11. 建立一個音頻文件

CFURLRef audioFileURL =
    CFURLCreateFromFileSystemRepresentation (            // 1
        NULL,                                            // 2
        (const UInt8 *) filePath,                        // 3
        strlen (filePath),                               // 4
        false                                            // 5
    );
 
AudioFileCreateWithURL (                                 // 6
    audioFileURL,                                        // 7
    fileType,                                            // 8
    &aqData.mDataFormat,                                 // 9
    kAudioFileFlags_EraseFile,                           // 10
    &aqData.mAudioFile                                   // 11
);
複製代碼
  • 1.建立一個CFURL類型的對象表明錄製文件路徑
  • 2.使用NULL(kCFAllocatorDefault)使用當前默認的內存分配器
  • 3.設置文件路徑
  • 4.文件名長度
  • 5.false表示是一個文件,不是文件夾.
  • 6.建立一個新的文件或初始化一個已經存在的文件.
  • 7.音頻文件的路徑(即3中建立的)
  • 8.音頻文件類型.(CAF,AIFF...)
  • 9.ASBD
  • 10.設置該值表示若是文件已經存在則覆蓋
  • 11.表明錄製的文件.

2.12. 設置音頻隊列數據大小

使用2.6.章節中的函數設置音頻隊列數據的大小以便後續使用.

DeriveBufferSize (                               // 1
    aqData.mQueue,                               // 2
    aqData.mDataFormat,                          // 3
    0.5,                                         // 4
    &aqData.bufferByteSize                       // 5
);

複製代碼

2.13. 爲Audio Queue準備指定數量的buffer

for (int i = 0; i < kNumberBuffers; ++i) {           // 1
    AudioQueueAllocateBuffer (                       // 2
        aqData.mQueue,                               // 3
        aqData.bufferByteSize,                       // 4
        &aqData.mBuffers[i]                          // 5
    );
 
    AudioQueueEnqueueBuffer (                        // 6
        aqData.mQueue,                               // 7
        aqData.mBuffers[i],                          // 8
        0,                                           // 9
        NULL                                         // 10
    );
}
複製代碼
  • 1.通常指定3個,這裏爲一個簡單的循環,爲指定數量的buffer分配內存並進行入隊操做
  • 2.爲每一個buffer分配內存
  • 3.指定分配內存的音頻隊列
  • 4.指定分配內存的Buffer的大小(即2.12中獲取的)
  • 5.輸出一個分配好內存的buffer
  • 6.音頻隊列入隊
  • 7.將要入隊的音頻隊列
  • 8.將要入隊的音頻數據
  • 9.對於錄製此參數沒用
  • 10.對於錄製此參數沒用

2.14. 錄製音頻

aqData.mCurrentPacket = 0;                           // 1
aqData.mIsRunning = true;                            // 2
 
AudioQueueStart (                                    // 3
    aqData.mQueue,                                   // 4
    NULL                                             // 5
);
// Wait, on user interface thread, until user stops the recording
AudioQueueStop (                                     // 6
    aqData.mQueue,                                   // 7
    true                                             // 8
);
 
aqData.mIsRunning = false;                           // 9
複製代碼
  • 初始化記錄當前錄製文件packet索引爲0
  • 代表audio queue正在運行
  • 開啓一個audio queue
  • 指定開啓的audio queue
  • 設置爲NULL表示當即開始採集數據
  • 中止並重置當前音頻隊列
  • 指定中止的音頻隊列
  • true:同步中止, false: 異步中止
  • 更新音頻隊列當前工做狀態.

2.15. 錄製完成清理內存

錄製完成後,回收音頻隊列數據,關閉音頻文件.

AudioQueueDispose (                                 // 1
    aqData.mQueue,                                  // 2
    true                                            // 3
);
 
AudioFileClose (aqData.mAudioFile);                 // 4
複製代碼
  • 1.回收音頻隊列中全部資源
  • 2.指定回收的音頻隊列
  • 3.true: 同步, false:異步
  • 4.關閉錄製文件.

3. 播放

使用 Audio Queue Services播放音頻時,源數據能夠是本地文件, 內存中的對象或者其餘音頻存儲方式.本章中僅介紹經過本地文件播放.

  • 定義一個結構體管理音頻格式狀態信息等.
  • 實現一個播放回調函數
  • 設置音頻隊列數據大小
  • 打開一個音頻文件,肯定音頻數據格式
  • 建立並配置一個播放的音頻隊列
  • 爲音頻隊列數據分配內存併入隊.告訴音頻隊列開始播放.完成時,告訴音頻隊列中止.
  • 回收內存,釋放資源

3.1. 定義一個結構體管理音頻狀態

static const int kNumberBuffers = 3;                              // 1
struct AQPlayerState {
    AudioStreamBasicDescription   mDataFormat;                    // 2
    AudioQueueRef                 mQueue;                         // 3
    AudioQueueBufferRef           mBuffers[kNumberBuffers];       // 4
    AudioFileID                   mAudioFile;                     // 5
    UInt32                        bufferByteSize;                 // 6
    SInt64                        mCurrentPacket;                 // 7
    UInt32                        mNumPacketsToRead;              // 8
    AudioStreamPacketDescription  *mPacketDescs;                  // 9
    bool                          mIsRunning;                     // 10
};
複製代碼

此結構體中的數據基本與錄製時相同.

  • 1.設置音頻隊列中可複用的音頻數據個數,一般爲3
  • 2.ASBD
  • 3.播放使用的音頻隊列
  • 4.管理音頻隊列中音頻數據的數組
  • 5.播放用的音頻文件
  • 6.每一個音頻數據的大小
  • 7.當前準備播放的音頻數據包索引
  • 8.每次調用回調函數要讀取的音頻數據包的個數
  • 9.對於VBR音頻數據,表示正在播放的音頻數據包描述性數組,對於CBR音頻數據能夠設爲NULL.
  • 10.音頻隊列是否正在運行.

3.2.回調函數

做用

  • 從音頻文件中讀取指定數量的音頻數據並將其裝入音頻隊列數據.
  • 將音頻隊列數據入隊
  • 文件讀取完成後,中止音頻隊列
3.2.1. 定義回調函數
static void HandleOutputBuffer (
    void                 *aqData,                 // 1
    AudioQueueRef        inAQ,                    // 2
    AudioQueueBufferRef  inBuffer                 // 3
)

複製代碼
  • 1.同錄製,自定義的結構體或類對象,可傳入回調函數中使用,即OC類與回調函數間的通訊對象
  • 2.當前工做的音頻隊列
  • 3.經過讀取音頻文件獲取的音頻數據
3.2.2. 讀取音頻文件
AudioFileReadPackets (                        // 1
    pAqData->mAudioFile,                      // 2
    false,                                    // 3
    &numBytesReadFromFile,                    // 4
    pAqData->mPacketDescs,                    // 5
    pAqData->mCurrentPacket,                  // 6
    &numPackets,                              // 7
    inBuffer->mAudioData                      // 8
);
複製代碼
  • 1.讀取文件的函數
  • 2.要讀取的音頻文件
  • 3.false:讀取時不該緩存數據.
  • 4.做爲輸出:將從文件讀取的字節數
  • 5.做爲輸出:VBR:從音頻文件讀取到的數據包描述數組,CBR:NULL
  • 6.當前讀取到的索引值,以便下次繼續讀取
  • 7.做輸入時:從音頻文件中讀取到的音頻數據包數,做輸出時:實際讀取到的音頻數據包
  • 8.做輸出時:從音頻文件中讀取的數據
3.2.3. 入隊

讀取完音頻數據後,執行入隊操做.

AudioQueueEnqueueBuffer (                      // 1
    pAqData->mQueue,                           // 2
    inBuffer,                                  // 3
    (pAqData->mPacketDescs ? numPackets : 0),  // 4
    pAqData->mPacketDescs                      // 5
);

複製代碼
  • 4.音頻數據包數,CBR的數據使用0
  • 5.對於壓縮數據使用其數據包描述信息
3.2.4. 中止音頻隊列

若是檢查到當前音頻文件讀取完畢,應該中止音頻隊列.

if (numPackets == 0) {                          // 1
    AudioQueueStop (                            // 2
        pAqData->mQueue,                        // 3
        false                                   // 4
    );
    pAqData->mIsRunning = false;                // 5
}
複製代碼
  • 1.經過AudioFileReadPackets檢查數據包是否爲0
  • 4.true:同步, false:異步
3.2.5. 完整的回調
static void HandleOutputBuffer (
    void                *aqData,
    AudioQueueRef       inAQ,
    AudioQueueBufferRef inBuffer
) {
    AQPlayerState *pAqData = (AQPlayerState *) aqData;        // 1
    if (pAqData->mIsRunning == 0) return;                     // 2
    UInt32 numBytesReadFromFile;                              // 3
    UInt32 numPackets = pAqData->mNumPacketsToRead;           // 4
    AudioFileReadPackets (
        pAqData->mAudioFile,
        false,
        &numBytesReadFromFile,
        pAqData->mPacketDescs, 
        pAqData->mCurrentPacket,
        &numPackets,
        inBuffer->mAudioData 
    );
    if (numPackets > 0) {                                     // 5
        inBuffer->mAudioDataByteSize = numBytesReadFromFile;  // 6
       AudioQueueEnqueueBuffer ( 
            pAqData->mQueue,
            inBuffer,
            (pAqData->mPacketDescs ? numPackets : 0),
            pAqData->mPacketDescs
        );
        pAqData->mCurrentPacket += numPackets;                // 7 
    } else {
        AudioQueueStop (
            pAqData->mQueue,
            false
        );
        pAqData->mIsRunning = false; 
    }
}
複製代碼
  • 3.記錄讀取到的字節數
  • 4.記錄讀取到音頻數據包數
  • 7.累加音頻數據包,使下次觸發回調能夠接着上次內容繼續播放

3.3. 計算音頻隊列數據

咱們須要指定一個音頻隊列buffer的大小.根據計算出來的大小爲音頻隊列數據分配內存.

  • 回調函數中調用AudioFileReadPackets獲取讀取到的包數
  • 設置音頻buffer下限值,避免訪問過於頻繁.
void DeriveBufferSize (
    AudioStreamBasicDescription &ASBDesc,                            // 1
    UInt32                      maxPacketSize,                       // 2
    Float64                     seconds,                             // 3
    UInt32                      *outBufferSize,                      // 4
    UInt32                      *outNumPacketsToRead                 // 5
) {
    static const int maxBufferSize = 0x50000;                        // 6
    static const int minBufferSize = 0x4000;                         // 7
 
    if (ASBDesc.mFramesPerPacket != 0) {                             // 8
        Float64 numPacketsForTime =
            ASBDesc.mSampleRate / ASBDesc.mFramesPerPacket * seconds;
        *outBufferSize = numPacketsForTime * maxPacketSize;
    } else {                                                         // 9
        *outBufferSize =
            maxBufferSize > maxPacketSize ?
                maxBufferSize : maxPacketSize;
    }
 
    if (                                                             // 10
        *outBufferSize > maxBufferSize &&
        *outBufferSize > maxPacketSize
    )
        *outBufferSize = maxBufferSize;
    else {                                                           // 11
        if (*outBufferSize < minBufferSize)
            *outBufferSize = minBufferSize;
    }
 
    *outNumPacketsToRead = *outBufferSize / maxPacketSize;           // 12
}
複製代碼
  • 2.估算當前播放音頻文件最大數據包大小,經過調用AudioFileGetProperty查詢kAudioFilePropertyPacketSizeUpperBound屬性可得
  • 3.採樣時間,根據採樣率與採樣時間可計算出音頻數據大小
  • 4.每一個音頻數據的大小
  • 5.每次從音頻播放回調中讀取的音頻數據包數
  • 6.音頻數據包大小的上限
  • 7.音頻數據包大小的下限
  • 8.計算音頻數據包總大小
  • 9.根據最大數據包大小和您設置的上限導出合理的音頻隊列數據大小
  • 10.設置上限
  • 11.設置下限
  • 12.計算讀取到的音頻數據包數

3.4. 打開音頻文件

  • 獲取一個CFURL對象表示音頻文件路徑
  • 打開音頻文件
  • 獲取文件格式
3.4.1. 獲取一個CFURL對象表示音頻文件路徑
CFURLRef audioFileURL =
    CFURLCreateFromFileSystemRepresentation (           // 1
        NULL,                                           // 2
        (const UInt8 *) filePath,                       // 3
        strlen (filePath),                              // 4
        false                                           // 5
    );
複製代碼
  • 1.建立一個CFURL類型的對象表明錄製文件路徑
  • 2.使用NULL(kCFAllocatorDefault)使用當前默認的內存分配器
  • 3.設置文件路徑
  • 4.文件名長度
  • 5.false表示是一個文件,不是文件夾.
3.4.2. 打開音頻文件
AQPlayerState aqData;                                   // 1
 
OSStatus result =
    AudioFileOpenURL (                                  // 2
        audioFileURL,                                   // 3
        fsRdPerm,                                       // 4
        0,                                              // 5
        &aqData.mAudioFile                              // 6
    );
 
CFRelease (audioFileURL);                               // 7
複製代碼
  • 2.打開一個想要播放的音頻文件
  • 3.音頻文件路徑
  • 4.文件權限
  • 5.可選文件類型,0:不使用此參數
  • 6.做爲輸出,獲取文件對象的引用
3.4.3. 獲取文件格式
UInt32 dataFormatSize = sizeof (aqData.mDataFormat);    // 1
 
AudioFileGetProperty (                                  // 2
    aqData.mAudioFile,                                  // 3
    kAudioFilePropertyDataFormat,                       // 4
    &dataFormatSize,                                    // 5
    &aqData.mDataFormat                                 // 6
);
複製代碼
  • 5.做爲輸入:輸入時,AudioStreamBasicDescription結構體的預期大小,用於描述音頻文件的數據格式。在輸出時,實際大小。做播放時不須要使用此值。
  • 6.輸出:將文件表明的ASBD數據格式賦給該變量

3.5. 建立播放音頻隊列

AudioQueueNewOutput (                                // 1
    &aqData.mDataFormat,                             // 2
    HandleOutputBuffer,                              // 3
    &aqData,                                         // 4
    CFRunLoopGetCurrent (),                          // 5
    kCFRunLoopCommonModes,                           // 6
    0,                                               // 7
    &aqData.mQueue                                   // 8
);
複製代碼
  • 3.回調函數
  • 4.音頻隊列數據
  • 5.調用播放回調的的運行循環
  • 6.調用播放回調運行循環的模式

3.6. 設置播放音頻隊列大小

3.6.1. 設置buffer size與讀取的音頻數據包數量
UInt32 maxPacketSize;
UInt32 propertySize = sizeof (maxPacketSize);
AudioFileGetProperty (                               // 1
    aqData.mAudioFile,                               // 2
    kAudioFilePropertyPacketSizeUpperBound,          // 3
    &propertySize,                                   // 4
    &maxPacketSize                                   // 5
);
 
DeriveBufferSize (                                   // 6
    aqData.mDataFormat,                              // 7
    maxPacketSize,                                   // 8
    0.5,                                             // 9
    &aqData.bufferByteSize,                          // 10
    &aqData.mNumPacketsToRead                        // 11
);
複製代碼
3.6.2. 爲數據包描述數組分配內存
bool isFormatVBR = (                                       // 1
    aqData.mDataFormat.mBytesPerPacket == 0 ||
    aqData.mDataFormat.mFramesPerPacket == 0
);
 
if (isFormatVBR) {                                         // 2
    aqData.mPacketDescs =
      (AudioStreamPacketDescription*) malloc (
        aqData.mNumPacketsToRead * sizeof (AudioStreamPacketDescription)
      );
} else {                                                   // 3
    aqData.mPacketDescs = NULL;
}
複製代碼
  • 1.判斷音頻文件數據是VBR仍是CBR.對於VBR數據,每一個數據包中的幀數(同理每一個數據包中的字節數也是同樣)是可變的,因此此屬性爲0.
  • 2.對於VBR數據,爲數據包描述字典分配指定內存.
  • 3.對於CBR數據,不須要使用該參數,直接設爲NULL

3.7. 設置magic cookie

對於壓縮的音頻數據格式(AAC...),咱們在播放前必須爲音頻隊列設置magic cookies,即元數據信息.

UInt32 cookieSize = sizeof (UInt32);                   // 1
bool couldNotGetProperty =                             // 2
    AudioFileGetPropertyInfo (                         // 3
        aqData.mAudioFile,                             // 4
        kAudioFilePropertyMagicCookieData,             // 5
        &cookieSize,                                   // 6
        NULL                                           // 7
    );
 
if (!couldNotGetProperty && cookieSize) {              // 8
    char* magicCookie =
        (char *) malloc (cookieSize);
 
    AudioFileGetProperty (                             // 9
        aqData.mAudioFile,                             // 10
        kAudioFilePropertyMagicCookieData,             // 11
        &cookieSize,                                   // 12
        magicCookie                                    // 13
    );
 
    AudioQueueSetProperty (                            // 14
        aqData.mQueue,                                 // 15
        kAudioQueueProperty_MagicCookie,               // 16
        magicCookie,                                   // 17
        cookieSize                                     // 18
    );
 
    free (magicCookie);                                // 19
}
複製代碼
  • 1.根據UInt32估算magic cookie數據大小
  • 2.記錄是否能獲取magic cookie結果
  • 3.獲取文件中的magic cookie的大小。
  • 4.想要播放的文件
  • 5.key值,表明音頻文件的kAudioFilePropertyMagicCookieData
  • 6.做輸入時表示magic cookie估算大小,輸出時表示實際大小
  • 7.設置爲NULL表示不關心此屬性的讀寫權限
  • 8.若是文件包含magic cookie,分配內存去持有它
  • 9.獲取文件中的magic cookie
  • 12.輸入時表示文件中的magic cookie的大小
  • 13.輸出爲文件的magic cookie
  • 14.設置audio queue的函數

3.8. 分配音頻隊列數據

aqData.mCurrentPacket = 0;                                // 1
 
for (int i = 0; i < kNumberBuffers; ++i) {                // 2
    AudioQueueAllocateBuffer (                            // 3
        aqData.mQueue,                                    // 4
        aqData.bufferByteSize,                            // 5
        &aqData.mBuffers[i]                               // 6
    );
 
    HandleOutputBuffer (                                  // 7
        &aqData,                                          // 8
        aqData.mQueue,                                    // 9
        aqData.mBuffers[i]                                // 10
    );
}
複製代碼
  • 1.初始化讀取音頻數據包索引爲0
  • 7.自定義的播放音頻回調函

3.9. 設置音量

開始播放前,能夠設置音量(0~1)

Float32 gain = 1.0;                                       // 1
    // Optionally, allow user to override gain setting here
AudioQueueSetParameter (                                  // 2
    aqData.mQueue,                                        // 3
    kAudioQueueParam_Volume,                              // 4
    gain                                                  // 5
);
複製代碼

3.10. 啓動Audio Queue

aqData.mIsRunning = true;                          // 1
 
AudioQueueStart (                                  // 2
    aqData.mQueue,                                 // 3
    NULL                                           // 4
);
 
do {                                               // 5
    CFRunLoopRunInMode (                           // 6
        kCFRunLoopDefaultMode,                     // 7
        0.25,                                      // 8
        false                                      // 9
    );
} while (aqData.mIsRunning);
 
CFRunLoopRunInMode (                               // 10
    kCFRunLoopDefaultMode,
    1,
    false
);

複製代碼
  • 4.設置爲NULL表示立刻開始播放
  • 8.設置運行循環的時間是0.25秒
  • 9.使用false表示運行循環應該在指定的完整時間內繼續
  • 10.音頻隊列中止後,運行循環運行一段時間以確保當前播放的音頻隊列緩衝區有時間完成。

3.11. 清理

播放完成後應該回收音頻隊列,關閉音頻文件,釋放全部相關資源

AudioQueueDispose (                            // 1
    aqData.mQueue,                             // 2
    true                                       // 3
);
 
AudioFileClose (aqData.mAudioFile);            // 4
 
free (aqData.mPacketDescs);                    // 5
複製代碼
  • 3:true: 同步, false:異步

Apple官方文檔

相關文章
相關標籤/搜索