幾種播放音頻文件的方式(八) —— 音頻隊列服務(Audio Queue Services)之播放音頻(五)

版本記錄

版本號 時間
V1.0 2017.12.28

前言

ios系統中有很多方式可以播放音頻文件,這里我們就詳細的說明下播放音樂文件的原理和實例。感興趣的可以看我寫的上面幾篇。
1. 幾種播放音頻文件的方式(一) —— 播放本地音樂
2. 幾種播放音頻文件的方式(二) —— 音效播放
3. 幾種播放音頻文件的方式(三) —— 網絡音樂播放
4. 幾種播放音頻文件的方式(四) —— 音頻隊列服務(Audio Queue Services)(一)
5. 幾種播放音頻文件的方式(五) —— 音頻隊列服務(Audio Queue Services)簡介(二)
6. 幾種播放音頻文件的方式(六) —— 音頻隊列服務(Audio Queue Services)之關于音頻隊列(三)
7. 幾種播放音頻文件的方式(七) —— 音頻隊列服務(Audio Queue Services)之錄制音頻(四)

Playing Audio - 播放音頻

使用音頻隊列服務播放音頻時,音源可以是任何東西 - 磁盤上文件,基于軟件的音頻合成器,內存中的對象等等。本章介紹最常見的情況:播放磁盤上的文件。

注意:本章介紹了一個基于ANSI-C的播放實現,并且使用了Mac OS X Core Audio SDK中的C ++類。對于基于Objective-C的示例,請參閱iOS Dev CenterSpeakHere示例代碼。

要為您的應用程序添加播放功能,通常需要執行以下步驟:

    1. 定義一個自定義結構來管理狀態,格式和路徑信息。
    1. 編寫音頻隊列回調函數來執行實際播放。
    1. 編寫代碼來確定音頻隊列緩沖區的大小。
    1. 打開音頻文件進行播放并確定其音頻數據格式。
    1. 創建一個播放音頻隊列并將其配置為播放。
    1. 分配和入隊音頻隊列緩沖區。告訴音頻隊列開始播放。完成后,回放回調告知音頻隊列停止。
    1. 銷毀音頻隊列,釋放資源。

本章的其余部分將詳細介紹這些步驟。


Define a Custom Structure to Manage State - 定義一個自定義結構管理狀態

首先,定義一個用來管理音頻格式和音頻隊列狀態信息的自定義結構。 Listing 3-1說明了這樣一個結構:

// Listing 3-1  A custom structure for a playback audio queue

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
};

此結構中的大多數字段與用于記錄的自定義結構中的字段相同(或接近),如Define a Custom Structure to Manage State中的“錄制音頻”章節中所述。例如,mDataFormat字段在此用于保存正在播放的文件的格式。錄制時,類似字段保存正在寫入磁盤的文件的格式。

以下是這個結構中的字段的描述:

    1. 設置要使用的音頻隊列緩沖區的數量。如Audio Queue Buffers中所述,“三”通常是一個很好的數字。
    1. 表示正在播放的文件的音頻數據格式的AudioStreamBasicDescription結構(來自CoreAudioTypes.h)。該格式被mQueue字段中指定的音頻隊列使用。通過查詢音頻文件的kAudioFilePropertyDataFormat屬性來填充mDataFormat字段,如Obtaining a File’s Audio Data Format中所述。有關AudioStreamBasicDescription結構的詳細信息,請參閱Core Audio Data Types Reference
    1. 由應用程序創建的播放音頻隊列。
    1. 一個數組,指向由音頻隊列管理的音頻隊列緩沖區的指針。
    1. 音頻文件對象,表示您的程序播放的音頻文件。
    1. 每個音頻隊列緩沖區的大小(以字節為單位)。在創建音頻隊列之后并在啟動之前,在DeriveBufferSize函數的這些示例中計算此值。請參閱 Write a Function to Derive Playback Audio Queue Buffer Size
    1. 從音頻文件播放下一個數據包的數據包索引。
    1. 每次調用音頻隊列的回放回調時讀取的數據包數量。與bufferByteSize字段類似,在創建音頻隊列之后并在啟動之前,在DeriveBufferSize函數的這些示例中計算此值。
    1. 對于VBR音頻數據,正在播放的文件的數據包描述數組。對于CBR數據,該字段的值為NULL。
    1. 指示音頻隊列是否正在運行的布爾值。

