Audio Queue 採集音頻實戰(支持不一樣格式)

需求

iOS中使用Audio Queue實現音頻數據採集,直接採集PCM無損數據或AAC及其餘壓縮格式數據.ios


實現原理

使用Audio Queue採集硬件輸入端,如麥克風,其餘外置具有麥克風功能設備(帶麥的耳機,話筒等,前提是其自己要和蘋果兼容).git


閱讀前提

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

本項目實現低耦合,高內聚,因此直接將相關模塊拖入你的項目設置參數就可直接使用.


GitHub地址(附代碼) : Audio Queue Capture

簡書地址 : Audio Queue Capture

博客地址 : Audio Queue Capture

掘金地址 : Audio Queue Capture


具體實現

1.代碼結構

1

如上所示,咱們整體分爲兩大類,一個是負責採集的類,一個是負責作音頻錄製的類,你能夠根據需求在適當時機啓動,關閉Audio Queue, 而且在Audio Queue已經啓動的狀況下能夠進行音頻文件錄製,前面需求僅僅須要以下四個API便可完成.github

// Start / Stop Audio Queue
[[XDXAudioQueueCaptureManager getInstance] startAudioCapture];
[[XDXAudioQueueCaptureManager getInstance] stopAudioCapture];

// Start / Stop Audio Record
[[XDXAudioQueueCaptureManager getInstance] startRecordFile];
[[XDXAudioQueueCaptureManager getInstance] stopRecordFile];
複製代碼

2.定義類中常量變量

  • 如下兩個參數描述在採集PCM數據時對於iOS平臺而言必須填入的信息
#define kXDXAudioPCMFramesPerPacket 1
#define kXDXAudioPCMBitsPerChannel 16
複製代碼
  • 定義一個結構體存儲音頻相關屬性,包括音頻流格式,Audio Queue引用及Audio Queue隊列中所使用的全部buffer組成的數據.
struct XDXRecorderInfo {
    AudioStreamBasicDescription  mDataFormat;
    AudioQueueRef                mQueue;
    AudioQueueBufferRef          mBuffers[kNumberBuffers];
};
typedef struct XDXRecorderInfo *XDXRecorderInfoType;
複製代碼
  • 定義一個全局變量判斷當前Audio Queue是否正在工做.另外一個變量爲當前是否正在錄製
@property (nonatomic, assign, readonly) BOOL isRunning;
@property (nonatomic, assign) BOOL isRecordVoice;
複製代碼
  • 注意

由於Audio Queue中自己就是用純C語言實現的,因此它會直接調用一些函數,咱們必需要理解函數跟OC方法的區別,以及指針的概念,由於函數中會出現一些相似&運算符,這裏能夠簡單給你們介紹下以便小白閱讀. &就是獲取某個對象的內存地址,使用它主要爲了知足讓Audio Queue的API能夠將其查詢到的值直接賦給這段內存地址,好比下面會講到的AudioSessionGetProperty查詢方法中就是這樣將查詢出來的值賦值給咱們定義的全局靜態變量的.macos

2.初始化並啓動Audio Queue

  • 本例經過XDXSingleton實現單例模式,即頭文件中使用SingletonH,實現文件中使用SingletonM便可,關於單例的實現自行百度.

爲何使用單例,由於iPhone中輸入端只能接收一個音頻輸入設備,因此若是使用Audio Queue採集,該採集對象在應用程序聲明週期內應該是單一存在的,因此使用單例實現.緩存

  • 首先爲記錄音頻信息的指向結構體的指針分配內存空間
+ (void)initialize {
    m_audioInfo = malloc(sizeof(struct XDXRecorderInfo));
}

複製代碼
  • 下面定義了公共啓動接口,你能夠直接在其中設置你須要的音頻參數,如音頻數據格式爲PCM仍是AAC,採樣率大小,聲道數,採樣時間等.
- (void)startAudioCapture {
    [self startAudioCaptureWithAudioInfo:m_audioInfo
                                 formatID:kAudioFormatMPEG4AAC // kAudioFormatLinearPCM
                               sampleRate:44100
                             channelCount:1
                              durationSec:0.05
                                isRunning:&_isRunning];
}

複製代碼

3. 設置音頻流數據格式

  • 注意點

須要注意的是,音頻數據格式與硬件直接相關,若是想獲取最高性能,最好直接使用硬件自己的採樣率,聲道數等音頻屬性,因此,如採樣率,當咱們手動進行更改後,Audio Queue會在內部自行轉換一次,雖然代碼上沒有感知,但必定程序上仍是下降了性能.bash

iOS中不支持直接設置雙聲道,若是想模擬雙聲道,能夠自行填充音頻數據,具體會在之後的文章中講到,喜歡請持續關注.數據結構

  • 獲取音頻屬性值

