iOS音頻-audioUnit總結(jié)

在看LFLiveKit代碼的時候,看到音頻部分使用的是audioUnit做的,所以把audioUnit學習了一下。總結(jié)起來包括幾個部分:播放、錄音、音頻文件寫入、音頻文件讀取.

demo放在VideoGather這個庫,里面的audioUnitTest是各個功能的測試研究、singASong是集合各種音頻處理組件來做的一個“播放伴奏+唱歌 ==> 混音合成歌曲”的功能。

基本認識

AudioUnitHostingFundamentals這個官方文檔里有幾個不錯的圖:

audioUnitScopes_2x.png

對于通用的audioUnit,可以有1-2條輸入輸出流,輸入和輸出不一定相等,比如mixer,可以兩個音頻輸入,混音合成一個音頻流輸出。每個element表示一個音頻處理上下文(context), 也稱為bus。每個element有輸出和輸出部分,稱為scope,分別是input scope和Output scope。Global scope確定只有一個element,就是element0,有些屬性只能在Global scope上設(shè)置。

IO_unit_2x (1).png

對于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)控制。

IOWithoutRenderCallback_2x (1).png

這個圖展示了一個完整的錄音+混音+播放的流程,在組件兩邊設(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ù)等。

所以:

  1. leftLength記錄上次輸入轉(zhuǎn)碼后遺留的數(shù)據(jù)長度,leftBuf保留上次的遺留數(shù)據(jù)

  2. 每次輸入,先合并上次遺留的數(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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,250評論 6 530
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 97,923評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,041評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,475評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,253評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,801評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,882評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,023評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,530評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,494評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,639評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,177評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 43,890評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,289評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,552評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,242評論 3 389
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,626評論 2 370

推薦閱讀更多精彩內(nèi)容