Write a Playback Audio Queue Callback - 編寫播放音頻隊列回調

接下來,寫一個回放音頻隊列回調函數。 這個回調有三個主要的事情:

  • 從音頻文件讀取指定數量的數據并將其放入音頻隊列緩沖區
  • 將音頻隊列緩沖區排入緩沖隊列
  • 當沒有更多的數據要從音頻文件讀取,告訴音頻隊列停止

本部分顯示了一個示例回調聲明,分別描述了這些任務,并最終呈現整個回放回調。 有關回放回調角色的說明,請參閱圖Figure 1-4

1. The Playback Audio Queue Callback Declaration - 播放音頻隊列回調函數聲明

Listing 3-2顯示了一個播放音頻隊列回調函數的示例聲明,聲明為AudioQueue.h頭文件中的AudioQueueOutputCallback

// Listing 3-2  The playback audio queue callback declaration

static void HandleOutputBuffer (
    void                 *aqData,                 // 1
    AudioQueueRef        inAQ,                    // 2
    AudioQueueBufferRef  inBuffer                 // 3
)

以下是這段代碼的工作原理:

    1. 通常,aqData是包含音頻隊列狀態信息的自定義結構,如Define a Custom Structure to Manage State所述。
    1. 擁有此回調的音頻隊列。
    1. 一個音頻隊列緩沖區,回調將通過讀取音頻文件來填充數據。

2. Reading From a File into an Audio Queue Buffer - 從文件中讀取數據到音頻隊列緩沖區

回放音頻隊列回調的第一個動作是從音頻文件讀取數據并將其放入音頻隊列緩沖區。Listing 3-3顯示了如何做到這一點。

// Listing 3-3  Reading from an audio file into an audio queue buffer

AudioFileReadPackets (                        // 1
    pAqData->mAudioFile,                      // 2
    false,                                    // 3
    &numBytesReadFromFile,                    // 4
    pAqData->mPacketDescs,                    // 5
    pAqData->mCurrentPacket,                  // 6
    &numPackets,                              // 7
    inBuffer->mAudioData                      // 8
);

以下是這段代碼的工作原理:

    1. AudioFile.h頭文件中聲明的AudioFileReadPackets函數從音頻文件讀取數據并將其放入緩沖區。
    1. 要從中讀取的音頻文件。
    1. 使用值為false來指示函數在讀取時不應該緩存數據。
    1. 輸出時,從音頻文件中讀取的音頻數據的字節數。
    1. 在輸出上,從音頻文件中讀取數據的數據包描述數組。 對于CBR數據,此參數的輸入值為NULL。
    1. 從音頻文件中讀取的第一個數據包數據包索引。
    1. 輸入時,從音頻文件中讀取的數據包數量。 輸出時,實際讀取的數據包數量。
    1. 輸出時,填充的音頻隊列緩沖區包含從音頻文件中讀取的數據。

3. Enqueuing an Audio Queue Buffer - 聲頻隊列緩沖入隊

現在,數據已經從音頻文件中讀取并放入音頻隊列緩沖區,回調將緩沖區排入隊列中,如Listing 3-4所示。 一旦進入緩沖區隊列,緩沖區中的音頻數據就可供音頻隊列發送到輸出設備。

// Listing 3-4  Enqueuing an audio queue buffer after reading from disk

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

以下是這段代碼的工作原理:

    1. AudioQueueEnqueueBuffer函數將音頻隊列緩沖區添加到緩沖區隊列中。
    1. 擁有緩沖隊列buffer queue的音頻隊列audio queue
    1. 要排隊的音頻隊列緩沖區
    1. 音頻隊列緩沖區數據中表示的數據包數量。 對于不使用數據包描述的CBR數據,使用0。
    1. 對于使用數據包描述的壓縮音頻數據格式,緩沖區中數據包的數據包描述。

