前言

上一篇文章初步了解了音频混合的原理及如何使用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秒的音频文件,耗时大概不到一秒钟。

给录音添加背景音乐

接下来完成上一篇文章中未完成的一个需求,就是录制音频时播放背景音乐,但是关闭耳返效果,结束后混合录制的声音和背景音乐并生成一个新的音频文件。

想一下,是不是很简单了,其实就是组合前面学习的单个部分的功能,先画一下流程图

 

audioscore教程 audiomulch教程_audioscore教程

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