iOS实时录音


Core Audio

Core Audio是iOS和OS X中处理音频的框架集合,具有高性能,低延迟的优点。Core Audio在iOS中的框架有:Audio Toolbox,Audio Unit,AV Foundation,OpenAL


iOS Core Audio architecture

录音方案

AVFoundation:提供AVAudioPlayer,AVAudioRecorder类,以及简单的OC接口,录音过程是把音频录制成音频文件,播放过程是播放音频文件,适合处理非实时的场景。
Audio Unit:Audio Unit在音频开发中处于最底层,可以实时获取和播放PCM数据,具有响应快,低延迟的优点,适用于低延迟实时场景。
Audio ToolBox:基于Audio Unit,提供Core Audio中层和高层服务的接口,包括Audio Session Services,AudioQueueService(音频队列)。音频队列是另一种录音方案,将录制的音频放置在队列中,取出播放。
OpenAL:基于Audio Unit,主要提供跨平台的接口。
可以看到,实时录音方案有两种,本文主要讲述这两种方式的特点。


Audio Queue

关于Audio Queue的知识,网上有很多比较好的总结,如果英文阅读无障碍,可以阅读官方文档的详细说明Audio Queue Services Programming Guide。其录制和播放示意图如下:

A recording audio queue
A playback audio queue

大致原理就是,使用缓存队列来达到实时录音和播放的效果,以录音为例,麦克风采集的PCM数据首先填充到队首的缓存中,缓存充满时就会出队,触发回调函数,可以在回调的时候做修音处理,写入文件,播放等操作,然后就清空改缓存,并将该缓存加入到队尾,等待填充,此过程一直循环,播放的过程同理。
注意:我们可以通过设置缓存的大小,来控制回调的时间,从而实时处理音频。其计算如下:

回调时间 ≈ 采样率 * 采样位数 / 缓存大小(注意是近似值!)

Audio Queue的录音方案使用比较简单,能够实时处理音频,但是也有其局限性,它的实时性不够准确,有一定的延迟,即回调函数的时间不稳定。当采样率为44100,位数为16,缓存大小为8820,根据公式回调时间约等于100ms,准确值为92.9ms(稍后解释)时,回调时间如下:

缓存为8820的回调时间

可以看到回调间隔多数是93ms,也有一些波动,第三次到第四次是105ms,而且回调间隔越小,波动就越大,比如将缓存大小设置为4410,回调时间如下:

缓存为4410的回调时间

这个时候波动已经很明显了,第二次到第三次甚至出现了7ms的情况。在实时场景中,每次调用表示一帧,在帧大小要求精细的时候,这样的误差是难以接受的,需要更稳定的录音方式。

思考:为什么会出现波动的情况?解决方法?

这种波动的原因是在Audio Queue的底层产生的,之前说过,Audio ToolBox是基于Audio Unit的,回调函数的波动要到底层才能解决。
[图片上传失败...(image-7780d3-1522826744091)]
可以猜想一下,底层可能有并发的线程,并发使得回调函数时间出现随机性,就会产生波动,甚至出现例子中7ms调用两次的情况。关于这一点,可以参考stackoverflow的讨论AudioQueueNewInput callback latency中的回答:

The Audio Queue API looks like it is built on top of the Audio Unit RemoteIO API. Small Audio Queue buffers are probably being used to fill a larger RemoteIO buffer behind the scenes. Perhaps even some rate resampling might be taking place (on the original 2G phone).
For lower latency, try using the RemoteIO Audio Unit API directly, and then requesting the audio session to provide your app a smaller lower latency buffer size.

可以看到,使用低延迟的录音方式,需要使用更底层的Audio Unit。


Audio Unit

关于Audio Unit的介绍,官方文档Audio Unit Hosting Guide for iOS解释的很详细,Audio Unit通常工作在一个封闭的上下文中,称之为audio processing graph,如下:

Audio Unit processing

麦克风采集到的音频输送到audio processing graph中,音频数据经过两路EQ unit(均衡),然后Mixer unit(混合),最终到与输出设备直接相连的I/O unit。这个过程可以看到,Audio Unit是对音频的直接处理,甚至可以将unit输出到外设,相比于音频队列的配置,Audio Unit要更复杂,下面详细介绍使用Audio Unit实现实时录音的例子。
Audio Unit的构建方式分为两种,一种是直接使用Unit API,一种是使用Audio Unit Graph,下面采用第一种方式。

AudioUnit audioUnit;

关于AudioUnit的解释:

The type used to represent an instance of a particular audio component

表示的结构如下:
[图片上传失败...(image-ee41ac-1522826744092)]
接下来就要构建Unit的结构,在不同的音频应用中,可以构建各种不同的结构,一个简单的结构如下:
[图片上传失败...(image-dc3dac-1522826744092)]
确定了结构,开始配置工作了。

配置AudioSession

和其他录音播放一样,需要配置录音播放的环境,响应耳机事件等。

NSError *error;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];

