AudioUnit playback - Introtuction of CoreAudio Programming
AudioUnit 是 Mac OS X 在 Audio Programming 上面使用,代表一段音樂資料的基本單位。一般來說,如果只是要讓應用程式具有播放音樂的能力,並不需要使用到這種東西。連同蘋果 10.2 開始提供的 CoreAudio API 在內,都屬於低階程式編輯的東西。一般來說,如果只是要讓你的程式播放音樂,還有 QTKit 之類高階的 API 讓你簡單的達到播放音樂的功能。但是如果你希望做到一些更加進階的功能:編輯、Codec 的開發,你就必須要瞭解這個東西。這邊只有簡單記錄一下要如何使用 AudioUnit 來播放音樂。我們會使用到一些 Framework,必須要把他們連結進來:
- AudioUnit.framework
- AudioToolbox.framework
- 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 需要資料的時候才會被呼叫。在這裡我們假設被呼叫的函式名稱為 MyFileRenderProc。AURenderCallbackStruct 有兩個 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 差距好大啊…
沒有留言:
張貼留言