理解AudioSessionGetProperty函數,該函數代表查詢當前硬件指定屬性的值,以下,kAudioSessionProperty_CurrentHardwareSampleRate爲查詢當前硬件採樣率,kAudioSessionProperty_CurrentHardwareInputNumberChannels爲查詢當前採集的聲道數.由於本例中使用手動賦值方式更加靈活,因此沒有使用查詢到的值.函數

  • 設置不一樣格式定製的屬性

首先,你必須瞭解未壓縮格式(PCM...)與壓縮格式(AAC...). 使用iOS直接採集未壓縮數據是能夠直接拿到硬件採集到的數據,而若是直接設置如AAC這樣的壓縮數據格式,其原理是Audio Queue在內部幫咱們作了一次轉換,具體原理在本文開篇中的閱讀前提中去查閱.oop

使用PCM數據格式必須設置採樣值的flag:mFormatFlags,每一個聲道中採樣的值換算成二進制的位寬mBitsPerChannel,iOS中每一個聲道使用16位的位寬,每一個包中有多少幀mFramesPerPacket,對於PCM數據而言,由於其未壓縮,因此每一個包中僅有1幀數據.每一個包中有多少字節數(即每一幀中有多少字節數),能夠根據以下簡單計算得出post

注意,若是是其餘壓縮數據格式,大多數不須要單獨設置以上參數,默認爲0.這是由於對於壓縮數據而言,每一個音頻採樣包中壓縮的幀數以及每一個音頻採樣包壓縮出來的字節數多是不一樣的,因此咱們沒法預知進行設置,就像mFramesPerPacket參數,由於壓縮出來每一個包具體有多少幀只有壓縮完成後才能得知.

audioInfo->mDataFormat = [self getAudioFormatWithFormatID:formatID
                                                   sampleRate:sampleRate
                                                 channelCount:channelCount];
                                                 
                                                 
-(AudioStreamBasicDescription)getAudioFormatWithFormatID:(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;
    
    // Set audio format
    dataFormat.mFormatID = formatID;
    
    // Set detail audio format params
    if (formatID == kAudioFormatLinearPCM) {
        dataFormat.mFormatFlags     = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        dataFormat.mBitsPerChannel  = kXDXAudioPCMBitsPerChannel;
        dataFormat.mBytesPerPacket  = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
        dataFormat.mFramesPerPacket = kXDXAudioPCMFramesPerPacket;
    }else if (formatID == kAudioFormatMPEG4AAC) {
        dataFormat.mFormatFlags = kMPEG4Object_AAC_Main;
    }

    NSLog(@"Audio Recorder: starup PCM audio encoder:%f,%d",sampleRate,channelCount);
    return dataFormat;
}
複製代碼

4. 初始化併爲Audio Queue分配內存

上面步驟中咱們已經拿到音頻流數據格式,使用AudioQueueNewInput函數能夠將建立出來的Audio Queue對象賦值給咱們定義的全局變量,另外還指定了CaptureAudioDataCallback採集音頻數據回調函數的名稱.回調函數的定義必須聽從以下格式.由於系統會將採集到值賦值給此函數中的參數,函數名稱能夠本身指定.

typedef void (*AudioQueueInputCallback)(
                                    void * __nullable               inUserData,
                                    AudioQueueRef                   inAQ,
                                    AudioQueueBufferRef             inBuffer,
                                    const AudioTimeStamp *          inStartTime,
                                    UInt32                          inNumberPacketDescriptions,
                                    const AudioStreamPacketDescription * __nullable inPacketDescs);
複製代碼
// New queue
    OSStatus status = AudioQueueNewInput(&audioInfo->mDataFormat,
                                         CaptureAudioDataCallback,
                                         (__bridge void *)(self),
                                         NULL,
                                         kCFRunLoopCommonModes,
                                         0,
                                         &audioInfo->mQueue);
    
    if (status != noErr) {
        NSLog(@"Audio Recorder: AudioQueueNewInput Failed status:%d \n",(int)status);
        return NO;
    }
    
複製代碼

如下是AudioQueueNewInput函數的定義

  • inFormat: 音頻流格式
  • inCallbackProc: 設置回調函數
  • inUserData: 開發者本身定義的任何數據,通常將本類的實例傳入,由於回調函數中沒法直接調用OC的屬性與方法,此參數能夠做爲OC與回調函數溝通的橋樑.即傳入本類對象.
  • inCallbackRunLoop: 回調函數在哪一個循環中被調用.設置爲NULL爲默認值,即回調函數所在的線程由audio queue內部控制.
  • inCallbackRunLoopMode: 回調函數運行循環模式一般使用kCFRunLoopCommonModes.
  • inFlags: 系統保留值,只能爲0.
  • outAQ:將建立好的audio queue賦值給填入對象.