4. Stopping an Audio Queue - 停止音頻隊列

你回調的最后一件事是檢查是否沒有更多的數據從你正在播放的音頻文件中讀取。 一旦發現文件結束,你的回調告訴播放音頻隊列停止。 Listing 3-5說明了這一點。

// Listing 3-5  Stopping an audio queue

if (numPackets == 0) {                          // 1
    AudioQueueStop (                            // 2
        pAqData->mQueue,                        // 3
        false                                   // 4
    );
    pAqData->mIsRunning = false;                // 5
}

以下是這段代碼的工作原理:

    1. 檢查AudioFileReadPackets函數讀取的數據包的數量是否為0。
    1. AudioQueueStop函數停止音頻隊列。
    1. 要停止的音頻隊列。
    1. 當所有排隊的緩沖區都被播放時,異步停止音頻隊列。 請參閱Audio Queue Control and State
    1. 在自定義結構中設置一個標志來指示播放完成。

5. A Full Playback Audio Queue Callback - 播放音頻隊列回調函數完整版

Listing 3-6顯示了完整播放音頻隊列回調的基本版本。 與本文檔中的其他代碼示例一樣,此列表不包括錯誤處理。

// Listing 3-6  A playback audio queue callback function

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; 
    }
}

以下是這段代碼的工作原理:

    1. 實例化時提供給音頻隊列的自定義數據,包括表示要播放的文件的音頻文件對象(類型AudioFileID)以及各種狀態數據。 請參閱 Define a Custom Structure to Manage State
    1. 如果音頻隊列停止,立即返回。
    1. 保存正在播放的文件中讀取的音頻數據字節數的變量。
    1. 使用要從正在播放的文件中讀取的數據包數初始化numPackets變量。
    1. 測試是否從文件中檢索到某些音頻數據。 如果是,排隊新填充的緩沖區。 如果不是,則停止音頻隊列。
    1. 告訴音頻隊列緩沖區結構讀取數據的字節數。
    1. 根據讀取的數據包數量遞增數據包索引。

Write a Function to Derive Playback Audio Queue Buffer Size - 編寫函數獲取播放音頻隊列緩沖區的大小

音頻隊列服務期望您的應用程序指定您使用的音頻隊列緩沖區的大小。 Listing 3-7顯示了一種方法。 它產生足夠大的緩沖區大小來保存給定持續時間的音頻數據。

在創建一個回放音頻隊列之后,您將在您的應用程序中調用此DeriveBufferSize函數,作為要求音頻隊列分配緩沖區的先決條件。 請參閱Set Sizes for a Playback Audio Queue

與你在Write a Function to Derive Recording Audio Queue Buffer Size類似的函數相比,這里的代碼做了兩個額外的事情, 對于回放同樣如此:

    1. 每次您的回調調用AudioFileReadPackets函數時,要讀取數據包的數量
    1. 設置緩沖區大小的下限,以避免過度頻繁的磁盤訪問

這里的計算考慮了您從磁盤讀取的音頻數據格式。 格式包括可能影響緩沖區大小的所有因素,例如音頻通道的數量。

// Listing 3-7  Deriving a playback audio queue buffer size

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
}

以下是這段代碼的工作原理:

    1. 音頻隊列的AudioStreamBasicDescription結構。
    1. 您正在播放的音頻文件中數據的估計最大數據包大小。您可以通過調用AudioFileGetProperty函數(在AudioFile.h頭文件中聲明)使用屬性ID kAudioFilePropertyPacketSizeUpperBound來確定此值。請參閱Set Sizes for a Playback Audio Queue
    1. 按照音頻的秒數指定每個音頻隊列緩沖區的大小。
    1. 輸出時,每個音頻隊列緩沖區的大小(以字節為單位)。
    1. 輸出時,每次調用回放音頻隊列回調時從文件中讀取的音頻數據包的數量。
    1. 音頻隊列緩沖區大小的上限(以字節為單位)。在這個例子中,上限設置為320 KB。這對應于大約五秒鐘的立體聲,采樣率為96kHz的24位音頻。
    1. 音頻隊列緩沖區大小的下限,以字節為單位。在這個例子中,下限設置為16 KB。
    1. 對于定義每個數據包固定數量幀的音頻數據格式,獲取音頻隊列緩沖區大小。
    1. 對于沒有為每個數據包定義固定數量幀的音頻數據格式,根據最大數據包大小和您設置的上限獲取合理的音頻隊列緩沖區大小。
    1. 如果導出的緩沖區大小高于您設置的上限,則根據估計的最大數據包大小調整邊界考慮。
    1. 如果導出的緩沖區大小低于您設置的下限,則將其調整到界限。
    1. 計算每次調用回調時從音頻文件中讀取的數據包數量。

