over 7 years ago

前言

上次使用 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 能對音訊做許多不同的處理,以下舉幾個例子來實作

  1. 普通播放
  2. 混音播放
  3. 同時輸入輸出

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

← iOS : Audio Streaming ( Audio Queue ) WWDC 2015 →