Audio Unit採集音訊實戰

小東邪發表於2019-05-11

需求

iOS中使用Audio unit實現音訊資料採集,直接採集PCM無損資料, Audio Unit不能直接採集壓縮資料,在以後的文章會講到音訊壓縮.


實現原理

使用Audio Unit採集硬體輸入端,如麥克風,其他外接具備麥克風功能裝置(帶麥的耳機,話筒等,前提是其本身要和蘋果相容).


閱讀前提

本文直接為實戰篇,如需瞭解理論基礎參考上述連結中的內容,本文側重於實戰中注意點.

本專案實現低耦合,高內聚,所以直接將相關模組拖入你的專案設定引數就可直接使用.


GitHub地址(附程式碼) : Audio Unit Capture

簡書地址 : Audio Unit Capture

掘金地址 : Audio Unit Capture

部落格地址 : Audio Unit Capture


具體實現

1.程式碼結構

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)formatID  sampleRate:(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: 通過先前建立的ASBD設定音訊資料流的格式
    • 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,
                                     UInt32                     inBusNumber,
                                     UInt32                     inNumberFrames,
                                     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;
    UInt32   bufferSize = 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;
    }
}
複製程式碼

10. 音訊檔案錄製

此部分可參考另一篇文章: 音訊檔案錄製

相關文章