extern OSStatus             
AudioQueueNewInput(                 const AudioStreamBasicDescription *inFormat,
                                    AudioQueueInputCallback         inCallbackProc,
                                    void * __nullable               inUserData,
                                    CFRunLoopRef __nullable         inCallbackRunLoop,
                                    CFStringRef __nullable          inCallbackRunLoopMode,
                                    UInt32                          inFlags,
                                    AudioQueueRef __nullable * __nonnull outAQ)          API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));
複製代碼
5. 獲取設置的音頻流格式

用如下方法驗證獲取到音頻格式是否與咱們設置的相符.

// Set audio format for audio queue
    UInt32 size = sizeof(audioInfo->mDataFormat);
    status = AudioQueueGetProperty(audioInfo->mQueue,
                                   kAudioQueueProperty_StreamDescription,
                                   &audioInfo->mDataFormat,
                                   &size);
    if (status != noErr) {
        NSLog(@"Audio Recorder: get ASBD status:%d",(int)status);
        return NO;
    }
複製代碼

6. 計算Audio Queue中每一個buffer的大小

該計算要區分壓縮與未壓縮數據.

  • 壓縮數據

只能進行估算,即用採樣率與採樣時間相乘,可是須要注意由於直接設置採集壓縮數據(如AAC),至關因而Audio Queue在內部本身進行一次轉換,而像AAC這樣的壓縮數據,每次至少須要1024個採樣點(即採樣時間最小爲23.219708 ms)才能完成一個壓縮,因此咱們不能將buffer size設置太小,不信能夠本身嘗試,若是設置太小直接crash.

而咱們計算出來的這個大小隻是原始數據的大小,通過壓縮後每每低於咱們計算出來的這個值.能夠在回調中打印查看.

  • 未壓縮數據

對於未壓縮數據,咱們時能夠經過計算精確得出採樣的大小. 即以下公式

// Set capture data size
    UInt32 bufferByteSize;
    if (audioInfo->mDataFormat.mFormatID == kAudioFormatLinearPCM) {
        int frames = (int)ceil(durationSec * audioInfo->mDataFormat.mSampleRate);
        bufferByteSize = frames*audioInfo->mDataFormat.mBytesPerFrame*audioInfo->mDataFormat.mChannelsPerFrame;
    }else {
        // AAC durationSec MIN: 23.219708 ms
        bufferByteSize = durationSec * audioInfo->mDataFormat.mSampleRate;
        
        if (bufferByteSize < 1024) {
            bufferByteSize = 1024;
        }
    }
複製代碼

7. 內存分配,入隊

關於audio queue,能夠理解爲一個隊列的數據結構,buffer就是隊列中的每一個結點.具體設計請參考文中閱讀前提中的概念篇.

官方建議咱們將audio queue中的buffer設置爲3個,由於,一個用於準備去裝數據,一個正在使用的數據以及若是出現I/0緩存時還留有一個備用數據,設置過少,採集效率可能變低,設置過多浪費內存,3個剛恰好.

以下操做就是先爲隊列中每一個buffer分配內存,而後將分配好內存的buffer作入隊操做,準備接收音頻數據

// Allocate and Enqueue
    for (int i = 0; i != kNumberBuffers; i++) {
        status = AudioQueueAllocateBuffer(audioInfo->mQueue,
                                              bufferByteSize,
                                          &audioInfo->mBuffers[i]);
        if (status != noErr) {
            NSLog(@"Audio Recorder: Allocate buffer status:%d",(int)status);
        }
        
        status = AudioQueueEnqueueBuffer(audioInfo->mQueue,
                                         audioInfo->mBuffers[i],
                                         0,
                                         NULL);
        if (status != noErr) {
            NSLog(@"Audio Recorder: Enqueue buffer status:%d",(int)status);
        }
    }
複製代碼

8. 啓動Audio Queue

第二個參數設置爲NULL表示當即開始採集數據.

status = AudioQueueStart(audioInfo->mQueue, NULL);
    if (status != noErr) {
        NSLog(@"Audio Recorder: Audio Queue Start failed status:%d \n",(int)status);
        return NO;
    }else {
        NSLog(@"Audio Recorder: Audio Queue Start successful");
        *isRunning = YES;
        return YES;
    }
複製代碼

9. 回調函數中接收音頻數據.

