iOS 音頻-audioUnit 總結

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

demo 放在VideoGather這個庫,裏面的 audioUnitTest 是各個功能的測試研究、singASong 是集合各類音頻處理組件來作的一個「播放伴奏+唱歌 ==> 混音合成歌曲」的功能。git

###基本認識github

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

audioUnitScopes_2x.png

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

IO_unit_2x (1).png

對於 remote_IO 類型 audioUnit,即從硬件採集和輸出到硬件的 audioUnit,它的邏輯是固定的:固定 2 個 element,麥克風通過 element1 到 APP,APP 經 element0 到揚聲器。app

咱們能把控的是中間的「APP 內處理」部分,結合上圖,淡黃色的部分就是APP可控的,Element1 這個組件負責連接麥克風和 APP,它的輸入部分是系統控制,輸出部分是APP控制;Element0 負責鏈接 APP 和揚聲器,輸入部分 APP 控制,輸出部分系統控制。ide

IOWithoutRenderCallback_2x (1).png

這個圖展現了一個完整的錄音+混音+播放的流程,在組件兩邊設置 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 方法裏,不斷接受音頻數據輸入,由於每次接收的數目跟你轉碼的數目不必定相同,甚至不是倍數關係,因此一次輸入可能有屢次轉碼,也可能屢次輸入纔有一次轉碼,還要考慮上次輸入後遺留的數據等。

因此:

  1. leftLength記錄上次輸入轉碼後遺留的數據長度,leftBuf 保留上次的遺留數據

  2. 每次輸入,先合併上次遺留的數據,而後進入循環每次轉換 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;
}
複製代碼
相關文章
相關標籤/搜索