在看 LFLiveKit 程式碼的時候,看到音訊部分使用的是 audioUnit 做的,所以把 audioUnit 學習了一下。總結起來包括幾個部分:播放、錄音、音訊檔案寫入、音訊檔案讀取.
demo 放在VideoGather這個庫,裡面的 audioUnitTest 是各個功能的測試研究、singASong 是集合各種音訊處理元件來做的一個“播放伴奏+唱歌 ==> 混音合成歌曲”的功能。
###基本認識
在AudioUnitHostingFundamentals這個官方文件裡有幾個不錯的圖:
對於通用的audioUnit,可以有1-2條輸入輸出流,輸入和輸出不一定相等,比如mixer,可以兩個音訊輸入,混音合成一個音訊流輸出。每個element表示一個音訊處理上下文(context), 也稱為bus。每個element有輸出和輸出部分,稱為 scope,分別是 input scope 和 Output scope。Global scope 確定只有一個 element,就是 element0,有些屬性只能在 Global scope 上設定。
對於 remote_IO 型別 audioUnit,即從硬體採集和輸出到硬體的 audioUnit,它的邏輯是固定的:固定 2 個 element,麥克風經過 element1 到 APP,APP 經 element0 到揚聲器。
我們能把控的是中間的“APP 內處理”部分,結合上圖,淡黃色的部分就是APP可控的,Element1 這個元件負責連結麥克風和 APP,它的輸入部分是系統控制,輸出部分是APP控制;Element0 負責連線 APP 和揚聲器,輸入部分 APP 控制,輸出部分系統控制。
這個圖展示了一個完整的錄音+混音+播放的流程,在元件兩邊設定 stream 的格式,在程式碼裡的概念是 scope。
檔案讀取
demo 在 TFAudioUnitPlayer 這個類,播放需要音訊檔案讀取和輸出的 audioUnit。
檔案讀取使用 ExtAudioFile,這個據我瞭解,有兩點很重要:1.自帶轉碼 2.只處理 pcm。
不僅是 ExtAudioFile,包括其他 audioUnit,其實應該是流資料處理的性質,這些元件都是“輸入+輸出”的這種工作模式,這種模式決定了你要設定輸出格式、輸出格式等。
-
ExtAudioFileOpenURL
使用檔案地址構建一個 ExtAudioFile
檔案裡的音訊格式是儲存在檔案裡的,不用設定,反而可以讀取出來,比如得到取樣率用作後續的處理。 -
設定輸出格式
AudioStreamBasicDescription clientDesc;
clientDesc.mSampleRate = fileDesc.mSampleRate;
clientDesc.mFormatID = kAudioFormatLinearPCM;
clientDesc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
clientDesc.mReserved = 0;
clientDesc.mChannelsPerFrame = 1; //2
clientDesc.mBitsPerChannel = 16;
clientDesc.mFramesPerPacket = 1;
clientDesc.mBytesPerFrame = clientDesc.mChannelsPerFrame * clientDesc.mBitsPerChannel / 8;
clientDesc.mBytesPerPacket = clientDesc.mBytesPerFrame;
複製程式碼
pcm是沒有編碼、沒有壓縮的格式,更方便處理,所以輸出這種格式。首先格式用 AudioStreamBasicDescription 這個結構體描述,這裡包含了音訊相關的知識:
-
取樣率 SampleRate: 每秒鐘取樣的次數
-
幀 frame:每一次取樣的資料對應一幀
-
聲道數 mChannelsPerFrame:人的兩個耳朵對統一音源的感受不同帶來距離定位,多聲道也是為了立體感,每個聲道有單獨的取樣資料,所以多一個聲道就多一批的資料。
-
最後是每一次取樣單個聲道的資料格式:由 mFormatFlags 和 mBitsPerChannel 確定。mBitsPerChannel 是資料大小,即取樣位深,越大取值範圍就更大,不容易資料溢位。mFormatFlags 裡包含是否有符號、整數或浮點數、大端或是小端等。有符號數就有正負之分,聲音也是波,振動有正負之分。這裡採用 s16 格式,即有符號的 16 位元整數格式。
-
從上至下是一個包含關係:每秒有 SampleRate 次取樣,每次取樣一個 frame,每個 frame有mChannelsPerFrame 個樣本,每個樣本有 mBitsPerChannel 這麼多資料。所以其他的資料大小都可以用以上這些來計算得到。當然前提是資料時沒有編碼壓縮的
-
設定格式:
size = sizeof(clientDesc);
status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientDesc);
複製程式碼
在APP這一端的是 client,在檔案那一端的是 file,帶 client 代表設定 APP 端的屬性。測試 mp3 檔案的讀取,是可以改變取樣率的,即mp3檔案取樣率是 11025,可以直接讀取輸出 44100 的取樣率資料。
- 讀取資料
ExtAudioFileRead(audioFile, framesNum, bufferList)
framesNum 輸入時是想要讀取的 frame 數,輸出時是實際讀取的個數,資料輸出到 bufferList 裡。bufferList 裡面的 AudioBuffer 的 mData 需要分配記憶體。
播放
播放使用 AudioUnit,首先由3個相關的東西:AudioComponentDescription、AudioComponent 和 AudioComponentInstance。AudioUnit 和 AudioComponentInstance是一個東西,typedef 定義的別名而已。
AudioComponentDescription 是描述,用來做元件的篩選條件,類似於 SQL 語句 where 之後的東西。
AudioComponent 是元件的抽象,就像類的概念,使用AudioComponentFindNext
來尋找一個匹配條件的元件。
AudioComponentInstance 是元件,就像物件的概念,使用 AudioComponentInstanceNew
構建。
構建了 audioUnit 後,設定屬性:
- kAudioOutputUnitProperty_EnableIO,開啟 IO。預設情況 element0,也就是從 APP 到揚聲器的IO時開啟的,而 element1,即從麥克風到 APP 的 IO 是關閉的。使用
AudioUnitSetProperty
函式設定屬性,它的幾個引數分別作用是:- 1.要設定的 audioUnit
- 2.屬性名稱
- 3.element, element0 和 element1 選一個,看你是接收音訊還是播放
- 4.scope 也就是範圍,這裡是播放,我們要開啟的是輸出到系統的通道,使用 kAudioUnitScope_Output
- 5.要設定的值
- 6.值的大小。
比較難搞的就是 element 和 scope,需要理解 audioUnit 的工作模式,也就是最開始的兩張圖。
-
設定輸入格式
AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, renderAudioElement, &audioDesc, sizeof(audioDesc));
,格式就用 AudioStreamBasicDescription 結構體資料。輸出部分是系統控制,所以不用管。 -
然後是設定怎麼提供資料。這裡的工作原理是:audioUnit 開啟後,系統播放一段音訊資料,一個 audioBuffer,播完了,通過回撥來跟 APP 索要下一段資料,這樣迴圈,知道你關閉這個 audioUnit。重點就是:
- 1.是系統主動來跟你索要,不是我們的程式去推送資料
- 2.通過回撥函式。就像 APP 這邊是工廠,而系統是商店,他們斷貨了或者要斷貨了,就來跟我們進貨,直到你工廠倒閉了、不賣了等等
所以設定播放的回撥函式:
AURenderCallbackStruct callbackSt;
callbackSt.inputProcRefCon = (__bridge void * _Nullable)(self);
callbackSt.inputProc = playAudioBufferCallback;
AudioUnitSetProperty(audioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Group, renderAudioElement, &callbackSt, sizeof(callbackSt));
複製程式碼
傳入的資料型別是 AURenderCallbackStruct 結構體,它的inputProc 是回撥函式,inputProcRefCon 是回撥函式呼叫時,傳遞給 inRefCon 的引數,這是回撥模式常用的設計,在其他地方可能叫 context。這裡把 self 傳進去,就可以拿到當前播放器物件,獲取音訊資料等。
回撥函式
回撥函式裡最主要的目的就是給 ioData 賦值,把你想要播放的音訊資料填入到 ioData 這個 AudioBufferList 裡。結合上面的音訊檔案讀取,使用 ExtAudioFileRead 讀取資料就可以實現音訊檔案的播放。
播放功能本身是不依賴資料來源的,因為使用的是回撥函式,所以檔案或者遠端資料流都可以播放。
錄音
錄音類 TFAudioRecorder,檔案寫入類 TFAudioFileWriter 和 TFAACFileWriter。為了更自由的組合音訊處理的元件,定義了 TFAudioOutput 類和 TFAudioInput 協議,TFAudioOutput 定義了一些方法輸出資料,而 TFAudioInput 接受資料。
在 TFAudioUnitRecordViewController 類的 setupRecorder 方法裡設定了4種測試:
- pcm 流寫入到 caf 檔案
- pcm 通過 extAudioFile 寫入,extAudioFile 內部轉換成aac格式,寫入 m4a 檔案
- pcm 轉 aac 流,寫入到 adts 檔案
- 比較 2 和 3 兩種方式效能
1. 使用audioUnit獲取錄音資料
和播放時一樣,構建 AudioComponentDescription 變數,使用AudioComponentFindNext
尋找 audioComponent,再使用 AudioComponentInstanceNew
構建一個 audioUnit。
- 開啟 IO:
UInt32 flag = 1;
status = AudioUnitSetProperty(audioUnit,kAudioOutputUnitProperty_EnableIO, // use io
kAudioUnitScope_Input, // 開啟輸入
kInputBus, //element1是硬體到APP的元件
&flag, // 開啟,輸出YES
sizeof(flag));
複製程式碼
element1是系統硬體輸入到APP的element,傳入值1標識開啟。
- 設定輸出格式:
AudioStreamBasicDescription audioFormat;
audioFormat = [self audioDescForType:encodeType];
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
kInputBus,
&audioFormat,
sizeof(audioFormat));
複製程式碼
在 audioDescForType
這個方法裡,只處理了AAC和PCM兩種格式,pcm的時候可以自己計算,也可以利用系統提供的一個函式 FillOutASBDForLPCM
計算,邏輯是跟上面的說的一樣,理解音訊裡的取樣率、聲道、取樣位數等關係就好搞了。
對 AAC 格式,因為是編碼壓縮了的,AAC 固定 1024frame 編碼成一個包(packet),許多屬性沒有用了,比如 mBytesPerFrame,但必須把他們設為0,否則未定義的值可能造成影響。
- 設定輸入的回撥函式
AURenderCallbackStruct callbackStruct;
callbackStruct.inputProc = recordingCallback;
callbackStruct.inputProcRefCon = (__bridge void * _Nullable)(self);
status = AudioUnitSetProperty(audioUnit,kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global,
kInputBus,
&callbackStruct,
sizeof(callbackStruct));
複製程式碼
屬性kAudioOutputUnitProperty_SetInputCallback
指定輸入的回撥,kInputBus 為 1,表示 element1。
- 開啟 AVAudioSession
AVAudioSession *session = [AVAudioSession sharedInstance];
[session setPreferredSampleRate:44100 error:&error];
[session setCategory:AVAudioSessionCategoryRecord withOptions:AVAudioSessionCategoryOptionDuckOthers
error:&error];
[session setActive:YES error:&error];
複製程式碼
AVAudioSessionCategoryRecord 或 AVAudioSessionCategoryPlayAndRecord 都可以,後一種可以邊播邊錄,比如錄歌的APP,播放伴奏同時錄製人聲。
- 最後,使用回撥函式獲取音訊資料
構建 AudioBufferList,然後使用 AudioUnitRender
獲取資料。AudioBufferList 的記憶體資料需要我們自己分配,所以需要計算 buffer 的大小,根據傳入的樣本數和聲道數來計算。
2.pcm資料寫入 caf 檔案
在 TFAudioFileWriter
類裡,使用 extAudioFile 來做音訊資料的寫入。首先要配置 extAudioFile:
- 構建
OSStatus status = ExtAudioFileCreateWithURL((__bridge CFURLRef _Nonnull)(recordFilePath),_fileType, &_audioDesc, NULL, kAudioFileFlags_EraseFile, &mAudioFileRef);
複製程式碼
引數分別是:檔案地址、型別、音訊格式、輔助設定(這裡是移除就檔案)、audioFile 變數。
這裡 _audioDesc 是使用-(void)setAudioDesc:(AudioStreamBasicDescription)audioDesc
從外界傳入的,是上面的錄音的輸出資料格式。
- 寫入
OSStatus status = ExtAudioFileWrite(mAudioFileRef, _bufferData->inNumberFrames, &_bufferData->bufferList);
複製程式碼
在接收到音訊的資料後,不斷的寫入,格式需要 AudioBufferList,中間引數是寫入的 frame 個數。frame 和 audioDesc 裡面的 sampleRate 共同影響音訊的時長計算,frame 傳錯,時長計算就出錯了。
3. 使用ExtAudioFile自帶轉換器來錄製aac編碼的音訊檔案
從錄製的 audioUnit 輸出pcm資料,測試是可以直接輸入給 ExtAudioFile 來錄製 AAC 編碼的音訊檔案。在構建 ExtAudioFile 的時候設定好格式:
AudioStreamBasicDescription outputDesc;
outputDesc.mFormatID = kAudioFormatMPEG4AAC;
outputDesc.mFormatFlags = kMPEG4Object_AAC_Main;
outputDesc.mChannelsPerFrame = _audioDesc.mChannelsPerFrame;
outputDesc.mSampleRate = _audioDesc.mSampleRate;
outputDesc.mFramesPerPacket = 1024;
outputDesc.mBytesPerFrame = 0;
outputDesc.mBytesPerPacket = 0;
outputDesc.mBitsPerChannel = 0;
outputDesc.mReserved = 0;
複製程式碼
重點 是mFormatID和mFormatFlags,還有個坑是那些沒用的屬性沒有重置為0。
然後建立ExtAudioFile:
OSStatus status = ExtAudioFileCreateWithURL((__bridge CFURLRef _Nonnull)(recordFilePath),_fileType, &outputDesc, NULL, kAudioFileFlags_EraseFile, &mAudioFileRef);
設定輸入的格式:
ExtAudioFileSetProperty(mAudioFileRef, kExtAudioFileProperty_ClientDataFormat, sizeof(_audioDesc), &_audioDesc);
其他的不變,和寫入pcm一樣使用 ExtAudioFileWrite
迴圈寫入,只是需要在結束後呼叫 ExtAudioFileDispose
來標識寫入結束,可能跟檔案格式有關。
4. pcm 編碼 AAC
使用 AudioConverter 來處理,demo 寫在 TFAudioConvertor 類裡了。
- 構建
OSStatus status = AudioConverterNew(&sourceDesc, &_outputDesc, &_audioConverter);
和其他元件一樣,需要配置輸入和輸出的資料格式,輸入的就是錄音 audiounit輸出的 pcm 格式,輸出希望轉化為 aac,則把 mFormatID 設為 kAudioFormatMPEG4AAC,mFramesPerPacket 設為 1024。然後取樣率 mSampleRate 和聲道數 mChannelsPerFrame 設一下,其他的都設為 0 就好。為了簡便,取樣率和聲道數可以設為和輸入的pcm資料一樣。
編碼之後資料壓縮,所以輸出大小是未知的,通過屬性 kAudioConverterPropertyMaximumOutputPacketSize 獲取輸出的 packet 大小,依靠這個給輸出 buffer 申請合適的記憶體大小。
- 輸入和轉化
首先要確定每次轉換的資料大小:bufferLengthPerConvert = audioDesc.mBytesPerFrame*_outputDesc.mFramesPerPacket*PACKET_PER_CONVERT;
即每個 frame 的大小 *每個 packet 的 frame 數 * 每次轉換的 pcket 數目。每次轉換後多個 frame打包成一個 packet,所以 frame 數量最好是 mFramesPerPacket 的倍數。
在 receiveNewAudioBuffers
方法裡,不斷接受音訊資料輸入,因為每次接收的數目跟你轉碼的數目不一定相同,甚至不是倍數關係,所以一次輸入可能有多次轉碼,也可能多次輸入才有一次轉碼,還要考慮上次輸入後遺留的資料等。
所以:
-
leftLength
記錄上次輸入轉碼後遺留的資料長度,leftBuf
保留上次的遺留資料 -
每次輸入,先合併上次遺留的資料,然後進入迴圈每次轉換 bufferLengthPerConvert 長度的資料,直到剩餘的不足,把它們儲存到
leftBuf
進行下一次處理
轉換函式本身很簡單:AudioConverterFillComplexBuffer(_audioConverter, convertDataProc, &encodeBuffer, &packetPerConvert, &outputBuffers, NULL);
引數分別是:轉換器、回撥函式、回撥函式引數 inUserData 的值、轉換的 packet 大小、輸出的資料。
資料輸入是在會掉函式裡處理,這裡輸入資料就通過”回撥函式引數 inUserData 的值”傳遞進去,也可以在回撥裡再讀取資料。
OSStatus convertDataProc(AudioConverterRef inAudioConverter,UInt32 *ioNumberDataPackets,AudioBufferList *ioData,AudioStreamPacketDescription **outDataPacketDescription,void *inUserData){
AudioBuffer *buffer = (AudioBuffer *)inUserData;
ioData->mBuffers[0].mNumberChannels = buffer->mNumberChannels;
ioData->mBuffers[0].mData = buffer->mData;
ioData->mBuffers[0].mDataByteSize = buffer->mDataByteSize;
return noErr;
}
複製程式碼