Audio Queue錄製 播放原理

小東邪發表於2019-05-03

閱讀前提:

  • C語言基礎
  • 音視訊基礎
  • Core Audio基本資料結構
  • Audio Session

Audio Queue Services是官方推薦的方式以一種直接的,低開銷的方式在iOS與Mac OS X中完成錄製與播放的操作.不像上層的API,它可以通過回撥拿到音訊幀資料,以完成更加精細的操作.

使用場景:

比上層API而言,可以直接獲取每一幀音訊資料,因此可以對音訊幀做一些需要的處理. 但是無法對聲音做一些更加精細的處理,如回聲消除,混音,降噪等等,如果需要做更底層的操作,需要使用Audio Unit.

Overview

Audio Queue Service是Core Audio的Audio Toolbox框架中的基於C語言的一套介面.

Audio Queue Services是一套高階的API. 它不僅可以在無需瞭解硬體的基礎上使程式與音訊硬體(麥克風,揚聲器等)之間完成互動,也在無需瞭解編解碼器的原理情況下讓我們使用複雜的編解碼器.

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

支援以下格式

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

注意: Audio Queue Services是一套純C的介面,所以基礎的C,C++需要有一定了解.

1. Audio Queues概述

在iOS, Mac OS X中audio queue是一個軟體層面的物件,可以用來做錄製與播放操作.使用AudioQueueRef代表其資料結構.

作用

  • 連線音訊硬體
  • 管理相關模組記憶體
  • 使用編解碼器
  • 調解錄製與播放

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官方文件

相關文章