前言
上次使用 Audio Queue 來實作 streaming player,這次說明另外一個實作 Audio Unit。
Audio Unit
在開始前要先解釋幾個 Audio Unit 的物件。
1. Audio Graph : 類似一個 Audio Unit 的容器。
2. Audio Node : 用來表示 Audio Graph 內的最小單位。
3. Audio Unit : 用來表述 Audio Node 的物件。
由於 Audio Unit 能對音訊做許多不同的處理,以下舉幾個例子來實作
- 普通播放
- 混音播放
- 同時輸入輸出
Get started!
Play Streaming Music
先確認最後要完成的實作流程。
1. Prepare Audio Graph
建立 Audio Graph 並且開啟。
AUGraph audioGraph;
status = NewAUGraph(&audioGraph);
status = AUGraphOpen(audioGraph);
將 Audio Node 加進去,並且要帶此 node 的 AudioComponentDescription,
再將 node 的資訊放到用來表示 audio node 的 audio unit 上。
AUNode node;
AudioUnit audioUnit;
- (AudioComponentDescription)unitDescription
{
AudioComponentDescription outputUnitDescription;
bzero(&outputUnitDescription, sizeof(AudioComponentDescription));
outputUnitDescription.componentType = kAudioUnitType_Output;
outputUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
outputUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
outputUnitDescription.componentFlags = 0;
outputUnitDescription.componentFlagsMask = 0;
return outputUnitDescription;
}
AudioComponentDescription unitDescription = [self unitDescription];
status = AUGraphAddNode(audioGraph, &unitDescription, &node);
status = AUGraphNodeInfo(audioGraph, node, &unitDescription, &audioUnit);
接著設定這個 audio unit 中 input 與 output 的格式,這邊都是設定 linerPCM,
在 remoteIO 的 audio unit 內,element 0 的 output 會接上輸出硬體(喇叭),
element 1 的 input 會接上輸入硬體(麥克風),
因為目前的輸入源是 audio data,不需要透過硬體來源,因此我們只需設定 element 0。
AudioStreamBasicDescription LinearPCMStreamDescription()
{
AudioStreamBasicDescription destFormat;
bzero(&destFormat, sizeof(AudioStreamBasicDescription));
destFormat.mSampleRate = 44100.0;
destFormat.mFormatID = kAudioFormatLinearPCM;
destFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger;
destFormat.mFramesPerPacket = 1;
destFormat.mBytesPerPacket = 4;
destFormat.mBytesPerFrame = 4;
destFormat.mChannelsPerFrame = 2;
destFormat.mBitsPerChannel = 16;
destFormat.mReserved = 0;
return destFormat;
}
AudioStreamBasicDescription destFormat = LinearPCMStreamDescription();
status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));
加上觀察 audio unit 狀態改變的 listener
status = AudioUnitAddPropertyListener(audioUnit, kAudioOutputUnitProperty_IsRunning, MyAudioUnitPropertyListenerProc, (__bridge void *)(self));
跟著接上 input 的 renderCallback,跟剛剛設定格式一樣,我們是針對 element 0 的 callback。
static OSStatus RenderCallback(void *userData, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData)
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProcRefCon = (__bridge void *)(self);
callbackStruct.inputProc = RenderCallback;
status = AUGraphSetNodeInputCallback(audioGraph, node, 0, &callbackStruct);
最後初始化
status = AUGraphInitialize(audioGraph);
2. Prepare Audio Data
- Parse
與 Audio Queue 相同,可以參考 iOS : Audio Streaming ( Audio Queue )
- Store
在上次的 Audio Queue 中,我們使用 NSData 來儲存 packet data 與 packet description data,
這次我們改用另一種方法來儲存,兩種方法都可以達成目的。
首先我們先建立一個 struct,用來表示每個 packet 資料以及讀取到的 packet index。
typedef struct {
AudioStreamPacketDescription packetDescription;
void *data;
} AudioPacketInfo;
AudioPacketInfo *packets;
size_t packetReadIndex;
packetCount = 2048;
packets = (AudioPacketInfo *)calloc(packetCount, sizeof(AudioPacketInfo));
跟之前一樣,parse 出的資料主要有 data length、packet count、packet data 與
packet description。
針對每個 packet 初始化一個 AudioPacketInfo,並放入 packet data 與 packet description。
- (void)storePacketData:(const void * )inBytes dataLength:(UInt32)inLength packetDescriptions:(AudioStreamPacketDescription* )inPacketDescriptions packetsCount:(UInt32)inPacketsCount
{
@synchronized (self) {
for (size_t index = 0; index < inPacketsCount; index ++) {
AudioStreamPacketDescription emptyDescription;
AudioStreamPacketDescription *currentDescription = inPacketDescriptions ? &(inPacketDescriptions[index]) : &emptyDescription;
AudioPacketInfo *nextInfo = &packets[packetWriteIndex];
nextInfo->data = malloc(currentDescription->mDataByteSize);
memcpy(nextInfo->data, inBytes + currentDescription->mStartOffset, currentDescription->mDataByteSize);
memcpy(&nextInfo->packetDescription, currentDescription, sizeof(AudioStreamPacketDescription));
}
}
}
3. Audio Converter
與 Audio Queue 不同,Audio Unit 需要自行將資料轉換成 LinerPCM 資料。
首先先自行建立一個 class,裡面包含 audio converter、audio buffer list、buffer size、
轉換前格式與目標格式。
@interface SKAudioConverter : NSObject
{
AudioStreamBasicDescription audioStreamDescription;
AudioStreamBasicDescription destFormat;
AudioConverterRef converter;
AudioBufferList *renderBufferList;
UInt32 renderBufferSize;
}
- (instancetype)initWithSourceFormat:(AudioStreamBasicDescription *)sourceFormat;
AudioConverterNew 帶入轉換前格式與目標格式以及 audio converter ref 來建立 converter。
- (instancetype)initWithSourceFormat:(AudioStreamBasicDescription *)sourceFormat
{
self = [super init];
if (self) {
audioStreamDescription = *sourceFormat;
destFormat = LinearPCMStreamDescription();
AudioConverterNew(&audioStreamDescription, &destFormat, &converter);
}
return self;
}
建立一個 AudioBufferList,以八秒來計算最大 packet 數量。
UInt32 packetSize = 44100 * 1 * 8;
renderBufferList = (AudioBufferList *)calloc(1, sizeof(UInt32) + sizeof(AudioBuffer));
renderBufferList->mNumberBuffers = 1;
renderBufferList->mBuffers[0].mNumberChannels = 2;
renderBufferList->mBuffers[0].mDataByteSize = packetSize;
renderBufferList->mBuffers[0].mData = calloc(1, packetSize);
4. Render Callback
啟動 audioGraph 與 audioUnit。
AUGraphStart(audioGraph);
AudioOutputUnitStart(audioUnit);
Audio Unit 的 callback 的資料中有,inNumberFrames(packet size)、ioData (sample data)與
inBusNumber(element)。
typedef OSStatus
(*AURenderCallback)( void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData);
接下來我們要把資料格式轉換成 LinerPCM 格式,使用 AudioConverterFillComplexBuffer,
參數包含 audio converter、AudioConverter callback、custom data、packet size 以及
audio buffer list。
- (OSStatus)requestNumberOfFrames:(UInt32)inNumberOfFrames ioData:(AudioBufferList *)inIoData busNumber:(UInt32)inBusNumber buffer:(SKAudioBuffer *)inBuffer
{
UInt32 packetSize = inNumberOfFrames;
NSArray *args = @[self, inBuffer];
OSStatus status = noErr;
@synchronized(inBuffer) {
status = AudioConverterFillComplexBuffer(converter, AudioConverterFiller, (__bridge void *)(args), &packetSize, renderBufferList, NULL);
}
}
在 converter callback ,把之前儲存的 audio data 與 audio description 塞到 renderBufferlist 內。
void *data = currentPacketInfo.data;
UInt32 length = (UInt32)currentPacketInfo.packetDescription.mDataByteSize;
ioData->mBuffers[0].mData = data;
ioData->mBuffers[0].mDataByteSize = length;
static AudioStreamPacketDescription aspdesc;
*outDataPacketDescription = &aspdesc;
aspdesc.mDataByteSize = length;
aspdesc.mStartOffset = 0;
aspdesc.mVariableFramesInPacket = 1;
回到剛剛 Audio Unit 的 callback 中,再將 renderBufferlist 已轉換好的資料塞到 ioData 內。
- (OSStatus)requestNumberOfFrames:(UInt32)inNumberOfFrames ioData:(AudioBufferList *)inIoData busNumber:(UInt32)inBusNumber buffer:(SKAudioBuffer *)inBuffer
{
......
if (noErr == status && packetSize) {
inIoData->mNumberBuffers = 1;
inIoData->mBuffers[0].mNumberChannels = 2;
inIoData->mBuffers[0].mDataByteSize = renderBufferList->mBuffers[0].mDataByteSize;
inIoData->mBuffers[0].mData = renderBufferList->mBuffers[0].mData;
status = noErr;
}
return status;
}
Mix Audio
一樣先確認最後要完成的實作流程。
跟剛剛實作 Playing Streaming Music 一樣,要先建立 audio graph 與 output node。
NewAUGraph(&audioGraph);
AUGraphOpen(audioGraph);
AudioComponentDescription outputUnitDescription = [self outputUnitDescription];
AUGraphAddNode(audioGraph, &outputUnitDescription, &outputNode);
AUGraphNodeInfo(audioGraph, outputNode, &outputUnitDescription, &outputAudioUnit);
UInt32 maxFPS = 4096;
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));
按照實作流程需要多一個屬於 mix unit 的 audio node。
- (AudioComponentDescription)mixUnitDescription
{
AudioComponentDescription mixerUnitDescription;
bzero(&mixerUnitDescription, sizeof(AudioComponentDescription));
mixerUnitDescription.componentType = kAudioUnitType_Mixer;
mixerUnitDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer;
mixerUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
mixerUnitDescription.componentFlags = 0;
mixerUnitDescription.componentFlagsMask = 0;
return mixerUnitDescription;
}
AudioComponentDescription mixUnitDescription = [self mixUnitDescription];
AUGraphAddNode(audioGraph, &mixUnitDescription, &mixNode);
AUGraphNodeInfo(audioGraph, mixNode, &mixUnitDescription, &mixAudioUnit);
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));
再來把 mix node 的 output 與 remote I/O node 的 input 連接起來。
AUGraphConnectNodeInput(audioGraph, mixNode, 0, outputNode, 0)
設定 remote I/O node input 與 output 的 LinerPCM 格式。
AudioStreamBasicDescription destFormat = LinearPCMStreamDescription();
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(outputAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));
設定 mix node 的 input format 格式,兩個 bus 都需要設定。
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &destFormat, sizeof(destFormat));
之後一樣設定 render callback,但要注意區分不同 bus 給予不同 callback。
也可以設定某一個 bus 的音量。
AudioUnitSetParameter(mixAudioUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, 0.5, 0);
也可以設定一個 bus 在右聲道,另一個 bus 在左聲道。
AudioUnitSetParameter(mixAudioUnit, kMultiChannelMixerParam_Pan, kAudioUnitScope_Input, 0, 1, 1);
AudioUnitSetParameter(mixAudioUnit, kMultiChannelMixerParam_Pan, kAudioUnitScope_Input, 1, -1, 1);
Hardware Input And Ouput
實作流程
建立 Audio Graph、output node 與 mix node
NewAUGraph(&audioGraph);
AUGraphOpen(audioGraph)
AudioComponentDescription outputUnitDescription = [self outputUnitDescription];
AUGraphAddNode(audioGraph, &outputUnitDescription, &remoteIONode);
AUGraphNodeInfo(audioGraph, remoteIONode, &outputUnitDescription, &remoteIOAudioUnit);
UInt32 maxFPS = 4096;
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));
AudioComponentDescription mixUnitDescription = [self mixUnitDescription];
AUGraphAddNode(audioGraph, &mixUnitDescription, &mixNode);
AUGraphNodeInfo(audioGraph, mixNode, &mixUnitDescription, &mixAudioUnit);
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0,&maxFPS, sizeof(maxFPS));
- (AudioComponentDescription)outputUnitDescription
{
AudioComponentDescription outputUnitDescription;
bzero(&outputUnitDescription, sizeof(AudioComponentDescription));
outputUnitDescription.componentType = kAudioUnitType_Output;
outputUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
outputUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
outputUnitDescription.componentFlags = 0;
outputUnitDescription.componentFlagsMask = 0;
return outputUnitDescription;
}
- (AudioComponentDescription)mixUnitDescription
{
AudioComponentDescription mixerUnitDescription;
bzero(&mixerUnitDescription, sizeof(AudioComponentDescription));
mixerUnitDescription.componentType = kAudioUnitType_Mixer;
mixerUnitDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer;
mixerUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
mixerUnitDescription.componentFlags = 0;
mixerUnitDescription.componentFlagsMask = 0;
return mixerUnitDescription;
}
按照流程,需要把 mix node 的 output 與 remote I/O 的 Input 接起來,
以及 remote I/O 的 output 與 mix node 的 input 接起來。
AUGraphConnectNodeInput(audioGraph, mixNode, 0, remoteIONode, 0);
AUGraphConnectNodeInput(audioGraph, remoteIONode, 1, mixNode, 1);
remote I/O 的 input 預設是關閉,因此要先把它打開,設定 flag = 1。
AudioUnitSetProperty(remoteIOAudioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, busOne, &oneFlag, sizeof(oneFlag));
再設定每個 node 的 input 與 output 格式。
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 1, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(remoteIOAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, 0, &destFormat, sizeof(destFormat));
AudioUnitSetProperty(mixAudioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &destFormat, sizeof(destFormat));
後續就跟上面例子一樣接好 mix node 的 render callback,
就可以邊播放音樂邊讓自己的聲音透過輸入裝置輸出。
後續
Audio Unit 相對於 Audio Queue 較複雜,前置動作要先依照需求將 graph 與 node 設置完成,
再自行將音訊轉成 LPCM 格式塞到 ioData 內,但是能夠做較多變化,除了上述的例子外,
還有如去人聲(左聲道減右聲道)或 EQ 等處理。
詳細的實作可以參考我的 github AudioUnitSample