如何使用Audio Unit 混合音频

1,089 阅读2分钟

在之前的AudioUnit和ExtAudioFile两篇文章基础之上,我们来做一些更有意思也更有挑战性的工作,那就是从文件中读取两个音频,将它们混合之后播放出来。

本篇文章分为以下2个部分:

  1. 使用ExtAudioFile读取文件。
  2. AudioMixerUnit的具体使用。

使用ExtAudioFile读取文件

ExtAudioFile可以按照我们设置的数据格式读取文件,很方便,具体参照这篇文章。 ExtAudioFile如何使用

AudioMixerUnit 的具体使用

我们这里使用两个AudioUnit来实现混音和播放的功能,AudioMixerUnit 用来混合数据,AudioOutputUnit用来播放数据。画了一个草图,如下: 结构图.png

创建AudioUnit

先设置描述,然后通过AudioComponentFindNextAudioComponentInstanceNew来获取一个AudioUnit实例,也可以通过AudioGraph来实现。

- (void)createUnits {
    AudioComponentDescription ioUnitDesc = {};
    ioUnitDesc.componentType = kAudioUnitType_Output;
    ioUnitDesc.componentSubType = kAudioUnitSubType_RemoteIO;
    ioUnitDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    ioUnitDesc.componentFlags = 0;
    ioUnitDesc.componentFlagsMask = 0;
    AudioComponent outputComp = AudioComponentFindNext(NULL, &ioUnitDesc);
    if (outputComp == NULL) {
        printf("can't get AudioComponent");
    }
    OSStatus status = AudioComponentInstanceNew(outputComp, &_ioUnit);
    CheckError(status, "creat output unit");
    
    AudioComponentDescription mixerDesc = {};
    mixerDesc.componentType = kAudioUnitType_Mixer;
    mixerDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
    mixerDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
    mixerDesc.componentFlags = 0;
    mixerDesc.componentFlagsMask = 0;
    AudioComponent mixerComp = AudioComponentFindNext(NULL, &mixerDesc);
    if (mixerComp == NULL) {
        printf("can't get AudioComponent");
    }
    status = AudioComponentInstanceNew(mixerComp, &_mixerUnit);
    CheckError(status, "creat mixer unit");
}

设置AudioUnit属性

设置AudioMixerUnit输出格式,并将AudioMixerUnit的输出和AudioOutputUnit的输入连接起来。这里的Bus或者Element可以看作是箭头的下标,AudioMixerUnit的输出和AudioOutputUnit的输入只有一根线,所以是0。

-(void)setupUnits {
    OSStatus status;
    UInt32 propertySize = sizeof(AudioStreamBasicDescription);
    //设置mixer输出格式
    status = AudioUnitSetProperty(_mixerUnit,
                                  kAudioUnitProperty_StreamFormat,
                                  kAudioUnitScope_Output,
                                  0,
                                  _asbd,
                                  propertySize);
    CheckError(status, "set stream format");

    //make connection
    AudioUnitElement inputBus  = 0;
    AudioUnitElement outputBus = 0;
    AudioUnitConnection mixerOutToIoUnitIn;
    mixerOutToIoUnitIn.sourceAudioUnit    = _mixerUnit;
    mixerOutToIoUnitIn.sourceOutputNumber = outputBus;
    mixerOutToIoUnitIn.destInputNumber    = inputBus;

    status = AudioUnitSetProperty(_ioUnit,                         // connection destination
                                  kAudioUnitProperty_MakeConnection,   // property key
                                  kAudioUnitScope_Input,              // destination scope
                                  outputBus,                            // destination element
                                  &mixerOutToIoUnitIn,                 // connection definition
                                  sizeof(mixerOutToIoUnitIn));
    CheckError(status, "make connection");
}

设置AudioUnit输入源回调

这里设置AudioMixerUnit有几个输入源,输入格式,获取数据的回调。

- (void)setStreamCount:(UInt32)count {
    OSStatus status;
    UInt32 propertySize = sizeof(AudioStreamBasicDescription);
    status = AudioUnitSetProperty(_mixerUnit,
                                  kAudioUnitProperty_ElementCount,
                                  kAudioUnitScope_Input,
                                  0,
                                  &count,
                                  sizeof(UInt32));
    CheckError(status, "set stream format");
    for (UInt32 i = 0; i < count; i++) {
        // Set the callback method
        AURenderCallbackStruct callbackStruct;
        callbackStruct.inputProc = inputCallback;
        callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
        status = AudioUnitSetProperty(_mixerUnit,
                                      kAudioUnitProperty_SetRenderCallback,
                                      kAudioUnitScope_Input,
                                      i,
                                      &callbackStruct,
                                      sizeof(callbackStruct));
        CheckError(status, "set callback");
        status = AudioUnitSetProperty(_mixerUnit,
                                      kAudioUnitProperty_StreamFormat,
                                      kAudioUnitScope_Input,
                                      i,
                                      _asbd,
                                      propertySize);
        CheckError(status, "set stream format");
    }
}

控制AudioOutputUnit

同播放与录制时一样,开始时先调用InitializeStart,结束时先StopUninitialize

- (void)startMix {
    dispatch_async(_queue, ^{
        OSStatus status;
        status = AudioUnitInitialize(self.mixerUnit);
        CheckError(status, "initialize mixer unit");
        status = AudioUnitInitialize(self.ioUnit);
        CheckError(status, "initialize output unit");
        status = AudioOutputUnitStart(self.ioUnit);
        CheckError(status, "start output unit");
    });
}
- (void)stopMix {
    dispatch_async(_queue, ^{
        OSStatus status;
        status = AudioOutputUnitStop(self.ioUnit);
        CheckError(status, "stop output unit");
        status = AudioUnitUninitialize(self.ioUnit);
        CheckError(status, "uninitialize output unit");
        status = AudioUnitUninitialize(self.mixerUnit);
        CheckError(status, "uninitialize mixer unit");
    });
}

填充数据

这里设置了一个代理,回调触发的时候去取数据,我的demo中是从文件中读取数据。

OSStatus inputCallback(void *inRefCon,
                       AudioUnitRenderActionFlags *ioActionFlags,
                       const AudioTimeStamp *inTimeStamp,
                       UInt32 inBusNumber,
                       UInt32 inNumberFrames,
                       AudioBufferList *ioData) {
    ZFAudioUnitMixer *mixer = (__bridge ZFAudioUnitMixer *)inRefCon;
    AudioBuffer buffer = ioData->mBuffers[0];
    [mixer.delegate audioMixer:mixer data:buffer.mData size:buffer.mDataByteSize forStreamIndex:inBusNumber];

    return noErr;
}

Github地址