iOS AudioUnit 总结
iOS 的 AudioUnit 功能十分强大,使用图的形式连接各个节点,来实现我们不通的需求,AUGraph
就是我们的图,,然后将 AudioUnit 连接到我们的图上面,就可以实现例如混音功能的实现。
AudioUnit 提供了低延迟的音频处理,可以实现,混音,回声消除,均衡器,压缩器,混响,滤波器等各个强大的功能。
这里有一篇官方文档 ,图画的很 nice
以下综合几个不错的博客,混在一起,当做个记录,以后会附上我自己的demo
每个 AudioUnit 都有 Element1 和 Element0,也就对应 input 和 output,因为i和o对应1和0很想,所以苹果的连个节点的定义就是这么来的,element0 控制输出,Element1 控制输入,也叫做 Bus,音频流从 input 输入,然后从 output 输出。
如果想使用扬声器播放,就必须将 AudioUnit 的 Element0的 outputScope 和 扬声器链接,如果想使用麦克风录音,就必须将 AudioUnit 的 Element1的 inputScope 和 麦克风链接。
下面选取我觉得比较好的博客,参考,参考,参考,参考,参考,参考
其中如果你读懂这篇,你就掌握 AudioUnit 的精髓了。
AudioUnit 初始化
播放使用AudioUnit,首先由3个相关的东西:AudioComponentDescription、AudioComponent和AudioComponentInstance。AudioUnit和AudioComponentInstance是一个东西,typedef定义的别名而已。
AudioComponentDescription是描述,用来做组件的筛选条件,类似于SQL语句where之后的东西。
AudioComponent是组件的抽象,就像类的概念,使用AudioComponentFindNext来寻找一个匹配条件的组件。
AudioComponentInstance是组件,就像对象的概念,使用AudioComponentInstanceNew构建。
k
//首先构造出要用到创建Unit的结构体
AudioComponentDescription ioUnitDescription;
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags = 0;
ioUnitDescription.componentFlagsMask = 0;
AudioComponent ioUnitRef = AudioComponentFindNext(NULL, &ioUnitDescription);
//创建AudioUnit实例
AudioComponentInstanceNew(ioUnitRef, &ioUnitInstance);
//首先构造出要用到创建Unit的结构体
AudioComponentDescription ioUnitDescription;
ioUnitDescription.componentType = kAudioUnitType_Output;
ioUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
ioUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
ioUnitDescription.componentFlags = 0;
ioUnitDescription.componentFlagsMask = 0;
//1 new
NewAUGraph(&processingGraph);
AUGraphAddNode(processingGraph, &ioUnitDescription, &ioNode);
//2 open
AUGraphOpen(processingGraph);
//3 从相应的Node中获得AudioUnit
AUGraphNodeInfo(processingGraph, ioNode, NULL, &ioUnit);
推荐使用第二种,扩展性更高,注意 AUNode 和 AudioUnit 必须成对出现
当我们控制Remote IO Unit的时候想告诉麦克风 各种input的参数 可以通过 一个叫ASBD 格式的结构体数据描述来设置给相应的Unit
UInt32 bytePerSample = sizeof(Float32);
AudioStreamBasicDescription asbd;
bzero(&asbd, sizeof(asbd));
asbd.mFormatID = kAudioFormatLinearPCM;
asbd.mSampleRate = 44100;
asbd.mChannelsPerFrame = channels;
asbd.mFramesPerPacket = 1;
asbd.mFormatFlags = kAudioFormatFlagsNativeFloatPacked | kAudioFormatFlagIsNonInterleaved;
asbd.mBitsPerChannel = 8 * bytePerSample;
asbd.mBytesPerFrame = bytePerSample;
asbd.mBytesPerPacket = bytePerSample;
mFormatID 可用来指定编码格式 eg:PCM
mSampleRate 采样率
mChannelsPerFrame 每个Frame有几个channel
mFramesPerPacket 每个Packet有几Frame
mFormatFlags 这个是用来描述声音格式表示格式的参数,上面代码我们指定的是每个sample的表示格式为Float格式,有点类似SInt16,如果后边是NonInterleaved代表非交错的,对于这个音频来讲就是左右声道的是非交错存放的,实际的音频数据会存储在一个AudioBufferList结构中的变量mBuffers中,如果mFormatFlags指定的是NonInterleaved,那么左声道就在会在mBuffers[0]里面,右声道就在mBuffers[1]里面.
mBitsPerChannel 表示一个声道的音频数据用多少位来表示,上面我们用的是Float来表示, 所以这里使用的是 8 乘以 每个采样的字节数来赋值.
mBytesPerFrame 和 mBytesPerPacket 这两个的赋值需要根据mFormatFlags 的值来进行分配,如果是NonInterleaved非交错的情况下, 就赋值bytePerSample(因为左右声道是分开的).但如果是Interleaved的话,那就应该是 bytePerSample * channels (因为左右声道是交错存放),这样才能表示一个Frame里面到底有多少byte.
如下代码 设置ASBD给相应的Audio Unit
AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &asbd, sizeof(asbd));
//设置ASBD
AudioStreamBasicDescription inputFormat;
inputFormat.mSampleRate = 44100;
inputFormat.mFormatID = kAudioFormatLinearPCM;
inputFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsNonInterleaved;
inputFormat.mFramesPerPacket = 1;
inputFormat.mChannelsPerFrame = 1;
inputFormat.mBytesPerPacket = 2;
inputFormat.mBytesPerFrame = 2;
inputFormat.mBitsPerChannel = 16;
//设置给输入端 配置麦克风输出的数据是什么格式
OSStatus status = noErr;
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
InputBus,
&inputFormat,
sizeof(inputFormat));
CheckStatus(status, @"AudioUnitGetProperty bus1 output ASBD error", YES);
构建AUGraph
NewAUGraph(&processingGraph);
...
status = AUGraphAddNode(processingGraph, &playDesc, &recordPlayNode);
...
status = AUGraphAddNode(processingGraph, &mixerDesc, &mixerNode);
status = AUGraphOpen(processingGraph);
NewAUGraph新建,然后不断通过AUGraphAddNode添加节点,也就是一个处理组件。最后AUGraphOpen打开。
AUGraphAddNode的3个参数分别是:要添加的AUGraph、节点性质描述和节点变量。
属性描述使用AudioComponentDescription对象,对于录音和播放都使用:
playDesc.componentType = kAudioUnitType_Output;
playDesc.componentSubType = kAudioUnitSubType_RemoteIO;
而混音组件是:
mixerDesc.componentType = kAudioUnitType_Mixer;
mixerDesc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
开启之后,使用status = AUGraphNodeInfo(processingGraph, recordPlayNode, NULL, &recordPlayUnit);获取node对应的AudioUnit。可以使用audioUnit的大量功能函数来做复杂的处理。
在node之间建立连接
status = AUGraphConnectNodeInput(processingGraph, mixerNode, 0, recordPlayNode, 0);
参数分别是:AUGraph变量、前一个node、前一个node的element索引、后一个node、后一个node的element索引。
每个node都可能有多个输入输出流,每个对应一个element,可以理解为机器的连接线之类的。上面的这段代码就是:把mixerNode的element0输出连接到recordPlayNode的element0。
使用AUGraphConnect的好处是不需要我们编程处理数据了,两个node之间连接好之后,系统会处理它们之间的数据传输。mixerNode是负责混音的节点,recordPlayNode即负责播放也负责录音(remoteIO的audioUnit固定两个element,一个录音一个播放),它的element0负责播放,所以最后一个参数传了0。而对于kAudioUnitSubType_MultiChannelMixer类型混音节点,输入可能有多个,但输出是一个,即element0。
所以上面这段代码的实际作用是:把混音结束后的音频流输出给播放组件。
我们可以通过设置 0来作为索引的值来指定element和bus。如果设置全局的scope的属性和参数的时候,我们需要设置element 的值是0,因为Global scope 只有一个element 0(element output)。
其实可以这么理解,scope 可以包含多个element元素,有时候我们需要设置scope 的属性和参数,有时候需要设置element 的属性和参数,而设置属性函数AudioUnitSetProperty每次需要指定scope 和element ,但是设置scope的属性和参数的值的时候是不需要element的,因此我们就把element 所在的位置设置成0就行了。
例如 kAudioUnitProperty_ElementCount 是用来配置混音器单元的element 的数量的,这就算是scope的属性了,因此,设置element 参数所在位置是0
而kAudioOutputUnitProperty_EnableIO 是用来开启I/O 的,是针对元素的,因此element 就要根据需要自己选择位置了。
OSStatus result = AudioUnitSetProperty (
ioUnit,
kAudioUnitProperty_ElementCount, // the property key
kAudioUnitScope_Input, // the scope to set the property on
0, // the element to set the property on
&busCount, // the property value
sizeof (busCount)
);
kAudioOutputUnitProperty_EnableIO 用于在I/O 单元上启动和禁止输入和输出。默认启用输出,关闭输入。
kAudioUnitProperty_ElementCount,用于配置混音器单元上的输入元素的数量
kAudioUnitProperty_MaximumFramesPerSlice 用于指定音频单元应准备响应于渲染调用而产生的音频数据的最大帧数。对应大多数音频设备,在大多数情况下,必须按照参考文档的说明来设置此属性。如果不这样设置,屏幕锁定时候音频将停止。
kAudioUnitProperty_StreamFormat 用于指定特定音频单元输入或者输出总线的音频流数据格式
I/O 单元的基本特征
上面有个官方文档的地址,里面有个图画的很好,
I/O 单元是app应用中常用的音频单元,而且在几个方面也是很特殊的。因此,我们需要了解I/O 单元的基本特性才能获取更好的音频单元编程。
I/O 音频单元有两个元素
虽然这两个elements是音频单元的组成部分,但是我们的app还是需要将他们视为独立的实体。 打个比方,我们可以根据需要使用kAudioOutputUnitProperty_EnableIO 属性来独立启用或者禁用每个element。
可以这么想,把I/O 音频单元相当于,输入音频单元和输出音频单元封装在一起了。
在 I/O 单元中的Element 1 直接连接到设备上的音频输入硬件,在图中是麦克风表示。element 1 和麦克风通过input scope 具体怎么连接,我们是不需要关心的。我们第一次获取的数据是来自output scope 的element 1 。
同理 element 0 直接连接在音频的输出硬件,例如图中的扬声器。数据传输也是首先经过input scope 中的 element 0 到达output scope 的 element0 ,在传递给扬声器的。
使用音频单元时,我们经常听到I/O单元的两个element ,而不是他们的编号是名称:
input element是 element 1(助记符设备:但是input的字母i具有类似数字1的外观)
output element 是 element 0(助记符设备:output的字母o具有类似于0 的外观)
上图,每个element 都有个一个input scope 和 output scope。因此,描述I/O 单元的这些部分可能有点混乱。例如,我们可说在一个同步的I/O APP中,我们从 element 1 通过output scope接受音频,并将音频通过input scope 传递给element 0。
上图,每个element 都有个一个input scope 和 output scope。因此,描述I/O 单元的这些部分可能有点混乱。例如,我们可说在一个同步的I/O APP中,我们从 element 1 通过output scope接受音频,并将音频通过input scope 传递给element 0。
最后,I/O 单元是唯一能够在音频处理graph中启动和停止音频流的音频单元。通过这种方式,I/O 音频单元负责音频单元应用中的音频流。
音频处理graph具有一个完整的 I / O单元
无论您是在进行录制,回放还是同步I / O,每个音频处理grahp都有一个I / O单元。I / O单元可以是iOS中的任何一个,具体取决于您的应用程序的需求。
graph 可以通过AUGraphStart 和AUGraphStop 函数启动和停止音频流。反过来,这些函数可以通过调用AudioOutputUnitStart 和AudioOutputUnitStop 函数将开始或者停止消息传递给I/O单元。
设置音频格式
AUGraphConnect可以建立连接后让系统处理,但对于更复杂的需求,还需要自己来手动处理音频数据。
在这之前要先设定音频格式,为了简便,固定3个输入源:索引0是第一个音频文件,1是录音数据,2是第二个音频文件。
for (int i = 0; i<MixerInputSourceCount; i++) {
if ([[self.audioChannelTypes objectForKey:@(i)] integerValue] == AUGraphMixerChannelTypeStereo) {
sourceStreamFmts[i] = *([[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
sampleRate:44100
channels:2
interleaved:NO].streamDescription);
}else{
sourceStreamFmts[i] = *([[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
sampleRate:44100
channels:1
interleaved:YES].streamDescription);
}
}
MixerInputSourceCount是输入源数量,根据设置的声道类型,来确定音频格式。两种格式的区别只是声道和interleaved这个属性。
在双声道时设为2,左边或右边单声道设为1。interleaved这个单词是"交错,交叉存取"的意思,这个在设为NO的时候,AudioBufferList包含两个AudioBuffer,每个负责一个声道的数据,而设为YES时,是一个AudioBuffer,两个声道的数据混在一起的。跟视频数据如YUV里面的plane的概念类似。
左右声道分开的好处是,可以单独的填充左边或右边的声音,比如把音频文件1的数据都只填充到第一个AudioBuffer里,那只有左边有声音。
给每个输入源设置回调和输入格式:
for (int i = 0; i<inputCount; ++i) {
AURenderCallbackStruct mixerInputCallback;
mixerInputCallback.inputProc = &mixerDataInput;
mixerInputCallback.inputProcRefCon = (__bridge void*)self;
status = AUGraphSetNodeInputCallback(processingGraph, mixerNode, i, &mixerInputCallback);
status = AudioUnitSetProperty(mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &mixStreamFmt, sizeof(AudioStreamBasicDescription));
}
这里变量i代表着输入源的索引,也是element的索引。在文档里,element和bus是同一个东西,都是指一个完整的数据流处理环境(context),和输入输出流是对应的。
读取文件
文件读取使用ExtAudioFile,这个据我了解,有两点很重要:1.自带转码 2.只处理pcm。
不仅是ExtAudioFile,包括其他audioUnit,其实应该是流数据处理的性质,这些组件都是“输入+输出”的这种工作模式,这种模式决定了你要设置输出格式、输出格式等。
ExtAudioFileOpenURL使用文件地址构建一个ExtAudioFile
文件里的音频格式是保存在文件里的,不用设置,反而可以读取出来,比如得到采样率用作后续的处理。
设置输出格式
AudioStreamBasicDescription clientDesc;
clientDesc.mSampleRate = fileDesc.mSampleRate;
clientDesc.mFormatID = kAudioFormatLinearPCM;
clientDesc.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
clientDesc.mReserved = 0;
clientDesc.mChannelsPerFrame = 1; //2
clientDesc.mBitsPerChannel = 16;
clientDesc.mFramesPerPacket = 1;
clientDesc.mBytesPerFrame = clientDesc.mChannelsPerFrame * clientDesc.mBitsPerChannel / 8;
clientDesc.mBytesPerPacket = clientDesc.mBytesPerFrame;
设置格式
size = sizeof(clientDesc);
status = ExtAudioFileSetProperty(audioFile, kExtAudioFileProperty_ClientDataFormat, size, &clientDesc);
在APP这一端的是client,在文件那一端的是file,带client代表设置APP端的属性。测试mp3文件的读取,是可以改变采样率的,即mp3文件采样率是11025,可以直接读取输出44100的采样率数据。
读取数据
ExtAudioFileRead(audioFile, framesNum, bufferList)
framesNum输入时是想要读取的frame数,输出时是实际读取的个数,数据输出到bufferList里。bufferList里面的AudioBuffer的mData需要分配内存。
AudioOutputUnitProperty_EnableIO,打开IO。默认情况element0,也就是从APP到扬声器的IO时打开的,而element1,即从麦克风到APP的IO是关闭的。使用AudioUnitSetProperty函数设置属性,它的几个参数分别作用是:1.要设置的audioUnit 2.属性名称 3.element, element0和element1选一个,看你是接收音频还是播放 4.scope也就是范围,这里是播放,我们要打开的是输出到系统的通道,使用kAudioUnitScope_Output 5.要设置的值 6.值的大小。
比较难搞的就是element和scope,需要理解audioUnit的工作模式,也就是最开始的两张图。
设置输入格式AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, renderAudioElement, &audioDesc, sizeof(audioDesc));,格式就用AudioStreamBasicDescription结构体数据。输出部分是系统控制,所以不用管。
然后是设置怎么提供数据。这里的工作原理是:audioUnit开启后,系统播放一段音频数据,一个audioBuffer,播完了,通过回调来跟APP索要下一段数据,这样循环,知道你关闭这个audioUnit。重点就是:1. 是系统主动来跟你索要,不是我们的程序去推送数据 2.通过回调函数。就像APP这边是工厂,而系统是商店,他们断货了或者要断货了,就来跟我们进货,直到你工厂倒闭了、不卖了等等
比较难搞的就是element和scope,需要理解audioUnit的工作模式,也就是最开始的两张图。
设置输入格式AudioUnitSetProperty(audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, renderAudioElement, &audioDesc, sizeof(audioDesc));,格式就用AudioStreamBasicDescription结构体数据。输出部分是系统控制,所以不用管。
然后是设置怎么提供数据。这里的工作原理是:audioUnit开启后,系统播放一段音频数据,一个audioBuffer,播完了,通过回调来跟APP索要下一段数据,这样循环,知道你关闭这个audioUnit。重点就是:1. 是系统主动来跟你索要,不是我们的程序去推送数据 2.通过回调函数。就像APP这边是工厂,而系统是商店,他们断货了或者要断货了,就来跟我们进货,直到你工厂倒闭了、不卖了等等
调整音量
AudioUnitSetParameter(mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, (UInt32)index, volume, 0);
把输入源封装成一个类,可以是文件、是录音、是网络数据流等,然后混音可以自由的组合和拆解各个输入源。不仅调节音量,或者还可以加上变调等,就跟使用滤镜处理图像一样。
混音输出可以加一个实时输出到文件,给mixer组件加一个renderCallback就可以拿到数据,然后可以输出到文件或者推送到服务器都没问题。
录音和混音之间加一个缓冲区,为了简便,是在mixer需要数据的时候调用AudioUnitRender,但混音需求数据的频率和录音输出数据的频率不一定一致,会导致某些数据丢失。
在其他iphone或mac试一下双声道录音是否可以得到两个声道数据不同。否则双声道没有意义了。
Effect Unit
|子类型| 用途说明| 子枚举类型|
均衡效果器 为声音的某些 频带 增强或衰减能量,效果器需要指定多个频带,然后为各频带设置增益最终改变声音在音域上的能量分布 kAudioUnitSubType_NBandEQ
压缩效果器 当声音较小或较大通过设置阀值来提高或降低声音能量 eg:作用时间、释放时间、以及触发值从而最终控制声音在时域上的能量范围 kAudioUnitSubType_DynamicsProcessor
混响效果器 通过声音反射的延迟控制声音效果 kAudioUnitSubType_Reverb2
Mixer Units
子类型 用途说明 子枚举类型
3D Mixer 仅支持 macOS
MultiChannelMixer 多路声音混音效果器,可以接受多路音频输入,还可以分别调整每一路的音频增益和开关,并将多路音频合成一路 kAudioUnitSubType_MultiChannelMixer
子类型 用途说明 子枚举类型
Remote I/O 采集音频与播放音频,在Audio Unit中使用麦克风和扬声器的时候会用到这个Unit kAudioUnitType_Output
Generic Output 进行离线处理,或者说AUGraph中不使用扬声器来驱动整个数据流,而希望使用一个输出(可以放入内存队列或者磁盘I/O操作)来驱动数据流时
子类型 用途说明 子枚举类型
AUConverter 格式转换,当某些效果器对输入的音频格式有明确要求时,或者我们将音频数据输入给一些其它的编码器进行编码。。。 kAudioUnitSubType_AUConverter
Time Pitch 变速变调效果器,调整声音音高. eg:会说话的Tom猫 kAudioUnitSubType_NewTimePitch
注意: AUConverter 如果由FFMpeg解码出来的PCM 是SInt16格式 如果要用格式转换效果器unit必须转成Float32格式表示的数据.
耳返
直播中的 耳返 就是用的这个把麦克风采集的数据直接扔给扬声器 这样就能做到 低延迟的实时听到麦克风的声音.
直播中一般使用 Remote I/O unit来进行采集工作
- 使用AudioUnit连接扬声器
OSStatus status = noErr;
UInt32 onFlag = 1;
UInt32 busZero = 0; //Element0 就是bus0
status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, busZero, &onFlag, sizeof(onFlag));
CheckStatus(status, @"不能连接扬声器", YES);
kAudioUnitScope_Output 就是连接扬声器的key.
- 连接麦克风
OSStatus status = noErr;
UInt32 busOne = 1; //Element1 就是bus1 接麦克风输入
UInt32 oneFlag = 1;
status = AudioUnitSetProperty(remoteIOUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, busOne, &oneFlag, sizeof(oneFlag));
CheckStatus(status, @"不能连接麦克风", YES);
- debug
可以使用如下代码检查每一步执行出错debug
static void CheckStatus(OSStatus status, NSString *message, BOOL fatal) {
if (status != noErr) {
char fourCC[16];
*(UInt32 *)fourCC = CFSwapInt32HostToBig(status);
fourCC[4] = '';
if (isprint(fourCC[0]) && isprint(fourCC[1]) &&
isprint(fourCC[2]) && isprint(fourCC[4])) {
NSLog(@"%@:%s",message, fourCC);
} else {
NSLog(@"%@:%d",message, (int)status);
}
if (fatal) {
exit(-1);
}
}
}