Core Audio
Core Audio是iOS和OS X中处理音频的框架集合,具有高性能,低延迟的优点。Core Audio在iOS中的框架有:Audio Toolbox,Audio Unit,AV Foundation,OpenAL
录音方案
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。其录制和播放示意图如下:
大致原理就是,使用缓存队列来达到实时录音和播放的效果,以录音为例,麦克风采集的PCM数据首先填充到队首的缓存中,缓存充满时就会出队,触发回调函数,可以在回调的时候做修音处理,写入文件,播放等操作,然后就清空改缓存,并将该缓存加入到队尾,等待填充,此过程一直循环,播放的过程同理。
注意:我们可以通过设置缓存的大小,来控制回调的时间,从而实时处理音频。其计算如下:
回调时间 ≈ 采样率 * 采样位数 / 缓存大小(注意是近似值!)
Audio Queue的录音方案使用比较简单,能够实时处理音频,但是也有其局限性,它的实时性不够准确,有一定的延迟,即回调函数的时间不稳定。当采样率为44100,位数为16,缓存大小为8820,根据公式回调时间约等于100ms,准确值为92.9ms(稍后解释)时,回调时间如下:
可以看到回调间隔多数是93ms,也有一些波动,第三次到第四次是105ms,而且回调间隔越小,波动就越大,比如将缓存大小设置为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 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);
通过以上设置,可以实时录音,并实时播放(本例中,输入输出都打开了)。
几个问题
- 在真机上运行的时候,会报错,错误信息如下:
这是因为没有开启录音权限,以source code的方式打开Info.plist文件,在dict标签中加入以下属性:
NSMicrophoneUsageDescription
microphoneDesciption
再次运行,就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实时录音