Audio Unit採集音訊實戰
iOS中使用Audio unit實現音訊資料採集,直接採集PCM無損資料, Audio Unit不能直接採集壓縮資料,在以後的文章會講到音訊壓縮.
實現原理
使用Audio Unit採集硬體輸入端,如麥克風,其他外接具備麥克風功能裝置(帶麥的耳機,話筒等,前提是其本身要和蘋果相容).
閱讀前提
- Core Audio基本原理:簡書, 掘金 , 部落格
- Audio Unit概念篇:簡書, 掘金 , 部落格
- Audio Session基礎:簡書, 掘金 , 部落格
- 音視訊基礎知識
- C,C++基本知識
本文直接為實戰篇,如需瞭解理論基礎參考上述連結中的內容,本文側重於實戰中注意點.
本專案實現低耦合,高內聚,所以直接將相關模組拖入你的專案設定引數就可直接使用.
GitHub地址(附程式碼) : Audio Unit Capture
簡書地址 :Audio Unit Capture
掘金地址 :Audio Unit Capture
部落格地址 :Audio Unit Capture
具體實現
1.程式碼結構

如上所示,我們總體分為兩大類,一個是負責採集的類,一個是負責做音訊錄製的類,你可以根據需求在適當時機啟動,關閉Audio Unit, 並且在Audio Unit已經啟動的情況下可以進行音訊檔案錄製,前面需求僅僅需要如下四個API即可完成.
// Start / Stop Audio Queue [[XDXAudioCaptureManager getInstance] startAudioCapture]; [[XDXAudioCaptureManager getInstance] stopAudioCapture]; // Start / Stop Audio Record [[XDXAudioQueueCaptureManager getInstance] startRecordFile]; [[XDXAudioQueueCaptureManager getInstance] stopRecordFile]; 複製程式碼
2. 初始化audio unit
本例採用單例實現,故將audio unit的實現放在初始化中,僅執行一次,如果銷燬了audio unit則需要在外層重新呼叫初始化API,一般不建議反覆銷燬建立audio unit,所以最好就是在單例初始化中配置audio unit其後僅僅需要開啟關閉即可.
iPhone裝置預設僅支援單聲道,如果設定雙聲道程式碼無法正常初始化. 如果需要模擬雙聲道,可以手動用程式碼對單聲道資料做一次拷貝.具體方法以後文章會講到.
注意: 這裡的取樣buffer大小的設定與取樣時間的設定不可隨意設定,換句話說,當取樣時間一定,我們設定的取樣資料大小不能超過其最大值,可通過公式算出取樣時間與取樣資料的關係.
取樣公式計算
資料量(位元組 / 秒)=(取樣頻率(Hz)* 取樣位數(bit)* 聲道數)/ 8 複製程式碼
- (instancetype)init { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _instace = [super init]; // Note: audioBufferSize can not more than durationSec max size. [_instace configureAudioInfoWithDataFormat:&m_audioDataFormat formatID:kAudioFormatLinearPCM sampleRate:44100 channelCount:1 audioBufferSize:2048 durationSec:0.02 callBack:AudioCaptureCallback]; }); return _instace; - (void)configureAudioInfoWithDataFormat:(AudioStreamBasicDescription *)dataFormat formatID:(UInt32)formatID sampleRate:(Float64)sampleRate channelCount:(UInt32)channelCount audioBufferSize:(int)audioBufferSize durationSec:(float)durationSec callBack:(AURenderCallback)callBack { // Configure ASBD [self configureAudioToAudioFormat:dataFormat byParamFormatID:formatID sampleRate:sampleRate channelCount:channelCount]; // Set sample time [[AVAudioSession sharedInstance] setPreferredIOBufferDuration:durationSec error:NULL]; // Configure Audio Unit m_audioUnit = [self configreAudioUnitWithDataFormat:*dataFormat audioBufferSize:audioBufferSize callBack:callBack]; } } 複製程式碼
3. 設定音訊流資料格式 ASBD
- 注意點
需要注意的是,音訊資料格式與硬體直接相關,如果想獲取最高效能,最好直接使用硬體本身的取樣率,聲道數等音訊屬性,所以,如取樣率,當我們手動進行更改後,Audio Unit會在內部自行轉換一次,雖然程式碼上沒有感知,但一定程式上還是降低了效能.
iOS中不支援直接設定雙聲道,如果想模擬雙聲道,可以自行填充音訊資料,具體會在以後的文章中講到,喜歡請持續關注.
- 獲取音訊屬性值
理解 AudioSessionGetProperty
函式,該函式表明查詢當前硬體指定屬性的值,如下, kAudioSessionProperty_CurrentHardwareSampleRate
為查詢當前硬體取樣率, kAudioSessionProperty_CurrentHardwareInputNumberChannels
為查詢當前採集的聲道數.因為本例中使用手動賦值方式更加靈活,所以沒有使用查詢到的值.
- 設定不同格式定製的屬性
首先,你必須瞭解未壓縮格式(PCM...)與壓縮格式(AAC...). 使用iOS直接採集未壓縮資料是可以直接拿到硬體採集到的資料,由於audio unit不能直接採集aac型別資料,所以這裡僅採集原始的PCM資料.
使用PCM資料格式必須設定取樣值的flag: mFormatFlags
,每個聲道中取樣的值換算成二進位制的位寬 mBitsPerChannel
,iOS中每個聲道使用16位的位寬,每個包中有多少幀 mFramesPerPacket
,對於PCM資料而言,因為其未壓縮,所以每個包中僅有1幀資料.每個包中有多少位元組數(即每一幀中有多少位元組數),可以根據如下簡單計算得出
注意,如果是其他壓縮資料格式,大多數不需要單獨設定以上引數,預設為0.這是因為對於壓縮資料而言,每個音訊取樣包中壓縮的幀數以及每個音訊取樣包壓縮出來的位元組數可能是不同的,所以我們無法預知進行設定,就像 mFramesPerPacket
引數,因為壓縮出來每個包具體有多少幀只有壓縮完成後才能得知.
#define kXDXAudioPCMFramesPerPacket 1 #define KXDXAudioBitsPerChannel 16 -(void)configureAudioToAudioFormat:(AudioStreamBasicDescription *)audioFormat byParamFormatID:(UInt32)formatIDsampleRate:(Float64)sampleRate channelCount:(UInt32)channelCount { AudioStreamBasicDescription dataFormat = {0}; UInt32 size = sizeof(dataFormat.mSampleRate); // Get hardware origin sample rate. (Recommended it) Float64 hardwareSampleRate = 0; AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate, &size, &hardwareSampleRate); // Manual set sample rate dataFormat.mSampleRate = sampleRate; size = sizeof(dataFormat.mChannelsPerFrame); // Get hardware origin channels number. (Must refer to it) UInt32 hardwareNumberChannels = 0; AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels, &size, &hardwareNumberChannels); dataFormat.mChannelsPerFrame = channelCount; dataFormat.mFormatID = formatID; if (formatID == kAudioFormatLinearPCM) { dataFormat.mFormatFlags= kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked; dataFormat.mBitsPerChannel= KXDXAudioBitsPerChannel; dataFormat.mBytesPerPacket= dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame; dataFormat.mFramesPerPacket = kXDXAudioPCMFramesPerPacket; } memcpy(audioFormat, &dataFormat, sizeof(dataFormat)); NSLog(@"%@:%s - sample rate:%f, channel count:%d",kModuleName, __func__,sampleRate,channelCount); } 複製程式碼
4. 設定取樣時間
使用AVAudioSession可以設定取樣時間,注意,在取樣時間一定的情況下,我們設定的取樣大小不能超過其最大值.
資料量(位元組 / 秒)=(取樣頻率(Hz)* 取樣位數(bit)* 聲道數)/ 8
比如: 取樣率是44.1kHz, 取樣位數是16, 聲道數是1, 取樣時間為0.01秒,則最大的取樣資料為882. 所以即使我們設定超過此數值,系統最大也只能採集882個位元組的音訊資料.
[[AVAudioSession sharedInstance] setPreferredIOBufferDuration:durationSec error:NULL]; 複製程式碼
5. 配置Audio Unit
m_audioUnit = [self configreAudioUnitWithDataFormat:*dataFormat audioBufferSize:audioBufferSize callBack:callBack]; - (AudioUnit)configreAudioUnitWithDataFormat:(AudioStreamBasicDescription)dataFormat audioBufferSize:(int)audioBufferSize callBack:(AURenderCallback)callBack { AudioUnit audioUnit = [self createAudioUnitObject]; if (!audioUnit) { return NULL; } [self initCaptureAudioBufferWithAudioUnit:audioUnit channelCount:dataFormat.mChannelsPerFrame dataByteSize:audioBufferSize]; [self setAudioUnitPropertyWithAudioUnit:audioUnit dataFormat:dataFormat]; [self initCaptureCallbackWithAudioUnit:audioUnit callBack:callBack]; // Calls to AudioUnitInitialize() can fail if called back-to-back on different ADM instances. A fall-back solution is to allow multiple sequential calls with as small delay between each. This factor sets the max number of allowed initialization attempts. OSStatus status = AudioUnitInitialize(audioUnit); if (status != noErr) { NSLog(@"%@:%s - couldn't init audio unit instance, status : %d \n",kModuleName,__func__,status); } return audioUnit; } 複製程式碼
- 建立audio unit物件
這裡可以指定使用audio unit哪個分類建立. 這裡使用的kAudioUnitSubType_VoiceProcessingIO分類是做回聲消除及增強人聲的分類,如果僅僅需要原始未處理音訊資料也可以改用kAudioUnitSubType_RemoteIO分類,如果想了解更多關於audio unit分類,文章最上方有相關連結可以訪問.
AudioComponentFindNext:第一個引數設定為NULL表示使用系統定義的順序查詢第一個匹配的audio unit.如果你將上一個使用的audio unit引用傳給該引數,則該函式將繼續尋找下一個與之描述匹配的audio unit.
- (AudioUnit)createAudioUnitObject { AudioUnit audioUnit; AudioComponentDescription audioDesc; audioDesc.componentType= kAudioUnitType_Output; audioDesc.componentSubType= kAudioUnitSubType_VoiceProcessingIO;//kAudioUnitSubType_RemoteIO; audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple; audioDesc.componentFlags= 0; audioDesc.componentFlagsMask= 0; AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc); OSStatus status = AudioComponentInstanceNew(inputComponent, &audioUnit); if (status != noErr){ NSLog(@"%@:%s - create audio unit failed, status : %d \n",kModuleName, __func__, status); return NULL; }else { return audioUnit; } } 複製程式碼
- 建立一個接收採集到音訊資料的資料結構
kAudioUnitProperty_ShouldAllocateBuffer
: 預設為true, 它將建立一個回撥函式中接收資料的buffer, 在這裡設定為false, 我們自己定義了一個bufferList用來接收採集到的音訊資料.
- (void)initCaptureAudioBufferWithAudioUnit:(AudioUnit)audioUnit channelCount:(int)channelCount dataByteSize:(int)dataByteSize { // Disable AU buffer allocation for the recorder, we allocate our own. UInt32 flag= 0; OSStatus status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_ShouldAllocateBuffer, kAudioUnitScope_Output, INPUT_BUS, &flag, sizeof(flag)); if (status != noErr) { NSLog(@"%@:%s - could not allocate buffer of callback, status : %d \n", kModuleName, __func__, status); } AudioBufferList * buffList = (AudioBufferList*)malloc(sizeof(AudioBufferList)); buffList->mNumberBuffers= 1; buffList->mBuffers[0].mNumberChannels= channelCount; buffList->mBuffers[0].mDataByteSize= dataByteSize; buffList->mBuffers[0].mData= (UInt32 *)malloc(dataByteSize); m_buffList = buffList; } 複製程式碼
- 設定audio unit屬性
kAudioUnitProperty_StreamFormat kAudioOutputUnitProperty_EnableIO
input bus / input element: 連線裝置硬體輸入端(如:麥克風)
output bus / output element: 連線裝置硬體輸出端(如:揚聲器)
input scope: 每個element/scope可能有一個input scope或output scope,以採集為例,音訊從audio unit的input scope流入,我們僅僅只能從output scope中獲取音訊資料.因為input scope是audio unit與硬體之間的互動.所以你可以看到程式碼中設定的兩項 INPUT_BUS
, kAudioUnitScope_Output
.
remote I/O audio unit預設是開啟輸出端,關閉輸入端的,而本文講的是利用audio unit做音訊資料採集,所以我們要開啟輸入端,禁止輸出端.
- (void)setAudioUnitPropertyWithAudioUnit:(AudioUnit)audioUnit dataFormat:(AudioStreamBasicDescription)dataFormat { OSStatus status; status = AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, INPUT_BUS, &dataFormat, sizeof(dataFormat)); if (status != noErr) { NSLog(@"%@:%s - set audio unit stream format failed, status : %d \n",kModuleName, __func__,status); } /* // remove echo but can not effect by testing. UInt32 echoCancellation = 0; AudioUnitSetProperty(m_audioUnit, kAUVoiceIOProperty_BypassVoiceProcessing, kAudioUnitScope_Global, 0, &echoCancellation, sizeof(echoCancellation)); */ UInt32 enableFlag = 1; status = AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, INPUT_BUS, &enableFlag, sizeof(enableFlag)); if (status != noErr) { NSLog(@"%@:%s - could not enable input on AURemoteIO, status : %d \n",kModuleName, __func__, status); } UInt32 disableFlag = 0; status = AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, OUTPUT_BUS, &disableFlag, sizeof(disableFlag)); if (status != noErr) { NSLog(@"%@:%s - could not enable output on AURemoteIO, status : %d \n",kModuleName, __func__,status); } } 複製程式碼
- 註冊回撥函式接收音訊資料
- (void)initCaptureCallbackWithAudioUnit:(AudioUnit)audioUnit callBack:(AURenderCallback)callBack { AURenderCallbackStruct captureCallback; captureCallback.inputProc= callBack; captureCallback.inputProcRefCon= (__bridge void *)self; OSStatus status= AudioUnitSetProperty(audioUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, INPUT_BUS, &captureCallback, sizeof(captureCallback)); if (status != noErr) { NSLog(@"%@:%s - Audio Unit set capture callback failed, status : %d \n",kModuleName, __func__,status); } } 複製程式碼
6. 開啟audio unit
直接呼叫 AudioOutputUnitStart
即可開啟audio unit.如果以上配置都正確,audio unit可以直接工作.
- (void)startAudioCaptureWithAudioUnit:(AudioUnit)audioUnit isRunning:(BOOL *)isRunning { OSStatus status; if (*isRunning) { NSLog(@"%@:%s - start recorder repeat \n",kModuleName,__func__); return; } status = AudioOutputUnitStart(audioUnit); if (status == noErr) { *isRunning= YES; NSLog(@"%@:%s - start audio unit success \n",kModuleName,__func__); }else { *isRunning= NO; NSLog(@"%@:%s - start audio unit failed \n",kModuleName,__func__); } } 複製程式碼
7. 回撥函式中處理音訊資料
-
inRefCon:開發者自己定義的任何資料,一般將本類的例項傳入,因為回撥函式中無法直接呼叫OC的屬性與方法,此引數可以作為OC與回撥函式溝通的橋樑.即傳入本類物件.
-
ioActionFlags: 描述上下文資訊
-
inTimeStamp: 包含取樣的時間戳
-
inBusNumber: 呼叫此回撥函式的匯流排數量
-
inNumberFrames: 此次呼叫包含了多少幀資料
-
ioData: 音訊資料.
-
AudioUnitRender
: 使用此函式將採集到的音訊資料賦值給我們定義的全域性變數m_buffList
static OSStatus AudioCaptureCallback(void*inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp*inTimeStamp, UInt32inBusNumber, UInt32inNumberFrames, AudioBufferList*ioData) { AudioUnitRender(m_audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, m_buffList); XDXAudioCaptureManager *manager = (__bridge XDXAudioCaptureManager *)inRefCon; /*Test audio fps static Float64 lastTime = 0; Float64 currentTime = CMTimeGetSeconds(CMClockMakeHostTimeFromSystemUnits(inTimeStamp->mHostTime))*1000; NSLog(@"Test duration - %f",currentTime - lastTime); lastTime = currentTime; */ void*bufferData = m_buffList->mBuffers[0].mData; UInt32bufferSize = m_buffList->mBuffers[0].mDataByteSize; //NSLog(@"demon = %d",bufferSize); if (manager.isRecordVoice) { [[XDXAudioFileHandler getInstance] writeFileWithInNumBytes:bufferSize ioNumPackets:inNumberFrames inBuffer:bufferData inPacketDesc:NULL]; } return noErr; } 複製程式碼
8. 停止audio unit
AudioOutputUnitStop
: 停止audio unit.
-(void)stopAudioCaptureWithAudioUnit:(AudioUnit)audioUnit isRunning:(BOOL *)isRunning { if (*isRunning == NO) { NSLog(@"%@:%s - stop capture repeat \n",kModuleName,__func__); return; } *isRunning = NO; if (audioUnit != NULL) { OSStatus status = AudioOutputUnitStop(audioUnit); if (status != noErr){ NSLog(@"%@:%s - stop audio unit failed. \n",kModuleName,__func__); }else { NSLog(@"%@:%s - stop audio unit successful",kModuleName,__func__); } } } 複製程式碼
9.釋放audio unit
當我們徹底不使用audio unit時,可以釋放本類audio unit相關的資源,注意釋放具有先後順序,首先應停止audio unit, 然後將初始化狀態還原,最後釋放audio unit所有相關記憶體資源.
- (void)freeAudioUnit:(AudioUnit)audioUnit { if (!audioUnit) { NSLog(@"%@:%s - repeat call!",kModuleName,__func__); return; } OSStatus result = AudioOutputUnitStop(audioUnit); if (result != noErr){ NSLog(@"%@:%s - stop audio unit failed.",kModuleName,__func__); } result = AudioUnitUninitialize(m_audioUnit); if (result != noErr) { NSLog(@"%@:%s - uninitialize audio unit failed, status : %d",kModuleName,__func__,result); } // It will trigger audio route change repeatedly result = AudioComponentInstanceDispose(m_audioUnit); if (result != noErr) { NSLog(@"%@:%s - dispose audio unit failed. status : %d",kModuleName,__func__,result); }else { audioUnit = nil; } } 複製程式碼