[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
[audioSession setPreferredSampleRate:44100 error:&error];
[audioSession setPreferredInputNumberOfChannels:1 error:&error];
[audioSession setPreferredIOBufferDuration:0.05 error:&error];

配置AudioComponentDescription

AudioComponentDescription是用来描述unit 的类型,包括均衡器,3D混音,多路混音,远端输入输出,VoIP输入输出,通用输出,格式转换等,在这里使用远端输入输出。

AudioComponentDescription audioDesc;
audioDesc.componentType = kAudioUnitType_Output;
audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;

AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
AudioComponentInstanceNew(inputComponent, &audioUnit);

配置输入输出的数据格式

设置采样率为44100,单声道,16位的格式,注意输入输出都要设置。

AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = 44100;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 1;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 2;
audioFormat.mBytesPerFrame = 2;

AudioUnitSetProperty(audioUnit,
                     kAudioUnitProperty_StreamFormat,
                     kAudioUnitScope_Output,
                     INPUT_BUS,
                     &audioFormat,
                     sizeof(audioFormat));
AudioUnitSetProperty(audioUnit,
                     kAudioUnitProperty_StreamFormat,
                     kAudioUnitScope_Input,
                     OUTPUT_BUS,
                     &audioFormat,
                     sizeof(audioFormat));

打开输入输出端口

在默认情况下,输入是关闭的,输出是打开的。在unit的Element中,Input用“1”(和I很像)表示,Output用“0”(和O很像)表示。

UInt32 flag = 1;

AudioUnitSetProperty(audioUnit,
                     kAudioOutputUnitProperty_EnableIO,
                     kAudioUnitScope_Input,
                     INPUT_BUS,
                     &flag,
                     sizeof(flag));
AudioUnitSetProperty(audioUnit,
                     kAudioOutputUnitProperty_EnableIO,
                     kAudioUnitScope_Input,
                     OUTPUT_BUS,
                     &flag,
                     sizeof(flag));

配置回调

根据应用的场景需求,可以在输入输出设置回调,以输入回调为例:

AURenderCallbackStruct recordCallback;
recordCallback.inputProc = RecordCallback;
recordCallback.inputProcRefCon = (__bridge void *)self;
AudioUnitSetProperty(audioUnit,
                     kAudioOutputUnitProperty_SetInputCallback,
                     kAudioUnitScope_Global,
                     INPUT_BUS,
                     &recordCallback,
                     sizeof(recordCallback));

需要定义回调函数,回调函数是AURenderCallback类型的,按照AUComponent.h中定义的参数类型,定义出输入回调函数:

static OSStatus RecordCallback(void *inRefCon,
                           AudioUnitRenderActionFlags *ioActionFlags,
                           const AudioTimeStamp *inTimeStamp,
                           UInt32 inBusNumber,
                           UInt32 inNumberFrames,
                           AudioBufferList *ioData)
{

AudioUnitRender(audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, buffList);

return noErr;
}

分配缓存

这是获取录音数据很重要的一步,需要分配缓存来存储实时的录音数据。如果不这样做,录音数据也可以在输出的时候获取,但意义不一样,获取录音数据应该在输入回调中完成,而不是输出回调。

UInt32 flag = 0;
AudioUnitSetProperty(audioUnit,
                     kAudioUnitProperty_ShouldAllocateBuffer,
                     kAudioUnitScope_Output,
                     INPUT_BUS,
                     &flag,
                     sizeof(flag));

buffList = (AudioBufferList*)malloc(sizeof(AudioBufferList));
buffList->mNumberBuffers = 1;
buffList->mBuffers[0].mNumberChannels = 1;
buffList->mBuffers[0].mDataByteSize = 2048 * sizeof(short);
buffList->mBuffers[0].mData = (short *)malloc(sizeof(short) * 2048);

通过以上设置,可以实时录音,并实时播放(本例中,输入输出都打开了)。


几个问题

  1. 在真机上运行的时候,会报错,错误信息如下:
真机运行错误信息

这是因为没有开启录音权限,以source code的方式打开Info.plist文件,在dict标签中加入以下属性:

<key>NSMicrophoneUsageDescription</key>
<string>microphoneDesciption</string>

再次运行,就OK了。

2.回调时间间隔问题。
Audio Unit的延迟很低,回调时间非常稳定,很适合严格地实时处理音频,即使把时间设置成0.000725623582766秒,回调时间依然很准:

回调间隔很短

事实上,Audio Unit没有回调间隔的配置,但是我们可以通过上下文环境配置,即:

[audioSession setPreferredIOBufferDuration:0.05 error:&error];

这样设置duration为0.05秒,表示每隔0.05秒就去读取缓存数据。假设采样率为44100,采样位数16,这时buffer大小应该为44100 * 0.05 * 16 / 8 = 4410,但是,Audio Unit 的buffer的大小是2的幂次方,那么就不可能有4410,这时buffer实际大小为4096,反过来计算时间就是0.0464秒,这也就解释了在Audio Queue中近似计算回调时间的原因了。
除此之外,如果不用AudioSession设置时间的话,会有一个默认大小的buffer,这个大小在模拟器和真机上不相同,所以为了程序可控,这个设置很有必要。

3.关于播放问题
测试发现,用耳机的效果更好,不用耳机在播放的时候会有噪声。如果想获得清晰的效果,可以将每次的PCM数据写入到文件,然后回放。推荐使用Lame,这个可以将PCM转换成MP3。

4.读取PCM数据
PCM数据存放在AudioBuffer的结构体中,音频数据是void *类型的数据:

/*!
    @struct         AudioBuffer
    @abstract       A structure to hold a buffer of audio data.
    @field          mNumberChannels
                        The number of interleaved channels in the buffer.
    @field          mDataByteSize
                        The number of bytes in the buffer pointed at by mData.
    @field          mData
                        A pointer to the buffer of audio data.
*/
struct AudioBuffer
{
    UInt32              mNumberChannels;
    UInt32              mDataByteSize;
    void* __nullable    mData;
};
typedef struct AudioBuffer  AudioBuffer;

如果采样位数是16位,即2Byte,即mData中每2Byte是一个PCM数据,以获取第一个数据为例:

short *data = (short *)buffList->mBuffers[0].mData;
NSLog(@"%d", data[0]);

这里需要注意的就是类型转换的时候位数要一致。

DEMO

Audio Unit实时录音

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容