Open an Audio File for Playback - 打開用于播放的音頻文件

現在您打開一個音頻文件進行播放,使用以下三個步驟:

    1. 獲取表示要播放的音頻文件的CFURL對象。
    1. 打開文件。
    1. 獲取文件的音頻數據格式

1. Obtaining a CFURL Object for an Audio File - 從音頻文件中獲取CFURL對象

Listing 3-8演示了如何獲取要播放的音頻文件的CFURL對象。 在下一步中使用CFURL對象,打開文件。

// Listing 3-8  Obtaining a CFURL object for an audio file

CFURLRef audioFileURL =
    CFURLCreateFromFileSystemRepresentation (           // 1
        NULL,                                           // 2
        (const UInt8 *) filePath,                       // 3
        strlen (filePath),                              // 4
        false                                           // 5
    );

以下是這段代碼的工作原理:

    1. CFURL.h頭文件中聲明的CFURLCreateFromFileSystemRepresentation函數創建一個代表要播放文件的CFURL對象。
    1. 使用NULL(或kCFAllocatorDefault)來使用當前的默認內存分配器。
    1. 您要轉換為CFURL對象的文件系統路徑。 在生產代碼中,通常會從用戶獲取filePath的值。
    1. 文件系統路徑中的字節數。
    1. 值為false表示filePath表示文件,而不是文件夾directory

2. Opening an Audio File - 打開音頻文件

Listing 3-9演示了如何打開一個音頻文件進行播放。

// Listing 3-9  Opening an audio file for playback

AQPlayerState aqData;                                   // 1
 
OSStatus result =
    AudioFileOpenURL (                                  // 2
        audioFileURL,                                   // 3
        fsRdPerm,                                       // 4
        0,                                              // 5
        &aqData.mAudioFile                              // 6
    );
 
CFRelease (audioFileURL);                               // 7

以下是這段代碼的工作原理:

    1. 創建AQPlayerState自定義結構的實例(請參閱Define a Custom Structure to Manage State)。 當您打開音頻文件進行播放時,您可以使用此實例作為放置表示音頻文件的音頻文件對象(AudioFileID類型)的位置。
    1. AudioFile.h頭文件中聲明的AudioFileOpenURL函數打開你想要播放的文件。
    1. 對要播放的文件的引用。
    1. 您想要與您正在播放的文件一起使用的文件權限。 可用權限在文件管理器的File Access Permission Constants枚舉中定義。 在這個例子中你要求讀取文件的權限。
    1. 一個可選的文件類型提示。 此處的值為0表示該示例不使用此功能。
    1. 在輸出時,對音頻文件的引用被放置在自定義結構的mAudioFile字段中。
    1. 釋放在步驟1中創建的CFURL對象。

3. Obtaining a File’s Audio Data Format - 獲取文件音頻數據格式

Listing 3-10顯示了如何獲取文件的音頻數據格式

Listing 3-10  Obtaining a file’s audio data format

UInt32 dataFormatSize = sizeof (aqData.mDataFormat);    // 1
 
AudioFileGetProperty (                                  // 2
    aqData.mAudioFile,                                  // 3
    kAudioFilePropertyDataFormat,                       // 4
    &dataFormatSize,                                    // 5
    &aqData.mDataFormat                                 // 6
);

