AudioUnit添加背景音乐(六)
前言
如果我要将自己录制的音频和另外一首音乐混合,如何做呢?这种场景非常常见,K歌应用中,将自己的录音和伴奏混合,视频录制在,给自己录制的声音添加一点背景音乐等等。AudioUnit可以实现吗?答案是完全可以,接下来我们将讲解如何用AudioUnit实现音频的混合,实现的关键组件就是AudioUnit的Mixer Unit。那有人就问了,标题为离线音频混合,撒意思呢。
AudioUnit音频系列
AudioUnit之-播放裸PCM音频文件(一)
AudioUnit之-录制音频+耳返(二)
AudioUnit之-录制音频保存为m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三)
AudioUnit之-录制音频并添加背景音乐(四)
AudioUnit之-generic output(离线)混合音频文件(五)
实现思路
首先看一下来自官网的一个常见混音应用场景的Augraph流程图
AudioProcessingGraphBeforeEQ_2x.png
先看最右边的RemoteIO Unit的 Element0的 Input scope,它连接着 Multichannel Mixer unit的唯一的输出 scope out。这里的Multichannel Mixer unit就是一种用于音频混合的混音组件。Multichannel Mixer unit左边有两个输入Input scope(其实可以有任意多个输入Input scope,这里以两个为例),用于接收需要混音的各个音频的数据
总结一下,要在扬声器中播放背景音乐的同时带有耳返效果,那么只需要将录制的音频数据作为Multichannel Mixer unit一个输入(假设为上图中input 0),背景音乐音频数据作为Multichannel Mixer unit另外一个输入(假设为上图中的input 1),然后Multichannel Mixer unit内部进行混音处理,最后将结果音频数据输入给扬声器的Element 0的input scope即可。
音轨混合的原理
在讲解具体的代码之前,先了解一下混音的原理。
1、声音混合从物理学角度讲就是将声波进行叠加,而声波的叠加等价于量化的语音信号的叠加
2、多路音轨混合的前提:
==需要叠加的音轨具有相同的采样频率,采样精度和采样通道,如果不相同,则需要先让他们相同
====1、不同采样频率需要算法进行重新采样处理
====2、不同采样精度则通过算法将精度保持一样,精度向上扩展和精度向下截取
====3、不同通道数也是和精度类似处理方式
3、音轨混合算法:
比如线性叠加平均、自适应混音、多通道混音等等
==线性叠加平均:原理就是把不同音轨的各个通道值(对应的每个声道的值)叠加之后取平均值,优点不会有噪音,缺点是如果某一路或几路音量特别小那么导致整个混音结果的音量变小
伪代码 音轨1:a11b11c11a12b12c12a13b13c13
音轨2:a21b21c21a22b22c22a23b23c23
混音: (a11+a21)/2(b11+b21)/2(c13+c23)/2
自适应混音:根据每路音轨所占的比例权重进行叠加,具体算法有很多种,这里不详解
多通道混音:将每路音轨分别放到各个声道上,就好比如果有两路音轨,则一路音轨放到左声道,一路音轨放到右声道。
其实混音算法有很多种,也可以进行很多优化,好在IOS平台为我们提供了功能强大的Mixer Unit组件,我们只需要配置相关混合参数即可实现多路音频的混合,不需要了解具体的实现算法。
Mixer Unit音频混合代码使用步骤
备注:我们这里先假设混合的音频具有相同的采样率,声道数,采样位数
1、创建混音器描述,添加到AuGraph
// 创建混音器描述;使用混音功能的时候才用到
_mixerDes = [ADUnitTool comDesWithType:kAudioUnitType_Mixer subType:kAudioUnitSubType_MultiChannelMixer fucture:kAudioUnitManufacturer_Apple];
混音器对应的Type和SubType分别是kAudioUnitType_Mixer和kAudioUnitSubType_MultiChannelMixer,注意SubType其实还有多种,比如kAudioUnitSubType_MatrixMixer/kAudioUnitSubType_SpatialMixer等等,这里只是讲解常用的kAudioUnitSubType_MultiChannelMixer
status = AUGraphAddNode(_augraph, &_mixerDes, &_mixerNode);
status = AUGraphNodeInfo(_augraph, _mixerNode, NULL, &_mixerUnit);
2、设置混音器参数
混音器有多个输入,但是只有一个输出。所以要手动设置混音器输入音轨数目,这里因为要混合录音和背景音乐,所以为2。
/** 指定混音器的输入音轨数目,这里是混合的音频文件和录音的音频数据,所以是两个
* 备注:混音器可以有多个输入,但是只有一个输出,AudioUnitElement值为0
*/
UInt32 mixerInputcount = _isEnablePlayWhenRecord?2:1;
AudioUnitSetProperty(_mixerUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &mixerInputcount, sizeof(mixerInputcount));
指定混音器的采样率(非必须)
status = AudioUnitSetProperty(_mixerUnit, kAudioUnitProperty_SampleRate, kAudioUnitScope_Output, 0, &rate, sizeof(rate));
设置AudioUnitRender()函数在处理输入数据时,最大的输入吞吐量(非必须)
UInt32 maximumFramesPerSlice = 4096;
AudioUnitSetProperty (
_ioUnit,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global,
0,
&maximumFramesPerSlice,
sizeof (maximumFramesPerSlice)
);
设置各路音频混合后的音量
CheckStatusReturn(AudioUnitSetParameter(_mixerUnit,kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, _volume[0], 0), @"AudioUnitSetProperty inputASDB");
CheckStatusReturn(AudioUnitSetParameter(_mixerUnit,kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 1, _volume[1], 0), @"AudioUnitSetProperty inputASDB");
这个参数可以控制各路音频在混音时音量的大小,挺有用的
为混音器配置输入
// 为混音器配置输入
for (int i=0; i<mixerCount; i++) {
AURenderCallbackStruct callback;
callback.inputProc = mixerInputDataCallback;
callback.inputProcRefCon = (__bridge void*)self;
status = AudioUnitSetProperty(_mixerUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, i, &callback, sizeof(callback));
if (status != noErr) {
NSLog(@"AudioUnitSetProperty kAudioUnitProperty_SetRenderCallback %d",status);
}
status = AudioUnitSetProperty(_mixerUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i, &_mixerStreamDesForInput, sizeof(_mixerStreamDesForInput));
if (status != noErr) {
NSLog(@"AudioUnitSetProperty kAudioUnitProperty_StreamFormat %d",status);
}
}
这里看到kAudioUnitProperty_SetRenderCallback是不是觉得很眼熟,没错,它和前面讲的播放PCM音频数据时 播放回调的使用方法是一样的,意思就是混音组件的各个input scope将通过该回调来索要数据,那么app要在该回调中按照_mixerStreamDesForInput的数据格式给它的各个input scope输送数据
3、配置Augraph各个Unit的连接顺序
前面在播放和录制音频时,Augraph只有convert和remoteIO两个Unit,按照上面的图示,应该要按照如下的代码来配置各个Unit的连接
混音器的输出作为扬声器的输入
OSStatus status = noErr;
AUGraphConnectNodeInput(_augraph, _mixerNode, 0, _ioNode, 0);
扬声器的输入则通过上面步骤配置的mixerInputDataCallback回调来获取
4、现在来看一下mixerInputDataCallback的代码
static OSStatus mixerInputDataCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData)
{
AudioUnitRecorder *recorder = ((__bridge AudioUnitRecorder*)inRefCon);
NSLog(@"输出 时间 %.2f 序号 %d frames %d",inTimeStamp->mSampleTime,inBusNumber,inNumberFrames);
if (inBusNumber == 0) { // 代表录音
// 将录音的数据填充进来
AudioUnitRender(recorder->_ioUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, ioData);
} else if (inBusNumber == 1){ // 代表音频文件
// 从音频文件中读取数据并填充进来
[recorder->_dataReader readFrames:&inNumberFrames toBufferData:ioData];
}
return noErr;
}
这个函数的意思就是,我们需要往ioData中填充前面指定格式的inNumberFrames个音频数据
inBusNumber == 0时代表是此时要输入录制的音频数据,这里用AudioUnitRender()来获取录制的音频数据,
inBusNumber == 1时代表背景音乐的音频数据,这里用上一篇文章中封装的从文件中读取音频数据类接口实现,代码可以具体参考上一篇文章AudioUnit之-录制音频保存为m4a/CAF/WAV文件和播放m4a/CAF/WAV文件(三)
至此,播放音频的同时实现耳返效果关键代码以全部实现,具体请参考工程