若是上面的操做所有執行成功,最終系統會將採集到的音頻數據以回調函數形式返回給開發者,以下.

  • inUserData: 註冊回調函數時傳入的開發者自定義的對象
  • inAQ: 當前使用的Audio Queue
  • inBuffer: Audio Queue產生的音頻數據
  • inStartTime其中包含音頻數據產生的時間戳
  • inNumberPacketDescriptions: 數據包描述參數.若是你正在錄製VBR格式,音頻隊列會提供此參數的值.若是錄製文件須要將其傳遞給AudioFileWritePackets函數.CBR格式不使用此參數(值爲0).
  • inPacketDescs: 音頻數據中一組packet描述.若是是VBR格式數據,若是錄製文件須要將此值傳遞給AudioFileWritePackets函數

經過回調函數,就能夠拿到當前採集到的音頻數據,你能夠對數據作你須要的任何自定義操做.如下以寫入文件爲例,咱們在拿到音頻數據後,將其寫入音頻文件.

static void CaptureAudioDataCallback(void *                                 inUserData,
                                     AudioQueueRef                          inAQ,
                                     AudioQueueBufferRef                    inBuffer,
                                     const AudioTimeStamp *                 inStartTime,
                                     UInt32                                 inNumPackets,
                                     const AudioStreamPacketDescription*    inPacketDesc) {
    
    XDXAudioQueueCaptureManager *instance = (__bridge XDXAudioQueueCaptureManager *)inUserData;
    
    /*  Test audio fps
    static Float64 lastTime = 0;
    Float64 currentTime = CMTimeGetSeconds(CMClockMakeHostTimeFromSystemUnits(inStartTime->mHostTime))*1000;
    NSLog(@"Test duration - %f",currentTime - lastTime);
    lastTime = currentTime;
    */
    
    // NSLog(@"Test data: %d,%d,%d,%d",inBuffer->mAudioDataByteSize,inNumPackets,inPacketDesc->mDataByteSize,inPacketDesc->mVariableFramesInPacket);
    
    if (instance.isRecordVoice) {
        UInt32 bytesPerPacket = m_audioInfo->mDataFormat.mBytesPerPacket;
        if (inNumPackets == 0 && bytesPerPacket != 0) {
            inNumPackets = inBuffer->mAudioDataByteSize / bytesPerPacket;
        }
        
        [[XDXAudioFileHandler getInstance] writeFileWithInNumBytes:inBuffer->mAudioDataByteSize
                                                      ioNumPackets:inNumPackets
                                                          inBuffer:inBuffer->mAudioData
                                                      inPacketDesc:inPacketDesc];
    }
    
    if (instance.isRunning) {
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}
複製代碼

10. 中止Audio Queue並回收內存

  • AudioQueueStop: 中止當前audio queue
  • AudioQueueFreeBuffer: 釋放audio queue中每一個buffer
  • AudioQueueDispose: 釋放audio queue

如下函數調用具備前後順序,咱們必須先停掉audio queue,才能釋放其中buffer的內存,最後再將整個audio queue完全釋放.

-(BOOL)stopAudioQueueRecorderWithAudioInfo:(XDXRecorderInfoType)audioInfo isRunning:(BOOL *)isRunning {
    if (*isRunning == NO) {
        NSLog(@"Audio Recorder: Stop recorder repeat \n");
        return NO;
    }
    
    if (audioInfo->mQueue) {
        OSStatus stopRes = AudioQueueStop(audioInfo->mQueue, true);
        
        if (stopRes == noErr){
            for (int i = 0; i < kNumberBuffers; i++)
                AudioQueueFreeBuffer(audioInfo->mQueue, audioInfo->mBuffers[i]);
        }else{
            NSLog(@"Audio Recorder: stop AudioQueue failed.");
            return NO;
        }
        
        OSStatus status = AudioQueueDispose(audioInfo->mQueue, true);
        if (status != noErr) {
            NSLog(@"Audio Recorder: Dispose failed: %d",status);
            return NO;
        }else {
            audioInfo->mQueue = NULL;
            *isRunning = NO;
            //        AudioFileClose(mRecordFile);
            NSLog(@"Audio Recorder: stop AudioQueue successful.");
            return YES;
        }
    }
    
    return NO;
}

複製代碼

11. 音頻文件錄製

此部分可參考另外一篇文章: 音頻文件錄製

補充

當音頻數據爲壓縮數據時,原本能夠經過一個函數求出每一個音頻數據包中最大的音頻數據大小,以進一步求出buffer size,但不知爲什麼調用一直失敗,因此在上述第6步中我才換了種方式估算.若是有人知道能夠評論補充下,感謝.

UInt32 propertySize = sizeof(maxPacketSize);
            OSStatus status     = AudioQueueGetProperty(audioQueue,
                                                        kAudioQueueProperty_MaximumOutputPacketSize,
                                                        &maxPacketSize,
                                                        &propertySize);
            if (status != noErr) {
                NSLog(@"%s: get max output packet size failed:%d",__func__,status);
            }
複製代碼
相關文章
相關標籤/搜索