以下是這段代碼的工作原理:

    1. 獲取預期的屬性值大小,用于查詢音頻文件的音頻數據格式。
    1. AudioFile.h頭文件中聲明的AudioFileGetProperty函數獲取音頻文件中指定屬性的值。
    1. 音頻文件對象(類型為AudioFileID),代表要獲取其音頻數據格式的文件。
    1. 用于獲取音頻文件的數據格式的值的屬性ID。
    1. 輸入時,描述音頻文件數據格式的AudioStreamBasicDescription結構的預期大小。 在輸出上,實際的大小。 您的回放應用程序不需要使用此值。
    1. 在輸出時,以音頻文件的形式從AudioStreamBasicDescription結構中獲得完整的音頻數據格式。 該行將文件的音頻數據格式存儲到音頻隊列的自定義結構中,以將其應用于音頻隊列。

Create a Playback Audio Queue - 創建播放音頻隊列

Listing 3-11顯示了如何創建一個回放音頻隊列。 請注意,AudioQueueNewOutput函數使用前面步驟中配置的自定義結構和回調,以及要播放的文件的音頻數據格式。

// Listing 3-11  Creating a playback audio queue

AudioQueueNewOutput (                                // 1
    &aqData.mDataFormat,                             // 2
    HandleOutputBuffer,                              // 3
    &aqData,                                         // 4
    CFRunLoopGetCurrent (),                          // 5
    kCFRunLoopCommonModes,                           // 6
    0,                                               // 7
    &aqData.mQueue                                   // 8
);

以下是這段代碼的工作原理:

    1. AudioQueueNewOutput函數創建一個新的播放音頻隊列。
    1. 音頻隊列正在設置播放的文件的音頻數據格式。 請參閱 Obtaining a File’s Audio Data Format
    1. 與回放音頻隊列一起使用的回調函數。 請參閱Write a Playback Audio Queue Callback
    1. 播放音頻隊列的自定義數據結構。 請參閱Define a Custom Structure to Manage State
    1. 當前運行循環,以及將調用音頻隊列回放回調的循環。
    1. 可以調用回調的運行循環模式。 通常,在這里使用kCFRunLoopCommonModes常量。
    1. 保留。 必須為0。
    1. 輸出時,新分配的播放音頻隊列。

Set Sizes for a Playback Audio Queue - 設置回放音頻隊列的大小

接下來,您為播放音頻隊列設置一些大小。 在為音頻隊列分配緩沖區之前以及在開始讀取音頻文件之前,請使用這些大小。

本節中的代碼清單顯示如何設置:

  • 音頻隊列緩沖區大小
  • 每次調用回放音頻隊列回調時要讀取的數據包數量
  • 數組大小,用于保存一個緩沖區的音頻數據的數據包描述

1. Setting Buffer Size and Number of Packets to Read - 設置緩存大小和要讀取的包的數量

Listing 3-12演示了如何使用之前編寫的DeriveBufferSize函數(請參閱 Write a Function to Derive Playback Audio Queue Buffer Size)。 此處的目標是為每個音頻隊列緩沖區設置一個以字節為單位的大小,并確定每次調用回放音頻隊列回調時要讀取的數據包數量。

此代碼使用保守估計的最大數據包大小,Core Audio通過kAudioFilePropertyPacketSizeUpperBound屬性提供。 在大多數情況下,與比花費時間讀取整個音頻文件來獲得實際的最大數據包大小相比,最好使用這種技術 - 這是近似而快速的 。

// Listing 3-12  Setting playback audio queue buffer size and number of packets to read

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
);

