Play Music using AudioUnit

| 6/29/2010
AudioUnit playback - Introtuction of CoreAudio Programming
AudioUnit 是 Mac OS X 在 Audio Programming 上面使用,代表一段音樂資料的基本單位。一般來說,如果只是要讓應用程式具有播放音樂的能力,並不需要使用到這種東西。連同蘋果 10.2 開始提供的 CoreAudio API 在內,都屬於低階程式編輯的東西。一般來說,如果只是要讓你的程式播放音樂,還有 QTKit 之類高階的 API 讓你簡單的達到播放音樂的功能。但是如果你希望做到一些更加進階的功能:編輯、Codec 的開發,你就必須要瞭解這個東西。這邊只有簡單記錄一下要如何使用 AudioUnit 來播放音樂。

我們會使用到一些 Framework,必須要把他們連結進來:
  1. AudioUnit.framework
  2. AudioToolbox.framework
  3. CoreService.framework

首先,我們使用 Component 來取得使用者設定的 Default Output Device Unit。AudioUnit 分成許多不同的種類,他們可以是 Outputs, Mixers, or DSP。閱讀 AUComponent.h 可以得到其他資訊。我們可以簡單的使用 ComponentDescription 來取得 Output Audio Unit。

Code:
ComponentDescription desc;
Component comp;
AudioUnit outputUnit;
OSStatus err;

// 設定 Component 型態
desc.componentType = kAudioUnitType_Output;
// 每一種型態都有一個 sub type,用來深入描述該型態
desc.componentSubType = kAudioUnitSubType_DefaultOutput;
// 下面的部份,所有的 AudioUint 都是一樣的,依樣畫葫蘆就可以了
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentFlags = 0;
desc.componentFlagsMask = 0;

// 取得與描述相同的 Component
comp = FindNextComponent(NULL, &desc);
if (comp == NULL) exit(-1);
// 取得 Output AudioUnit
err = OpenAComponent(comp, &outputUnit);

接下來,我們要設定 AudioUnit 的參數。
AudioUnit 有許多不同的參數可以設定,其中最常使用到的兩個函數就屬於 AudioUnitGetProperty 以及 AudioUnitSetProperty 兩個。其中,我們也經常使用 AudioUnitGetPropertyInfo 來取得一些資訊用以避免設定上的錯誤。

在 AudioUnit 的眾多參數裡面,最重要的一項就是 AudioStreamBasicDescription (ASBD)。這項變數裡面存放了關於 Audio Stream 的描述,包含 simple rate、Package information 以及 Stream 格式。我們可以思考 AudioUnit 包含了兩個最大的部份:input 跟 output。所以說我們可以為這兩個部分設定 ASBD。(注意:AudioUnit 型態本身就是個 Reference)

Code:
UInt32 size;
Boolean isWritable;
AudioStreamBasicDescription outputDesc;
AudioStreamBasicDescription inputDesc;

// 取得 Stream format 的 size,以及是否可以寫入。
err = AudioUnitGetPropertyInfo(outputUnit,
                               kAudioUnitPorperity_StreamFormat,
                               kAudioUnitScope_Output,
                               0, &size, &isWritable);

// 取得 Stream Format
err = AudioUnitGetProperty(outputUnit,
                           kAudioUnitProperty_StreamFormat,
                           kAudioUnitScope_Output,
                           0, &outputDesc, &size);

// 將 input 跟 output 的 stream format 設定相同
err = AudioUnitSetProperty(outputUnit,
                           kAudioUnitProperty_StreamFormat,
                           kAudioUnitScope_Input,
                           0, &outputDesc, &size);

// 注意,在所有 Property 設定結束之後才使用 AudioUnitInitialize。這個函式的消費很大。
err = AudioUintInitialize(outputUint);
這裡需要注意的地方還有一個,所有的 err 都可以作為結束的判斷式。

我們得到的 ASBD 資料格式應該如下面的範例:
接下來我們要設定一個 Render Callback 來做為 AudioUnit 取得播放資料的緩衝區的地方。我們使用 kAudioUnitProperty_SetRenderCallback 以及 AURenderCallbackStruct。被設定的函式只有在 Audio Converter 需要資料的時候才會被呼叫。在這裡我們假設被呼叫的函式名稱為 MyFileRenderProcAURenderCallbackStruct 有兩個 Member:inputProc 以及 inputProcRefCon。前者為我們要設定的 callback function 名稱,後者則是該函式的參數。

Code:
AURenderCallbackStruct renderCallback;
memset(&renderCallback, 0, sizeof(AURenderCallbackStruct));

// 設定 Render Callback。因為播放的時候不需要傳入參數,所以 RefCon 設定為 0
renderCallback.inputProc = MyFileRenderProc;
renderCallback.inputProcRefCon = 0;

// 設定到 AudioUnit 裡面
err = AudioUnitSetProperty(outputUnit,
                           kAudioUnitProperty_SetRenderCallback,
                           kAudioUnitScope_Input,
                           0, &renderCallback,
                           sizeof(AURenderCallbackStruct));


