1. 程式人生 > >iOS學習筆記2-使用Audio Queues錄音,取得實時PCM資料

iOS學習筆記2-使用Audio Queues錄音,取得實時PCM資料

1.學iOS接到的第一個專案就是需要用到實時錄音,所以也就接觸到了Audio Queues,蘋果的錄音相對安卓的較麻煩些,有以下兩種常見錄音方式:

(1)蘋果推薦我們使用AVFoundation框架中的AVAudioPlayer和AVAudioRecorder類。雖然用法比較簡單,但是不支援流式;這就意味著:在播放音訊前,必須等到整個音訊載入完成後,才能開始播放音訊;錄音時,也必須等到錄音結束後,才能獲取到錄音資料。這給應用造成了很大的侷限性。

適用場合:不需要實時處理音訊的時候,比如錄備忘錄等。

(2)在iOS和Mac OS X中,音訊佇列Audio Queues是一個用來錄製和播放音訊的軟體物件,也就是說,可以用來錄音和播放,錄音能夠獲取實時的PCM原始音訊資料。

使用場合:需要拿到實時的PCM錄音資料或者需要利用實時的PCM的音訊資料去播放。

實現程式碼如下:(錄音部分)

(1)首先,需要定義一些常數:

#define kNumberAudioQueueBuffers 3  //定義了三個緩衝區
#define kDefaultBufferDurationSeconds 0.1279   //調整這個值使得錄音的緩衝區大小為2048bytes
#define kDefaultSampleRate 8000   //定義取樣率為8000

(2)接著,需要初始化錄音的引數,在初始化時呼叫:
[self setupAudioFormat:kAudioFormatLinearPCM SampleRate:(int)self.sampleRate];</span>

呼叫的setupAudioFormat函式如下:

// 設定錄音格式
- (void)setupAudioFormat:(UInt32) inFormatID SampleRate:(int)sampeleRate
{
    //重置下
    memset(&_recordFormat, 0, sizeof(_recordFormat));
    
    //設定取樣率,這裡先獲取系統預設的測試下 //TODO:
    //取樣率的意思是每秒需要採集的幀數
    _recordFormat.mSampleRate = sampeleRate;//[[AVAudioSession sharedInstance] sampleRate];
    
    //設定通道數,這裡先使用系統的測試下 //TODO:
    _recordFormat.mChannelsPerFrame = 1;//(UInt32)[[AVAudioSession sharedInstance] inputNumberOfChannels];
    
    //    NSLog(@"sampleRate:%f,通道數:%d",_recordFormat.mSampleRate,_recordFormat.mChannelsPerFrame);
    
    //設定format,怎麼稱呼不知道。
    _recordFormat.mFormatID = inFormatID;
    
    if (inFormatID == kAudioFormatLinearPCM){
        //這個屌屬性不知道幹啥的。,//要看看是不是這裡屬性設定問題
        _recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        //每個通道里,一幀採集的bit數目
        _recordFormat.mBitsPerChannel = 16;
        //結果分析: 8bit為1byte,即為1個通道里1幀需要採集2byte資料,再*通道數,即為所有通道採集的byte數目。
        //所以這裡結果賦值給每幀需要採集的byte數目,然後這裡的packet也等於一幀的資料。
        //至於為什麼要這樣。。。不知道。。。
        _recordFormat.mBytesPerPacket = _recordFormat.mBytesPerFrame = (_recordFormat.mBitsPerChannel / 8) * _recordFormat.mChannelsPerFrame;
        _recordFormat.mFramesPerPacket = 1;
    }
}

(3)設定好格式後,可以繼續下一步,
-(void)startRecording
{
    NSError *error = nil;
    //設定audio session的category
    BOOL ret = [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];//注意,這裡選的是AVAudioSessionCategoryPlayAndRecord引數,如果只需要錄音,就選擇Record就可以了,如果需要錄音和播放,則選擇PlayAndRecord,這個很重要
  if (!ret) {
        NSLog(@"設定聲音環境失敗");
        return;
    }
    //啟用audio session
    ret = [[AVAudioSession sharedInstance] setActive:YES error:&error];
    if (!ret)
    {
        NSLog(@"啟動失敗");
        return;
    }
    
    _recordFormat.mSampleRate = self.sampleRate;//設定取樣率,8000hz
    
    //初始化音訊輸入佇列
    AudioQueueNewInput(&_recordFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &_audioQueue);//inputBufferHandler這個是回撥函式名

    //計算估算的快取區大小
    int frames = (int)ceil(self.bufferDurationSeconds * _recordFormat.mSampleRate);//返回大於或者等於指定表示式的最小整數
    int bufferByteSize = frames * _recordFormat.mBytesPerFrame;//緩衝區大小在這裡設定,這個很重要,在這裡設定的緩衝區有多大,那麼在回撥函式的時候得到的inbuffer的大小就是多大。
    NSLog(@"緩衝區大小:%d",bufferByteSize);
    
    //建立緩衝器
    for (int i = 0; i < kNumberAudioQueueBuffers; i++){
        AudioQueueAllocateBuffer(_audioQueue, bufferByteSize, &_audioBuffers[i]);
        AudioQueueEnqueueBuffer(_audioQueue, _audioBuffers[i], 0, NULL);//將 _audioBuffers[i]新增到佇列中
    }
    
    // 開始錄音
    AudioQueueStart(_audioQueue, NULL);
    
    self.isRecording = YES;
}
(4)執行AudioQueueStart後,接下來的就剩下編寫回調函式的內容了:
//相當於中斷服務函式,每次錄取到音訊資料就進入這個函式
//inAQ 是呼叫回撥函式的音訊佇列
//inBuffer 是一個被音訊佇列填充新的音訊資料的音訊佇列緩衝區,它包含了回撥函式寫入檔案所需要的新資料
//inStartTime 是緩衝區中的一取樣的參考時間,對於基本的錄製,你的毀掉函式不會使用這個引數
//inNumPackets是inPacketDescs引數中包描述符(packet descriptions)的數量,如果你正在錄製一個VBR(可變位元率(variable bitrate))格式, 音訊佇列將會提供這個引數給你的回撥函式,這個引數可以讓你傳遞給AudioFileWritePackets函式. CBR (常量位元率(constant bitrate)) 格式不使用包描述符。對於CBR錄製,音訊佇列會設定這個引數並且將inPacketDescs這個引數設定為NULL,官方解釋為The number of packets of audio data sent to the callback in the inBuffer parameter.

