demo地址,AudioMusicMixer這個target。
使用AudioUnitGraph來實現一個混音功能,受到官方混音例子的影響,做了一個不同輸入源到不同聲道的效果,如左邊放音樂、右邊放錄音。
這個 demo 為了認識兩點:1. AUGraph 2.audioUnit 自帶的混音。
AUGraph 是什麼?
graph是圖形的意思,它是指一個處理音訊的元件組成的功能網路。比如錄音元件、播放元件、混音元件、特效等,把它們組合在一起,構成一個音訊資料處理的流程,可以不是線性的,那麼就成了2維的圖。通過對各種元件的自由組合,幾乎可以完成你想要的任何需求。
如果瞭解濾鏡,那麼和這個網路結構也是類似的。
這個 demo 使用AUGraph構建一個流程:3個輸入源,兩個音訊檔案和一個錄音(remoteIO的audioUnit),提供資料給mixer,每個輸入源可以調整聲道和聲音大小。
流程類似
構建 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試一下雙聲道錄音是否可以得到兩個聲道資料不同。否則雙聲道沒有意義了。