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
[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;
}
}
-(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;
}
//相当于中断服务函数,每次录取到音频数据就进入这个函数
//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);
}
}
-(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,
最后指出解决方法:
好了,到此,我的笔记也写完了,希望大家一起探讨,多多指教;
我参考了以下网址的内容或者代码:
http://www.cnblogs.com/anjohnlv/p/3383908.html
http://blog.sina.com.cn/s/blog_c13ee7440102ux0t.html
大家转载的话记得附上本博客地址!