Audio Queue 初心者體驗

簡述

Audio Queue 是在Core Audio層級比較高階的API,可以用來錄音及播放音樂,也可以用codec去解析壓縮過的音檔,根據官方文檔知道支援以下格式:

  • Linear PCM.
  • 任何Apple平台支援的音檔格式
  • 任何使用者自己提供的解碼器(codec)

基本架構

  • buffer用來讓audio data暫存
  • buffer queue用來處理buffer的佇列
  • audio queue callback 一些callback讓你去設定及使用

不論是錄音或是播放都是一樣的架構,只是差別在inputoutput的不同而已。

運作方式

Audio Queue從名字上面就可以了解他是一個audio的佇列,一開始先透過AudioQueueAllocateBuffer產生多個AudioQueueBufferRef,這些buffer是用來儲存要播放的音源資料,透過AudioQueue去管理,當AudioQueueDispose的時候也一併被釋放,要播放或是錄音時會透過AudioQueueEnqueueBuffer將buffer讀進queue之中,當buffer使用完之後會重新填滿再enqueue進去,一直循環到播放結束為止。

不免俗放一下Apple 解釋播放音樂的流程圖:

實作流程

整個播放步驟如下:

  1. 定義自己的player結構,用來儲存音源格式、路徑、狀態等等。
  2. 實作AudioQueue播放時候的callback
  3. 計算一個buffer所需的最佳的小
  4. 讀取檔案取得音源資訊
  5. 建立一個AudioQueue準備播放
  6. 配置buffer並排進queue中,設定相關參數後就可以呼叫AudioStart開始播放。
  7. 使用完之後記得AudioDispose釋放記憶體。

其實整個過程是非常清楚,但是因為Apple文件非常的片段,所以在沒有足夠相關背景知識下很難去拼湊完整的程式碼,而且在實作之前建議先讀取Core Audio的Overview,雖然內容非常的冗長,不過因為裡面非常詳細的解釋整個Core Audio的機制,了解後再去撰寫程式會比較知道方向。

首先按照步驟來,我們先定自己的manage state:

// 從文件中可知道建議最佳Buffer數量為3
static const int kNumberOfBuffers = 3;

// 定義自己的strct
typedef struct {
    
   // 檔案格式的描述
 AudioStreamBasicDescription mDataFormat;
    
    // Audio Queue
 AudioQueueRef mQueue;
    
    // 用來暫存的buffer
 AudioQueueBufferRef mBuffers[kNumberOfBuffers];
    
    // 檔案來源
 AudioFileID mAudioFile;
    
    // buffer 所需大小
 UInt32 bufferByteSize;
    
    // 目前讀取到的packet數量
 SInt64 mCurrentPacket;
    
    // 總共的packet
 UInt32 mNumberPacketToRead;
    
    // 音檔packet的描述
 AudioStreamPacketDescription *mPacketDecription;
    
    // 用來判斷是否正在播放
 bool mIsRunning;

} MCAudioPlayerState;

這邊是用一個struct去儲存,是因為參考apple文件去實作,不然網路上有許多範例是直接變成Class的成員變數存取。第二步驟就是去實作AudioQueueNewOutputAudioQueueOutputCallback,也就是要處理output出來的buffer:

static void HandleOutputBuffer(void *inAqData, AudioQueueRef inQueue, AudioQueueBufferRef inBuffer)
{
    MCAudioPlayerState *aqData = inAqData;
    if (aqData->mIsRunning ==0) {
        return;
    }

   // 從檔案讀取出需要播放的packet數量以及使用多少byte
 UInt32 numberBytesReadFromFile;
    UInt32 numberPackets = aqData->mNumberPacketToRead;
    AudioFileReadPackets(aqData->mAudioFile, false, &numberBytesReadFromFile, aqData->mPacketDecription, aqData->mCurrentPacket, &numberPackets, inBuffer->mAudioData);

    if (numberPackets>0) {
       // 設定buffer所使用的byte大小
     inBuffer->mAudioDataByteSize = numberBytesReadFromFile;
        
        // 將buffer放進queue之中
     AudioQueueEnqueueBuffer(aqData->mQueue, inBuffer, (aqData->mPacketDecription ? numberPackets : 0), aqData->mPacketDecription);
        
        // 將讀取packet位置往前移
     aqData->mCurrentPacket += numberPackets;
    }
    else {
       // 如果沒有packet要讀取,代表已經讀完檔案
     AudioQueueStop(aqData->mQueue, false);
        aqData->mIsRunning = false;
    }
}