以下是這段代碼的工作原理:

    1. AudioFile.h頭文件中聲明的AudioFileGetProperty函數獲取音頻文件的指定屬性的值。在這里,你用它來獲得一個保守的上限,以字節為單位,你想播放的文件中的音頻數據包的大小。
    1. 表示要播放的文件的音頻文件對象(類型為AudioFileID)。請參閱打Opening an Audio File
    1. 用于獲取音頻文件中數據包大小保守上限的屬性ID。
    1. 在輸出上,kAudioFilePropertyPacketSizeUpperBound屬性的大小(以字節為單位)。
    1. 在輸出上,對于要播放的文件,數據包大小的保守上限(以字節為單位)。
    1. Write a Function to Derive Playback Audio Queue Buffer Size中描述的DeriveBufferSize函數設置每次調用回放音頻隊列回調時要讀取的緩沖區大小和數據包的數量。
    1. 您要播放的文件的音頻數據格式。請參閱Obtaining a File’s Audio Data Format
    1. 音頻文件中估計的最大數據包大小,來自此列表的第5行。
    1. 每個音頻隊列緩沖區應該容納的音頻的秒數。半秒鐘,如這里設置,通常是一個不錯的選擇。
    1. 輸出時,每個音頻隊列緩沖區的大小(以字節為單位)。該值放置在音頻隊列的自定義結構中。
    1. 輸出時,每次調用播放音頻隊列回調時要讀取的數據包數量。該值也放置在音頻隊列的自定義結構中。

2. Allocating Memory for a Packet Descriptions Array - 為包的描述數組分配內存

現在,您為數組分配內存,以保存一個緩沖區的音頻數據的數據包描述。 恒定比特率數據不使用數據包描述,因此Listing 3-13中的CBR情況步驟3非常簡單。

// Listing 3-13  Allocating memory for a packet descriptions array

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數據中,每個字節數據包或幀每個數據包值中的一個或兩個是可變的,因此在音頻隊列的AudioStreamBasicDescription結構中將被列為0。
    1. 對于包含VBR數據的音頻文件,為數據包描述數組分配內存。 根據每次調用回放回調時要讀取的音頻數據包的數量來計算所需的內存。 請參閱Setting Buffer Size and Number of Packets to Read
    1. 對于包含CBR數據(如線性PCM)的音頻文件,音頻隊列不使用數據包描述數組。

Set a Magic Cookie for a Playback Audio Queue - 為播放音頻隊列設置Magic Cookie

某些壓縮音頻格式(如MPEG 4 AAC)利用結構來包含音頻元數據。 這些結構被稱為Magic Cookie。 當您使用音頻隊列服務以這種格式播放文件時,您將從音頻文件中獲取Magic Cookie,并在開始播放之前將其添加到音頻隊列中。

Listing 3-14展示了如何從一個文件中獲得一個Magic Cookie并將其應用到一個音頻隊列中。 開始播放之前,您的代碼會調用此函數。

// Listing 3-14  Setting a magic cookie for a playback audio queue

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. 為magic cookie數據設置估計大小。
    1. 捕獲AudioFileGetPropertyInfo函數的結果。如果成功,則此函數返回NoErr的值,相當于布爾值false。
    1. AudioFile.h頭文件中聲明的AudioFileGetPropertyInfo函數獲取指定屬性值的大小。您可以使用它來設置保存屬性值的變量的大小。
    1. 音頻文件對象(類型為AudioFileID),表示要播放的音頻文件。
    1. 屬性ID代表音頻文件的magic cookie數據。
    1. 在輸入時,magic cookie數據的估計大小。在輸出上,實際的大小。
    1. 使用NULL來表示您不關心屬性的讀/寫訪問權限。
    1. 如果音頻文件包含一個magic cookie,分配內存來持有它。
    1. AudioFile.h頭文件中聲明的AudioFileGetProperty函數獲取指定屬性的值。在這種情況下,它會獲取音頻文件的magic cookie。
    1. 音頻文件對象(類型為AudioFileID),表示您要播放的音頻文件,以及您獲取的magic cookie。
    1. 表示音頻文件magic cookie數據的屬性ID。
    1. 在輸入上,使用AudioFileGetPropertyInfo函數獲得的magicCookie變量的大小。在輸出上,根據寫入到magicCookie變量的字節數來計算magic cookie的實際大小。
    1. 輸出時,音頻文件的magic cookie。
    1. AudioQueueSetProperty函數在音頻隊列中設置一個屬性。在這種情況下,它為音頻隊列設置一個magic cookie,匹配要播放的音頻文件中的magic cookie。
    1. 您要為其設置magic cookie的音頻隊列。
    1. 屬性ID代表音頻隊列的magic cookie。
    1. 您要播放的音頻文件中的magic cookie。
    1. magic cookie的大小,以字節為單位。
    1. 釋放為magic cookie分配的內存。

