audioUnit混音

FindCrt發表於2019-03-02

demo地址,AudioMusicMixer這個target。

使用AudioUnitGraph來實現一個混音功能,受到官方混音例子的影響,做了一個不同輸入源到不同聲道的效果,如左邊放音樂、右邊放錄音。

這個 demo 為了認識兩點:1. AUGraph 2.audioUnit 自帶的混音。

AUGraph 是什麼?

graph是圖形的意思,它是指一個處理音訊的元件組成的功能網路。比如錄音元件、播放元件、混音元件、特效等,把它們組合在一起,構成一個音訊資料處理的流程,可以不是線性的,那麼就成了2維的圖。通過對各種元件的自由組合,幾乎可以完成你想要的任何需求。

如果瞭解濾鏡,那麼和這個網路結構也是類似的。

這個 demo 使用AUGraph構建一個流程:3個輸入源,兩個音訊檔案和一個錄音(remoteIO的audioUnit),提供資料給mixer,每個輸入源可以調整聲道和聲音大小。

流程類似

IOWithoutRenderCallback_2x.png

構建 AUGraph

NewAUGraph(&processingGraph);
...
status = AUGraphAddNode(processingGraph, &playDesc, &recordPlayNode);
...
status = AUGraphAddNode(processingGraph, &mixerDesc, &mixerNode);
status = AUGraphOpen(processingGraph);
複製程式碼

NewAUGraph新建,然後不斷通過AUGraphAddNode新增節點,也就是一個處理元件。最後AUGraphOpen開啟。

AUGraphAddNode的3個引數分別是:要新增的AUGraph、節點性質描述和節點變數。

屬性描述使用AudioComponentDescription物件,對於錄音和播放都使用:

    playDesc.componentType = kAudioUnitType_Output;
    playDesc.componentSubType = kAudioUnitSubType_RemoteIO;
複製程式碼

而混音元件是:

    mixerDesc.componentType = kAudioUnitType_Mixer;
    mixerDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
複製程式碼

當然還有其他型別的混音元件,目前只研究了這個。

獲取AudioUnit

開啟之後,使用status = AUGraphNodeInfo(processingGraph, recordPlayNode, NULL, &recordPlayUnit);獲取node對應的AudioUnit。可以使用audioUnit的大量功能函式來做複雜的處理。

在node之間建立連線

status = AUGraphConnectNodeInput(processingGraph, mixerNode, 0, recordPlayNode, 0);
複製程式碼

引數分別是:AUGraph變數、前一個node、前一個node的element索引、後一個node、後一個node的element索引。

每個node都可能有多個輸入輸出流,每個對應一個element,可以理解為機器的連線線之類的。上面的這段程式碼就是:把mixerNode的element0輸出連線到recordPlayNode的element0。

使用AUGraphConnect的好處是不需要我們程式設計處理資料了,兩個node之間連線好之後,系統會處理它們之間的資料傳輸。mixerNode是負責混音的節點,recordPlayNode即負責播放也負責錄音(remoteIO的audioUnit固定兩個element,一個錄音一個播放),它的element0負責播放,所以最後一個引數傳了0。而對於kAudioUnitSubType_MultiChannelMixer型別混音節點,輸入可能有多個,但輸出是一個,即element0。

所以上面這段程式碼的實際作用是:把混音結束後的音訊流輸出給播放元件。

設定音訊格式

AUGraphConnect可以建立連線後讓系統處理,但對於更復雜的需求,還需要自己來手動處理音訊資料。

在這之前要先設定音訊格式,為了簡便,固定3個輸入源:索引0是第一個音訊檔案,1是錄音資料,2是第二個音訊檔案。

for (int i = 0; i<MixerInputSourceCount; i++) {
        if ([[self.audioChannelTypes objectForKey:@(i)] integerValue] == AUGraphMixerChannelTypeStereo) {
            sourceStreamFmts[i] = *([[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                                                         sampleRate:44100
                                                                           channels:2
                                                                        interleaved:NO].streamDescription);
        }else{
            sourceStreamFmts[i] = *([[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
                                                                         sampleRate:44100
                                                                           channels:1
                                                                        interleaved:YES].streamDescription);
        }
    }

複製程式碼

MixerInputSourceCount是輸入源數量,根據設定的聲道型別,來確定音訊格式。兩種格式的區別只是聲道和interleaved這個屬性。

在雙聲道時設為2,左邊或右邊單聲道設為1。interleaved這個單詞是"交錯,交叉存取"的意思,這個在設為NO的時候,AudioBufferList包含兩個AudioBuffer,每個負責一個聲道的資料,而設為YES時,是一個AudioBuffer,兩個聲道的資料混在一起的。跟視訊資料如YUV裡面的plane的概念類似。

左右聲道分開的好處是,可以單獨的填充左邊或右邊的聲音,比如把音訊檔案1的資料都只填充到第一個AudioBuffer裡,那只有左邊有聲音。

mixer設定多個輸入源

    UInt32 inputCount = MixerInputSourceCount;
    status = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &inputCount, sizeof(inputCount));
複製程式碼

然後給每個輸入源設定回撥和輸入格式:

for (int i = 0; i<inputCount; ++i) {
        AURenderCallbackStruct mixerInputCallback;
        mixerInputCallback.inputProc = &mixerDataInput;
        mixerInputCallback.inputProcRefCon = (__bridge void*)self;
        
        status = AUGraphSetNodeInputCallback(processingGraph, mixerNode, i, &mixerInputCallback);
        
        status = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &mixStreamFmt, sizeof(AudioStreamBasicDescription));
    }