上面這個步驟是在處理從mAudioFile中取得還有多少packet要讀取,AudioFileReadPackets會把資料放進buffer中的mAudioData裡面,接著設定buffer大小就可以排進queue之中。如果沒有多餘的packet要讀取的話,就將queue關閉。 下一步是要來處理一下buffer大小:

void DeriveBufferSize(AudioStreamBasicDescription asbd, UInt32 maxPacketSize, Float64 seconds, UInt32 *outputBufferSize, UInt32 *outputNumberOfPacketToRead)
{
    static const int maxBufferSize = 0x50000;    //320k
 static const int minBufferSize = 0x4000;     //64k

   // 如果有取得到frame資訊,就去計算一段時間內(seconds)需要取得多少packet
 if (asbd.mFramesPerPacket!=0) {
        Float64 numberPacketForTime = asbd.mSampleRate / asbd.mFramesPerPacket * seconds;
        *outputBufferSize = numberPacketForTime * maxPacketSize;
    }
    else {
       // 如果沒有就取最大packet或是buffer的size
     *outputBufferSize = MAX(maxBufferSize, maxPacketSize);
    }

   // 限制buffer size在定義的range裡面
 if (*outputBufferSize > maxBufferSize && *outputBufferSize > maxPacketSize) {
        *outputBufferSize = maxBufferSize;
    }
    else {
        if (*outputBufferSize < minBufferSize) {
            *outputBufferSize = minBufferSize;
        }
    }

    *outputNumberOfPacketToRead = *outputBufferSize / maxPacketSize;
}

透過DeriveBufferSize去初始化packet數量,這邊比較需要瞭解到packetsample rate以及frame之間的關係,否則不知道在計算什麼數值,目前只了解到buffer太小的話會播不出聲音,應該有一個最佳的範圍但還沒有理解這麼深。
到這步驟我們已經可以開始撰寫讀取檔案的部分,還有取得一些檔案基本資訊,假設我們在自定義的- (instancetype)initWithContentsOfFileURL:(NSURL *)inFileUR裡面開始初始化audioPlayer

- (instancetype)initWithContentsOfFileURL:(NSURL *)inFileURL
{
    self = [super init];
    if (self) {
        
        // 開啟檔案
     CFURLRef url = (__bridge CFURLRef)inFileURL;
        OSStatus status = AudioFileOpenURL(url, kAudioFileReadPermission, 0, &aqData.mAudioFile);
        if (status == noErr) {
            // 透過AudioFileGetProperty從檔案中獲得音檔資訊
            UInt32 dataFormat = sizeof(aqData.mDataFormat);
            AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyDataFormat, &dataFormat, &aqData.mDataFormat);
            
            // 建立Audio Queue
            AudioQueueNewOutput(&aqData.mDataFormat, HandleOutputBuffer, &aqData, CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &aqData.mQueue)

            // 取出最大的packet size計算buffer會需要
            UInt32 maxPacketSize;
            UInt32 propertySize = sizeof(maxPacketSize);
            AudioFileGetProperty(aqData.mAudioFile, kAudioFilePropertyPacketSizeUpperBound, &propertySize, &maxPacketSize)

            // 計算需要讀取的packet
            DeriveBufferSize(aqData.mDataFormat, maxPacketSize, 0.5, &aqData.bufferByteSize, &aqData.mNumberPacketToRead);

            // 如果是VBR的話會有packet description
            BOOL isFormatVBR = aqData.mDataFormat.mBytesPerPacket == 0 || aqData.mDataFormat.mFramesPerPacket == 0;
            if (isFormatVBR) {
                aqData.mPacketDecription = (AudioStreamPacketDescription *) malloc(aqData.mNumberPacketToRead * sizeof(AudioStreamPacketDescription));
            }
            else {  // CBR不需要設定
                aqData.mPacketDecription = NULL;
            }

            // 調整音量
            Float32 gain = 1.0;
            AudioQueueSetParameter(aqData.mQueue, kAudioQueueParam_Volume, gain);

            // 初始化設定
            aqData.mCurrentPacket = 0;
            aqData.mIsRunning = true;

            // 這邊就是開始初始化buffer並且丟入我們剛剛寫的callback之中取讀取資料
            for (int i = 0; i<kNumberOfBuffers; i++) {
                AudioQueueAllocateBuffer(aqData.mQueue, aqData.bufferByteSize, &aqData.mBuffers[i]);
                HandleOutputBuffer(&aqData, aqData.mQueue, aqData.mBuffers[i]);
            }

            // 萬事俱備後就可以開始播放
            checkStatus(AudioQueuePrime(aqData.mQueue, kNumberOfBuffers, NULL));
            checkStatus(AudioQueueStart(aqData.mQueue, NULL));
        }
    }
    return self;
}

