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