複製程式碼

這裡變數i代表著輸入源的索引,也是element的索引。在文件裡,element和bus是同一個東西,都是指一個完整的資料流處理環境(context),和輸入輸出流是對應的。

構建音訊讀取器

自己寫的TFAudioFileReader類,內部使用ExtAudioFile來讀取,因為這個系統元件自帶轉碼,而且還可以轉取樣率,非常好用。

開啟關閉

status = AUGraphInitialize(processingGraph);

在open之後,初始化。

然後就可以使用AUGraphStart(processingGraph);AUGraphStop(processingGraph);控制開啟關閉。

輸入回撥

開啟了AUGraph之後,mixer節點會不斷的從它的輸入源輸入資料,方式就是AUGraphSetNodeInputCallback設定的回撥函式。

而mixer輸出部分不需要我們操心了,連線建立後,播放系統會處理。

static OSStatus mixerDataInput(void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData){
    
    AUGraphMixer *mixer = (__bridge AUGraphMixer *)(inRefCon);
    
    //inBusNumber為輸入源的索引,根據這個值來從不用源獲取音訊資料
    if (inBusNumber == FirstAudioFileIndex) {
        
        [mixer readAudioFile:0 numberFrames:inNumberFrames toBuffer:ioData];
        
    }else if (inBusNumber == RecordUnitSourceIndex){
        
        [mixer readRecordedAudio:ioActionFlags timeStamp:inTimeStamp numberFrames:inNumberFrames toBuffer:ioData];
    }else if (inBusNumber == SecondAudioFileIndex){
        [mixer readAudioFile:1 numberFrames:inNumberFrames toBuffer:ioData];
    }
    
    return 0;
}
複製程式碼

inBusNumber這裡用了bus這個名詞,其實還是指element,或說輸入輸出流。使用這個索引確定是哪個輸入流,從不同的源獲取資料。

#####實現輸入源獨立的聲道控制

回撥函式裡的引數ioData是分配了記憶體的,只要把需要的資料填充進去就好了。

interleaved影響的就是這裡的這個ioData的格式。

雙聲道時,直接呼叫AudioUnitRender,把ioData穿進去賦值。

跟我設想稍微不同的是,這樣讀取出來的ioData只有第一個AudioBuffer有資料,即ioData->mBuffers[1].mData列印出來都是0,雖然錄音的audioUnit也是設定了雙聲道的,效果就是錄音只有左邊有聲音。簡便起見,直接把第一個的資料賦值到第二個。

但聲道的時候,就只填充一個AudioBuffer,比如只想在左邊,就只填充ioData->mBuffers[0]。然後把另一個AudioBuffer的資料全部抹掉。

        if (channelType == AUGraphMixerChannelTypeLeft) {
            bufList.mBuffers[0] = ioData->mBuffers[leftChannelIndex]; //只填充左聲道資料
            memset(ioData->mBuffers[rightChannelIndex].mData, 0, ioData->mBuffers[rightChannelIndex].mDataByteSize);
        }
        ...
複製程式碼

調整音量

AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, (UInt32)index, volume, 0);
複製程式碼

index是輸入源的索引,kAudioUnitScope_Input表示調節的是輸入聲音。

可繼續

  • 把輸入源封裝成一個類,可以是檔案、是錄音、是網路資料流等,然後混音可以自由的組合和拆解各個輸入源。不僅調節音量,或者還可以加上變調等,就跟使用濾鏡處理影象一樣。

  • 混音輸出可以加一個實時輸出到檔案,給mixer元件加一個renderCallback就可以拿到資料,然後可以輸出到檔案或者推送到伺服器都沒問題。

  • 錄音和混音之間加一個緩衝區,為了簡便,是在mixer需要資料的時候呼叫AudioUnitRender,但混音需求資料的頻率和錄音輸出資料的頻率不一定一致,會導致某些資料丟失。

  • 在其他iphone或mac試一下雙聲道錄音是否可以得到兩個聲道資料不同。否則雙聲道沒有意義了。

相關文章