iOS学习笔记2-使用Audio Queues录音,取得实时PCM数据

1.学iOS接到的第一个项目就是需要用到实时录音,所以也就接触到了Audio Queues,苹果的录音相对安卓的较麻烦些,有以下两种常见录音方式:

(1)苹果推荐我们使用AVFoundation框架中的AVAudioPlayer和AVAudioRecorder类。虽然用法比较简单,但是不支持流式;这就意味着:在播放音频前,必须等到整个音频加载完成后,才能开始播放音频;录音时,也必须等到录音结束后,才能获取到录音数据。这给应用造成了很大的局限性。

适用场合:不需要实时处理音频的时候,比如录备忘录等。

(2)在iOS和Mac OS X中,音频队列Audio Queues是一个用来录制和播放音频的软件对象,也就是说,可以用来录音和播放,录音能够获取实时的PCM原始音频数据。

使用场合:需要拿到实时的PCM录音数据或者需要利用实时的PCM的音频数据去播放。

2.这里不详细介绍音频队列Audio Queues的实现原理,主要讲代码,如果大家仍未熟悉Audio Queues,可以参考这位牛人的博客:http://blog.csdn.net/jiangyiaxiu/article/details/9190035


实现代码如下:(录音部分)

(1)首先,需要定义一些常数:

#define kNumberAudioQueueBuffers 3  //定义了三个缓冲区
#define kDefaultBufferDurationSeconds 0.1279   //调整这个值使得录音的缓冲区大小为2048bytes
#define kDefaultSampleRate 8000   //定义采样率为8000

(2)接着,需要初始化录音的参数,在初始化时调用:

[self setupAudioFormat:kAudioFormatLinearPCM SampleRate:(int)self.sampleRate];

调用的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://stackoverflow.com/questions/4595532/audioqueuenewinput-callback-latency,

最后指出解决方法:

iOS学习笔记2-使用Audio Queues录音,取得实时PCM数据_第1张图片

好了,到此,我的笔记也写完了,希望大家一起探讨,多多指教;


我参考了以下网址的内容或者代码:

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

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

大家转载的话记得附上本博客地址!


你可能感兴趣的:(学习笔记)