Allocate and Prime Audio Queue Buffers - 分配和填充音頻隊列緩沖區

現在您可以詢問您創建的音頻隊列(在Create a Playback Audio Queue中)以準備一組音頻隊列緩沖區。 Listing 3-15演示了如何做到這一點。

// Listing 3-15  Allocating and priming audio queue buffers for playback

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),它將從音頻文件的開始處開始。
    1. 分配和填充一組音頻隊列緩沖區。 (您可以在Define a Custom Structure to Manage State。將此編號kNumberBuffers設置為3)。
    1. AudioQueueAllocateBuffer函數通過為其分配內存來創建音頻隊列緩沖區。
    1. 正在分配音頻隊列緩沖區的音頻隊列。
    1. 新的音頻隊列緩沖區的大小(以字節為單位)。
    1. 在輸出上,將新的音頻隊列緩沖區添加到自定義結構中的mBuffers數組中。
    1. HandleOutputBuffer函數是你寫的回放音頻隊列回調函數。 請參閱 Write a Playback Audio Queue Callback
    1. 音頻隊列的自定義結構。
    1. 您正在調用的回調的音頻隊列。
    1. 您傳遞給音頻隊列回調的音頻隊列緩沖區。

Set an Audio Queue’s Playback Gain - 設置音頻隊列的播放增益

在您告訴音頻隊列開始播放之前,您需要通過音頻隊列參數機制設置其增益。 Listing 3-16顯示了如何做到這一點。 有關參數機制的更多信息,請參閱Audio Queue Parameters

// Listing 3-16  Setting an audio queue’s playback gain

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

以下是這段代碼的工作原理:

    1. 在0(用于靜音)和1(用于單位增益)之間設置與音頻隊列一起使用的增益。
    1. AudioQueueSetParameter函數設置音頻隊列的參數值。
    1. 您正在設置參數的音頻隊列。
    1. 您正在設置的參數的ID。 kAudioQueueParam_Volume常量讓您設置音頻隊列的增益。
    1. 您正在應用到音頻隊列的增益設置。

Start and Run an Audio Queue - 開始和運行音頻隊列

所有前面的代碼都說明了播放文件的過程。 這包括啟動音頻隊列并在文件播放時保持運行循環,如Listing 3-17所示

// Listing 3-17  Starting and running an 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
);

以下是這段代碼的工作原理:

    1. 在自定義結構中設置一個標志來指示音頻隊列正在運行。
    1. AudioQueueStart函數在其自己的線程上啟動音頻隊列。
    1. 音頻隊列開始。
    1. 使用NULL來指示音頻隊列應該立即開始播放。
    1. 定期輪詢自定義結構的mIsRunning字段,以檢查音頻隊列是否已停止。
    1. CFRunLoopRunInMode函數運行包含音頻隊列線程的運行循環。
    1. 使用運行循環的默認模式。
    1. 將運行循環的運行時間設置為0.25秒。
    1. 使用false來指示運行循環應該持續指定的全部時間。
    1. 音頻隊列停止后,再運行一次運行循環,以確保當前正在播放的音頻隊列緩沖區有時間完成。

Clean Up After Playing - 播放完后的清理

當您完成播放文件時,請釋放音頻隊列,關閉音頻文件并釋放剩余的資源。 Listing 3-18說明了這些步驟。

// Listing 3-18  Cleaning up after playing an audio file

AudioQueueDispose (                            // 1
    aqData.mQueue,                             // 2
    true                                       // 3
);
 
AudioFileClose (aqData.mAudioFile);            // 4
 
free (aqData.mPacketDescs);                    // 5

以下是這段代碼的工作原理:

    1. AudioQueueDispose函數銷毀音頻隊列及其所有資源,包括其緩沖區。
    1. 您想要處理的音頻隊列。
    1. 使用true來同步銷毀音頻隊列。
    1. 關閉播放的音頻文件。 AudioFileClose函數在AudioFile.h頭文件中聲明。
    1. 釋放用來保存數據包描述的內存。

后記

未完,待續~~~

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容