void inputBufferHandler(void *inUserData, AudioQueueRef inAQ, AudioQueueBufferRef inBuffer, const AudioTimeStamp *inStartTime,UInt32 inNumPackets, const AudioStreamPacketDescription *inPacketDesc)
{
    NSLog(@"we are in the 回撥函式\n");
    CSRecorder *recorder = (__bridge CSRecorder*)inUserData;

    if (inNumPackets > 0) {

        NSLog(@"in the callback the current thread is %@\n",[NSThread currentThread]);
            [recorder processAudioBuffer:inBuffer withQueue:inAQ];    //在這個函式你可以用錄音錄到得PCM資料:inBuffer,去進行處理了   

    }
    
    if (recorder.isRecording) {
        AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
    }
}

(5)關於如何停止:
-(void)stopRecording
{
    NSLog(@"stop recording out\n");//為什麼沒有顯示
    if (self.isRecording)
    {
        self.isRecording = NO;

        //停止錄音佇列和移除緩衝區,以及關閉session,這裡無需考慮成功與否
        AudioQueueStop(_audioQueue, true);
        AudioQueueDispose(_audioQueue, true);//移除緩衝區,true代表立即結束錄製,false代表將緩衝區處理完再結束
        [[AVAudioSession sharedInstance] setActive:NO error:nil];
        
    }
}

至此,你應該能夠錄到實時的PCM語音資料了。

但,在我實際寫的過程中,我遇到了一下幾個問題,特此筆記,供大家討論:

(1)網上有人的程式碼是用c++寫的,官網給的例子speakhere也是用c++寫的,而我用的是objective-c寫的,我查了下,發現有人說用objective-c寫會有記憶體洩露,發生在這句:

AudioQueueNewInput(&_recordFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &_audioQueue);//inputBufferHandler這個是回撥函式名(objective-c的寫法)

而用c++的寫法是:

AudioQueueNewOutput(&mDataFormat, AQPlayer::AQBufferCallback, this,CFRunLoopGetCurrent(), kCFRunLoopCommonModes, 0, &mQueue);(speakhere中C++的寫法)

差異在於(__bridge void *)(self)和this,有人說這裡導致了記憶體洩露,我這裡還搞不明白;

(2)錄音時呼叫回撥函式的時間問題:

理論上講,我們錄音的時候將引數設定好,那麼回撥函式就會根據我們設定的緩衝區的buffer大小去進行等間隔呼叫,比如我8000hz的取樣率,每次採16bit,那麼1s的話總共會採了16000bytes,我的buffer設定成2048個位元組的話,那麼應該是2048/16000=0.128s左右呼叫一次回撥函式,但實際上我發現不是這樣子的,比如我的呼叫回撥函式的列印結果如下:

2015-07-20 16:45:53.235 HelloWorld[4115:239431] bufferByteSize is :2048

2015-07-20 16:45:53.291 HelloWorld[4115:239431] we are turely begin recording

2015-07-20 16:45:53.802 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:53.803 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:53.803 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:53.803 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:54.313 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:54.313 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:54.314 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:54.314 HelloWorld[4115:239511] we are in callback

2015-07-20 16:45:54.824 HelloWorld[4115:239511] we are in callback

實際的現象是開始錄音後,從53.291-53.802s用了0.5s左右開始進入第一個回撥函式,接著,是幾乎同時呼叫了四個回撥函式,然後再間隔0.5s左右又重新呼叫4個回撥函式,我試著僅修改官網的例子speakhere裡的緩衝區buffer的大小,但是也出現同樣地情況,這個類似於在前面提到的一篇部落格《音訊佇列服務程式設計指南(Audio Queue Services Programming Guide)(二)》“在錄製或播放過程中,音訊佇列將反覆的呼叫它所擁有的音訊佇列回撥函式。呼叫的時間間隔取決於音訊佇列緩衝區的容量,並且一般來一說這個時間在半秒或者幾秒”。

這個問題我也糾結了很久,後來自己總結了問題所在,但不確定是否正確:

原因:

AudioQueueNewInput(&_recordFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &_audioQueue);//inputBufferHandler這個是回撥函式名
這個函式的第四個和第五個引數是有關於執行緒的,我設定成null,代表它預設使用內部執行緒去錄音,而且還是非同步的,所以在我的緩衝區buffer比較小的情況下,就會出現同時出現4個回撥函式的情況,應該是這個原因。

我還在stackoverflow尋找這個問題的答案,發現也有人遇到這個問題,相關問題網址是:

最後指出解決方法:


好了,到此,我的筆記也寫完了,希望大家一起探討,多多指教;

我參考了以下網址的內容或者程式碼:

http://www.cnblogs.com/anjohnlv/p/3383908.html

http://blog.sina.com.cn/s/blog_c13ee7440102ux0t.html

大家轉載的話記得附上本部落格地址!