上面其實包含了56的步驟,因為拆開來看不太好去理解,所以整個放在一起就一目了然了。整個過程中先透過AudioFileOpenURL去開啟檔案,接著再去取出音檔的完整資訊,下一步就是建立AudioQueue並指定處理的buffer的callback,也就是剛剛我們時做的HandleOutputBuffer,接著我們透過DeriveBufferSize計算每次取出來的packet數量,最後再判斷是否音檔為VBR格式。因為在VBR格式中的音檔bytes-per-packetframes-per-packet會是不同的,所以mBytesPerPacketmFramesPerPacket會是0,所以我們才會需要一堆AudioStreamPacketDescription去計算要讀取多少byte到記憶體中。以上基本設定做好之後就可以開始播放,不過這邊還稍微調整一下音量,最後再播放之前透過AudioQueuePrime去告訴AudioQueue我們有預先處理3個buffer,然後就可以順利的播放。

最後不要忘記把使用過的資源釋放掉:

- (void)dealloc
{
    AudioQueueDispose(aqData.mQueue, true);
    AudioFileClose(aqData.mAudioFile);
    free(aqData.mPacketDecription);
}

小結

看過Core Audio的Overview內容,只能說非常的枯燥乏味,而且還需要補充不少相關背景知識,不然沒有了解音訊的處理原理是無法很能體會一些關鍵字。重點是看完之後還是無從下手,當然選了比較上層的API下手,給自己建立一點信心,但萬萬沒想到才使用第一個API就遇到不少挫折。首先要了解很多API是用C/C++去實作,所以如果沒有具備c強大的背景後盾(不過大家應該都是沒有這個困擾我想...),是有點難理解他們API的使用方式。再來就是非常不好debug,因為你會先迷惘在每個function回傳的OSStatus,常常是一個非常神秘的數字,還好有google大神可以幫忙解謎一下。後來還在github找到很好的工具,可以直接幫你去爬framework裡面的說明,算是非常好用的debug tool。

最後要面對的是如果出現一些memory問題,是無法用objective-c方式去除錯,因為根本不知道那個address是在指誰,出現這種莫名的錯誤會讓你撞牆好久....然後千萬注意不要typo,否則也是會陷入鬼打牆。這次經驗就是因為typo而用錯function,但萬萬沒想到他們傳入的params都一樣,在模擬器會出現不知所謂的EXC_BAD_ACCESS,竟然實機還可以播放(what the...),一度以為是Apple的bug,但後來重寫一遍後才發覺原來使用錯誤,而且傳入的參數初始方法不同才導致不知名的crash。

發現心得感想比技術內容還多,基本上覺得是如果把文件看的仔仔細細的話,雖然會感到迷惘跟無力但實作應該沒有問題,不過還是建議有一份可以參考的sample會比較好理解,不過自己去摸索到播放的路程會比較坎坷,接下來要練習比較low level的API,感覺Core Audio這趟旅程非常漫長。

comments powered by Disqus