這些設定都設定完成之後,我們就來讀取音樂檔案,並且放置到緩衝區裡面。當 AudioUnit 在播放的時候他會自動取得緩衝區裡面的資料使用。如果是要用來播放其他格式的檔案,那麼我們就必須要擁有其他格式的 Codec 來將檔案解碼成系統所支援的格式。這裡,我們就使用蘋果提供的 Audio File API 來讀取檔案。記住,在這裡我們只有假設我們讀取的是很小的音樂檔案,所以說我們將整個檔案都放到了緩衝區中。事實上,當你的音樂比較龐大的時候,最好是將你解碼的部份分段放入緩衝區中,解碼的動作可能必須要開一個新的執行緒以避免和主程序互衝。

Code:
UInt64 totalPacketCount;
UInt64 fileByteCount;
UInt32 packetSize;
...
OSStatus
OpenAudioFile(AudioFileID * fileID,
                       AudioStreamBasicDescription * fileASBD,
                       const char * filename)

{
OSStatus err = noErr;
Uint32 size;

// 產生路徑,記住檔案路徑是以 UTF8 編碼的
CFURLRef fileURL = CFURLCreateFromFileSystemRepresentation(kCFAllocatorDefault, (const UInt8*)filename, strlen(filename), NO);

// AudioFileOpen 已經不再被支援了
err = AudioFileOpenURL(fileURL, kAudioFileReadPermission, 0, fileID);

// 讀取 Packet 資訊,這個資訊會在 AudioConverter 中使用
// 我們已經確定了 Format Size 了,這裡不再使用 AudioUnitGetPropertyInfo
size = sizeof(AudioStreamBasicDescription);
memset(fileASBD, 0, size);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyDataFormat, &size, fileASBD);
  
size = sizeof(totalPacketCount);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyAudioDataPacketCount, &size, &totalPacketCount);
  
size = sizeof(fileByteCount);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyAudioDataByteCount, &size, &fileByteCount);
  
size = sizeof(packetSize);
err = AudioFileGetProperty(*fileID, kAudioFilePropertyMaximumPacketSize, &size, &packetSize);

return err;
}

int main(int argc, char ** argv)
{
AudioFileID fileID;
UInt32 bytesReturned = 0;
UInt32 packets = totalPacketCount;
char * audioBuffer = malloc(fileByteCount);

...
// 將整個音樂檔案放到記憶體中
err = OpenAudioFile(&fileID, &inputDesc, argv[1]);
err = AudioFileReadPackets(fileID, NO, &butesReturned, NULL, 0, &packets, audioBuffer);
}

接下來,我們就來撰寫確實的 Render Callback。這個 Callback 會在 AudioUnit 需要 Audio Frame 的時候被呼叫。我們在這裡使用 AudioConverter 將讀近來的檔案轉換成系統支援的格式。這裡我們不多寫程式碼,所以我們就只有在 PCM 之間轉換。也就是說,這個程式碼只能夠播放 PCM 的檔案格式:WAV、AIF。

Code:
AudioConverter converter;
AudioConverterNew(&inputDesc, &outputDesc, &converter);

OSStatus MyFileRenderProc(void * inRefCon,
                              AudioUnitRenderActionFlags *inActionFlag,
                              const AudioTimeStamp *timeStamp, UInt32 inBusNumber,
                              UInt32 inNumFrames, AudioBufferList *ioData)
{
    OSStatus err = noErr;
  
    AudioConverterFillComplexBuffer(audioConverter,
                                    MyACComplexInputProc,
                                    0, &inNumFrames, ioData, 0);
  
    return err;
}

OSStatus MyACComplexInputProc(AudioConverterRef inAudioConverter,
                              UInt32 * ioNumberDataPackets,
                              AudioBufferList * ioData,
                              AudioStreamPacketDescription ** ioDataPacketDescription,
                              void * inUserData)
{
    OSStatus err = noErr;
    UInt32 bytesCopied = 0;
  
    ioData->mBuffers[0].mData = NULL;
    ioData->mBuffers[0].mDataByteSize = 0;
  
    if (playedPacketOffset + *ioNumberDataPackets > totalPacketCount)
        *ioNumberDataPackets = totalPacketCount - playedPacketOffset;
  
    if (*ioNumberDataPackets) {
        if (sourceBuffer != NULL) {
            free(sourceBuffer);
            sourceBuffer = NULL;
        }
      
        bytesCopied = *ioNumberDataPackets * packetSize;
        sourceBuffer = calloc(1, bytesCopied);
        memcpy(sourceBuffer, audioBuffer + playedByteOffset, bytesCopied);
      
        playedByteOffset += bytesCopied;
        playedPacketOffset += *ioNumberDataPackets;
      
        ioData->mBuffers[0].mData = sourceBuffer;
        ioData->mBuffers[0].mDataByteSize = bytesCopied;
    }
    else {
        ioData->mBuffers[0].mData = NULL;
        ioData->mBuffers[0].mDataByteSize = 0;
        isPlaying = NO;
        err = noErr;
    }

    return err;
}

以上,是使用 AudioUnit 來播放音樂檔案的簡單程式。如果有可能,我會繼續將學習 CoreAudio 時所學到的東西繼續發佈上來。也請發現有問題的前輩多多指教。

Download Source Code Here.
How to compile: gcc auplaysample.m -framework AudioUnit -framework AudioToolbox -framework CoreService -o AudioPlay

P.S. 話說回來,如果我用 NSSound 的話,上面一大串就剩下兩行了!高階 API 跟低階 API 差距好大啊…

沒有留言: