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 差距好大啊…