前言
上一篇文章初步了解了音频混合的原理及如何使用Mixer Unit混合音频的流程,学习了如何在录制音频时播放背景音乐开启耳返效果。那有人就会提出疑问,我录制时只需要播放背景音乐不想开启耳返,录制结束后再将录制的声音和背景音乐混合,这要如何实现?这是一个很好的问题,也是一个很常用的场景,对于问题的前半部分,我想只要学会了前面几篇文章,很简单实现,对于后半部分问题该如何做呢?离线音频混合,离线与在线是个相对的概念,在线就是音频数据最终会通过扬声器播放出来。离线的意思就是音频数据不发送给扬声器,由APP自己处理
实现思路
还是先回顾一下上一篇文章的AuGraph流程,这里用文字描述
录制音频-->(input 0)
混音器 --> Remote IO(Element0 output scope)-->扬声器
背景音乐-->(input 1)
它的驱动流程为:首先扬声器索向混音器索要数据,混音器的各个input scope通过设置的回调向app要数据,app在回调中输入数据,所以回调的调用最终是由扬声器驱动的,那么回调调用的频率也是跟扬声器播放有关的(基本是固定的)。
那么离线音频混合只要将流程中最后一步的扬声器(Remote IO) Unit 组件换成离线的generic output unit组件即可,其它一模一样,如下:
录制音频-->(input 0)
混音器 --> generic output(Element0 output scope)--->app
背景音乐-->(input 1)
离线处理组件generic output流程介绍
1、创建generic output
AudioComponentDescription generic = [ADUnitTool comDesWithType:kAudioUnitType_Output subType:kAudioUnitSubType_GenericOutput fucture:kAudioUnitManufacturer_Apple];
generic output的type和sub type分别为kAudioUnitType_Output和kAudioUnitSubType_GenericOutput,该Unit 有一个Element0
备注:
一个Augraph中只能有一个kAudioUnitType_Output类型的I/O Unit,否则调用AUGraphInitialize()初始化Augraph的时候回返回 -10862的错误
2、创建离线渲染线程
remoteIO Unit是由系统内部自驱动的,对于generic output Unit就需要我们手动来驱动了,原理就是创建一个离线渲染的线程,在这个线程中不停的调用AudioUnitRender()函数 伪代码如下:
AudioBufferList *bufferList = (AudioBufferList*)malloc(sizeof(AudioBufferList));
while(mOfflineRend) {
OSStatus status = AudioUnitRender(......bufferList);
if(status == 0) {
// 渲染成功
保存bufferList中音频数据的代码
} else {
渲染失败或离屏渲染输入端没有数据了,则退出线程
break;
}
}
每次执行AudioUnitRender()函数,它将驱动generic output向其input scope要数据,然后generic output的input scope在向连接它的Unit要数据(这里是mixer unit的输出端),以此类推
具体代码为:
/** 离线渲染的驱动原则:
* RemoteIO的驱动原理是:系统硬件(扬声器或者麦克风)会定期采集数据和向客户端要数据渲染,如果有设置回调函数,那么回调函数将被调用
* Generic Output不同的是,它需要手动调用AudioUnitRender()函数将AudioUnit中的数据渲染出来
*/
- (void)offlineRenderThread
{
NSLog(@"离线渲染线程 ==>%@",[NSThread currentThread]);
// 第一部分
AudioStreamBasicDescription outputASDB;
UInt32 outputASDBSize = sizeof(outputASDB);
CheckStatusReturn(AudioUnitGetProperty(_genericUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, &outputASDB,&outputASDBSize),@"get property fail");
[ADUnitTool printStreamFormat:outputASDB];
if (outputASDB.mBitsPerChannel == 0) { // 说明没有解析成功
_offlineRun = NO;
return;
}
AudioUnitRenderActionFlags flags = 0;
AudioTimeStamp inTimeStamp;
memset(&inTimeStamp, 0, sizeof(inTimeStamp));
inTimeStamp.mFlags = kAudioTimeStampSampleTimeValid;
inTimeStamp.mSampleTime = 0;
UInt32 framesPerRead = 512*2;
UInt32 bytesPerFrame = outputASDB.mBytesPerFrame;
int channelCount = outputASDB.mChannelsPerFrame;
int bufferListcout = channelCount;
// 第二部分
// 不停的向 generic output 要数据;放在外面,避免重复创建和分配内存
AudioBufferList *bufferlist = (AudioBufferList*)malloc(sizeof(AudioBufferList)+sizeof(AudioBuffer)*(channelCount-1));
if (outputASDB.mFormatFlags & kAudioFormatFlagIsNonInterleaved) { // planner 存储方式
bufferlist->mNumberBuffers = channelCount;
bufferListcout = channelCount;
} else { // packet 存储方式
bufferlist->mNumberBuffers = 1;
bufferListcout = 1;
}
for (int i=0; i<bufferListcout; i++) {
AudioBuffer buffer = {0};
buffer.mNumberChannels = 1;
buffer.mDataByteSize = framesPerRead*bytesPerFrame;
buffer.mData = (void*)calloc(framesPerRead, bytesPerFrame);
bufferlist->mBuffers[i] = buffer;
}
UInt32 totalFrames = _totalFrames;
while (_offlineRun && totalFrames > 0) {
if (totalFrames < framesPerRead) {
framesPerRead = totalFrames;
} else {
totalFrames -= framesPerRead;
}
// 第三部分
// 从generic output unit中将数据渲染出来
/** 遇到问题:返回-50;
* 解决方案:在AudioUnit框架中 -50错误代表参数不正确的意思,经过反复检查发现是bufferlist的格式与_genericUnit要输出的数据格式不一致导致,具体
* 情况为:
* _genericUnit为sigendInteger的packet格式,而分配的bufferlist确是sigendInteger planner格式(mNumberBuffers指定为2了),两边不一致
* 所以将两者保持一直即可
*
* tips:
* 调用此函数将向_genericUnit要数据,如果_genericUnit有设置inputCallBack回调,那么回调函数将被调用。
*/
OSStatus status = AudioUnitRender(_genericUnit,&flags,&inTimeStamp,0,framesPerRead,bufferlist);
if (status != noErr) {
NSLog(@"出错了 即将结束写入");
_offlineRun = NO;
}
inTimeStamp.mSampleTime += framesPerRead;
// 将渲染得到的数据保存下来
[_extFileWriter writeFrames:framesPerRead toBufferData:bufferlist];
}
// 第四部分
// 释放内存
for (int i=0; i<bufferListcout; i++) {
if (bufferlist->mBuffers[i].mData != NULL) {
free(bufferlist->mBuffers[i].mData);
bufferlist->mBuffers[i].mData = NULL;
}
}
if (bufferlist != NULL) {
free(bufferlist);
bufferlist = NULL;
}
/** 遇到问题:生成的混音文件无法正常播放
* 问题原因:通过ExtAudioFileRef生成的封装格式的文件必须要调用ExtAudioFileDispose()函数才会正常生成音频文件属性,由于没有调用导致此问题
* 解决方案:混音结束后调用ExtAudioFileDispose()即可。
*/
[_extFileWriter closeFile];
NSLog(@"渲染线程结束");
if (self.completeBlock) {
self.completeBlock();
}
}
上面有四部分的关键代码,下面一一讲解
第一部分:
1、获取generic output的输出端的数据格式
2、配置UInt32 framesPerRead = 512*2;表示每次渲染的音频frames个数
3、配置AudioUnitRenderActionFlags flags = 0;默认为0 即可
4、配置AudioTimeStamp inTimeStamp; mFlags设置为kAudioTimeStampSampleTimeValid,mSampleTime初始值为0,后面没调用一次AudioUnitRender()函数,值增加framesPerRead数目
3、设置mixerUnit输入回调
首先要配置Augraph中各个Unit的连接顺序
// 将文件1和文件2的音频 输入mixer进行混音处理
CheckStatusReturn(AUGraphConnectNodeInput(_auGraph, _source1Node, 0, _mixerNode, 0), @"AUGraphConnectNodeInput 1");
CheckStatusReturn(AUGraphConnectNodeInput(_auGraph, _source2Node, 0, _mixerNode, 1),@"AUGraphConnectNodeInput 2");
// 将混音的结果作为generic out的输入
CheckStatusReturn(AUGraphConnectNodeInput(_auGraph, _mixerNode, 0, _genericNode, 0),@"AUGraphConnectNodeInput _genericNode");
这里解释一下,上一篇文章中 混音器的input0和input1是设置的输入回调来索要数据,这里直接将两个AufioFilePlayer Unit的输出与之相连,意思就是input0和input1分别从对应的文件中读取数据(这里也可以采用回调方式,读者有兴趣的可以自己尝试)
4、创建AudioFilePlayer Unit,并配置相关参数
其实AudiofilePlayerUnit与前面将的ExtAudioFileRef使用非常类似,这里贴出关键代码
创建AudioFilePlayerUnit 它的Type和subtype分别为kAudioUnitType_Generator和kAudioUnitSubType_AudioFilePlayer
AudioComponentDescription source1 = [ADUnitTool comDesWithType:kAudioUnitType_Generator subType:kAudioUnitSubType_AudioFilePlayer fucture:kAudioUnitManufacturer_Apple];
CheckStatusReturn(AUGraphAddNode(_auGraph, &source1, &_source1Node),@"AUGraphAddNode _source1Node error");
AudioComponentDescription source2 = [ADUnitTool comDesWithType:kAudioUnitType_Generator subType:kAudioUnitSubType_AudioFilePlayer fucture:kAudioUnitManufacturer_Apple];
CheckStatusReturn(AUGraphAddNode(_auGraph, &source2, &_source2Node),@"AUGraphAddNode _source2Node");
打开音频文件并配置相关参数
- (void)setupSource:(NSString*)sourcePath sourceFileID:(AudioFileID)sourceFileID player:(AudioUnit)sourceplayer
{
CFURLRef sourceUrl = (__bridge CFURLRef)[NSURL fileURLWithPath:sourcePath];
// 打开文件,并生成一个文件句柄sourceFileID;第三个参数表示文件的封装格式后缀,如果为0,表示自动检测
CheckStatusReturn(AudioFileOpenURL(sourceUrl, kAudioFileReadPermission, 0, &sourceFileID), @"AudioFileOpenURL fail");
// 获取文件本身的数据格式(根据文件的信息头解析,未解压的,根据文件属性获取)
AudioStreamBasicDescription fileASBD;
UInt32 propSize = sizeof(fileASBD);
CheckStatusReturn(AudioFileGetProperty(sourceFileID, kAudioFilePropertyDataFormat,&propSize, &fileASBD),
@"setUpAUFilePlayer couldn't get file's data format");
/** 遇到问题:获取音频文件中packet数目时返回kAudioFileBadPropertySizeError错误
* 解决方案:kAudioFilePropertyAudioDataPacketCount的必须是UInt64 类型,替换即可
*/
// 获取文件中的音频packets数目
UInt64 nPackets;
propSize = sizeof(nPackets);
CheckStatusReturn(AudioFileGetProperty(sourceFileID, kAudioFilePropertyAudioDataPacketCount,&propSize, &nPackets),
@"setUpAUFilePlayer AudioFileGetProperty[kAudioFilePropertyAudioDataPacketCount] failed");
// 指定要播放的文件句柄;要想成功读取音频文件中数据,先要将该文件加入指定的AudioUnit中(AudioFilePlayer AudioUnit)
CheckStatusReturn(AudioUnitSetProperty(sourceplayer, kAudioUnitProperty_ScheduledFileIDs,
kAudioUnitScope_Global, 0, &sourceFileID, sizeof(sourceFileID)),
@"setUpAUFilePlayer AudioUnitSetProperty[kAudioUnitProperty_ScheduledFileIDs] failed");
// 指定从音频在读取数据的方式;前面将要播放的文件加入了AudioUnit,这里指定要播放的范围(比如是播放整个文件还是播放部分文件),播放方式
// (比如是否循环播放)等等
ScheduledAudioFileRegion rgn;
memset(&rgn.mTimeStamp, 0, sizeof(rgn.mTimeStamp));
rgn.mTimeStamp.mFlags = kAudioTimeStampSampleTimeValid; // 播放整个文件,这里必须为此值
rgn.mTimeStamp.mSampleTime = 0; // 播放整个文件,这里必须为此值
rgn.mCompletionProc = NULL; // 数据读取完毕之后的回调函数
rgn.mCompletionProcUserData = NULL; // 传给回调函数的对象
rgn.mAudioFile = sourceFileID; // 要读取的文件句柄
rgn.mLoopCount = 0; // 是否循环读取,0不循环,-1 一直循环 其它值循环的具体次数
rgn.mStartFrame = 0; // 读取的起始的frame 索引
rgn.mFramesToPlay = (UInt32)nPackets * fileASBD.mFramesPerPacket; // 从读取的起始frame 索引开始,总共要读取的frames数目
if (_totalFrames < rgn.mFramesToPlay) {
_totalFrames = rgn.mFramesToPlay;
}
/** 遇到问题:返回-10867
* 解决思路:设置kAudioUnitProperty_ScheduledFileRegion前要先调用AUGraphInitialize
* (_auGraph);初始化AUGraph
*/
CheckStatusReturn(AudioUnitSetProperty(sourceplayer, kAudioUnitProperty_ScheduledFileRegion,
kAudioUnitScope_Global, 0,&rgn, sizeof(rgn)),
@"setUpAUFilePlayer AudioUnitSetProperty[kAudioUnitProperty_ScheduledFileRegion] failed");
// 指定从音频文件中读取音频数据的行为,必须读取指定的frames数(也就是defaultVal设置的值,如果为0表示采用系统默认的值)才返回,否则就等待
// 这一步要在前一步骤之后设定
UInt32 defaultVal = 0;
CheckStatusReturn(AudioUnitSetProperty(sourceplayer, kAudioUnitProperty_ScheduledFilePrime,
kAudioUnitScope_Global, 0, &defaultVal, sizeof(defaultVal)),
@"setUpAUFilePlayer AudioUnitSetProperty[kAudioUnitProperty_ScheduledFilePrime] failed");
// 指定从音频文件读取数据的开始时间和时间间隔,此设定会影响AudioUnitRender()函数
AudioTimeStamp startTime;
memset (&startTime, 0, sizeof(startTime));
startTime.mFlags = kAudioTimeStampSampleTimeValid; // 要想mSampleTime有效,要这样设定
startTime.mSampleTime = -1; // 表示means next render cycle 否则按照这个指定的数值
CheckStatusReturn(AudioUnitSetProperty(sourceplayer, kAudioUnitProperty_ScheduleStartTimeStamp,
kAudioUnitScope_Global, 0, &startTime, sizeof(startTime)),
@"setUpAUFilePlayer AudioUnitSetProperty[kAudioUnitProperty_ScheduleStartTimeStamp]");
}
第二部分:
分配AudioBufferList内存,注意对于planner格式和packet格式创建方式不一致,这个很重要,要配置对,否则会返回-50错误。
详细参考上面代码
第三部分:
索要数据OSStatus status = AudioUnitRender(_genericUnit,&flags,&inTimeStamp,0,framesPerRead,bufferlist);
第四部分:
释放资源
总结:
可以看一下离线音频混合的速度还是很快的,这里我测试了混合两个时长为1分46秒的音频文件,耗时大概不到一秒钟。
给录音添加背景音乐
接下来完成上一篇文章中未完成的一个需求,就是录制音频时播放背景音乐,但是关闭耳返效果,结束后混合录制的声音和背景音乐并生成一个新的音频文件。
想一下,是不是很简单了,其实就是组合前面学习的单个部分的功能,先画一下流程图
1563169685852.jpg
代码这里就不写了,这个功能实际上就是前面所学单一功能的一个组合,具体可以参考工程代码
遇到问题
1、生成的混音文件无法正常播放
问题原因:通过ExtAudioFileRef生成的封装格式的文件必须要调用ExtAudioFileDispose()函数才会正常生成音频文件属性,由于没有调用导致此问题
解决方案:混音结束后调用ExtAudioFileDispose()即可。
2、返回-10862;返回-10860
问题原因:一个AUGraph中必须有一个并且只能有一个I/O Unit,
解决方案:RemoteIO Unit和generic output Unit 不要共存
3、返回-10867
解决思路:设置kAudioUnitProperty_ScheduledFileRegion前要先调用AUGraphInitialize (_auGraph);初始化AUGraph
项目地址
本功能位于目录下的AudioUnitGenericOutput.h/.m文件中
Demo