iOS 使用AUGraph录音同时播放(并转码成Mp3)

如果只需要完成简单的录音功能,苹果有更高级、方便的接口供开发者使用:AVAudioRecorder,面向对象,不关心细节实现。但是如果开发者想要拿到实时数据并对其进行处理,就要用到Audio Unit Services和Audio Processing Graph Services。下面我会介绍如何使用它们来完成一个最简单的录音DEMO。

AudioSession

首先,我们需要了解下AudioSession这个类。先看下苹果对它的介绍:

iOS handles audio behavior at the app, inter-app, and device levels through audio sessions

iOS通过AudioSession来控制APP中的音频表现,跨应用和硬件设备。按我的理解,它就是用来设定最基础的音频配置的,比如:
1、当耳机被拔出,是否停止音频的播放?
2、本APP的音频播放是否和其他APP的音频实现混音?还是让其他APP的音频暂停?
3、是否允许APP获取麦克风数据?

在本文中,我们需要用到麦克风录音并且播放,调用以下代码,APP就会弹出窗口询问是否允许APP访问麦克风:

AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];

Audio Processing Graph

先用一张表格(来自苹果文档)介绍下Audio Unit的类型:

Purpose Audio units
Effect iPod Equalizer
Mixing 3D Mixer
Multichannel Mixer
I/O Remote I/O
Voice-Processing I/O
Generic Output
Format conversion Format Converter

7种类型,4个作用:均衡器、混合、输入/输出和格式转换。
在本DEMO中只使用了Remote I/O完成简单的录音和播放功能。
而Audio Unit并不能独立完成工作,需要配合AUGraph来使用。AUGraph是管理者,不同的Unit作为Node添加到AUGraph中去发挥作用,如下图AUGraph管理着Mixer Unit和Remote I/O Unit:


iOS 使用AUGraph录音同时播放(并转码成Mp3)_第1张图片
AudioProcessingGraphBeforeEQ_2x.png

声明一个Remote I/O类型的Node,并添加到AUGraph中:

AUNode remoteIONode;
AudioComponentDescription componentDesc; //关于Node的描述
componentDesc.componentType = kAudioUnitType_Output;
componentDesc.componentSubType = kAudioUnitSubType_RemoteIO;
componentDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
componentDesc.componentFlags = 0;
componentDesc.componentFlagsMask = 0;

CheckError(NewAUGraph(&auGraph),"couldn't NewAUGraph"); //创建AUGraph
CheckError(AUGraphOpen(auGraph),"couldn't AUGraphOpen"); //打开AUGraph

CheckError(AUGraphAddNode(auGraph,&componentDesc,&remoteIONode),"couldn't add remote io node");
CheckError(AUGraphNodeInfo(auGraph,remoteIONode,NULL,&remoteIOUnit),"couldn't get remote io unit from node");

Remote I/O Unit

Remote I/O Unit 属于Audio Unit其中之一,是一个与硬件设备相关的Unit,它分为输入端和输出端,如扬声器、麦克风和耳机等。在这里我们需要在录音的同时播放,所以我们要让输入端和输出端的Unit相连,如下图所示:


iOS 使用AUGraph录音同时播放(并转码成Mp3)_第2张图片
IO_unit_2x.png

其中Element0代表着输出端,Element1代表输入端;而每个Element又分为Input scope和Output scope。我们具体要做的就是将Element0的Output scope和喇叭接上,Element1的Intput和麦克风接上。代码如下:

 UInt32 oneFlag = 1; 
 CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioOutputUnitProperty_EnableIO,
                                    kAudioUnitScope_Output,
                                    kOutputBus,
                                    &oneFlag,
                                    sizeof(oneFlag)),"couldn't kAudioOutputUnitProperty_EnableIO with kAudioUnitScope_Output");
    
 CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioOutputUnitProperty_EnableIO,
                                    kAudioUnitScope_Input,
                                    kInputBus,
                                    &oneFlag,
                                    sizeof(oneFlag)),"couldn't kAudioOutputUnitProperty_EnableIO with kAudioUnitScope_Input");

然后设置一下输入输出的音频格式:

AudioStreamBasicDescription mAudioFormat;
mAudioFormat.mSampleRate         = 44100.0;//采样率
mAudioFormat.mFormatID           = kAudioFormatLinearPCM;//PCM采样
mAudioFormat.mFormatFlags        = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
mAudioFormat.mFramesPerPacket    = 1;//每个数据包多少帧 mAudioFormat.mChannelsPerFrame   = 1;//1单声道,2立体声
mAudioFormat.mBitsPerChannel     = 16;//语音每采样点占用位数
mAudioFormat.mBytesPerFrame      = mAudioFormat.mBitsPerChannel*mAudioFormat.mChannelsPerFrame/8;//每帧的bytes数
mAudioFormat.mBytesPerPacket     = mAudioFormat.mBytesPerFrame*mAudioFormat.mFramesPerPacket;//每个数据包的bytes总数,每帧的bytes数*每个数据包的帧数
mAudioFormat.mReserved           = 0;
UInt32 size = sizeof(mAudioFormat);
    
CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Output,
                                    1,
                                    &mAudioFormat,
                                    size),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Output");
    
CheckError(AudioUnitSetProperty(remoteIOUnit,
                                    kAudioUnitProperty_StreamFormat,
                                    kAudioUnitScope_Input,
                                    0,
                                    &mAudioFormat,
                                    size),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Input");

至此我们就差不多完成了,只差最后一个步骤:设置CallBack,每次音频从麦克风进来转化为数字信号时就会调用此CallBack函数,将数字信号作你所想要的处理,完了再送到输出端去进行播放。回调函数是一个C语言的静态方法,代码如下:

static OSStatus CallBack(
                            void      *inRefCon,
                            AudioUnitRenderActionFlags  *ioActionFlags,
                            const AudioTimeStamp   *inTimeStamp,
                            UInt32       inBusNumber,
                            UInt32       inNumberFrames,
                            AudioBufferList    *ioData)
{
    RecordTool *THIS=(__bridge RecordTool*)inRefCon;
    
    OSStatus renderErr = AudioUnitRender(THIS->remoteIOUnit, ioActionFlag,  inTimeStamp, 1, inNumberFrames, ioData);

//--------------------------------------------//
//              在这里处理音频数据               //
//--------------------------------------------//
    
//转Mp3,后面有时间再详细介绍下    
//     [THIS->cover convertPcmToMp3:ioData->mBuffers[0] toPath:THIS->outPath];

    
    return renderErr;
}
ioData->mBuffers[n] //n=0~1,单声道n=0,双声道n=1
ioData->mBuffers[0].mData //PCM数据
ioData->mBuffers[0].mDataByteSize //PCM数据的长度

定义好CallBack函数后,将其与AUGraph关联起来:

AURenderCallbackStruct inputProc;
inputProc.inputProc = CallBack;
inputProc.inputProcRefCon = (__bridge void *)(self);
CheckError(AUGraphSetNodeInputCallback(auGraph, remoteIONode, 0, &inputProc),"Error setting io output callback");
 
CheckError(AUGraphInitialize(auGraph),"couldn't AUGraphInitialize" );
CheckError(AUGraphUpdate(auGraph, NULL),"couldn't AUGraphUpdate" );

//最后再调用以下代码即可开始录音
CheckError(AUGraphStart(auGraph),"couldn't AUGraphStart");
CAShow(auGraph);

最后

因为时间的关系,未能细说PCM转MP3(使用LAME)的内容,我已经把DEMO上传到GitHub,有需要的朋友可以下载来看看,往后有时间了我再补全关于转码MP3的内容。
如果觉得我的DEMO对您有帮助,请Star,非常感谢!

更新

使用LAME转码请看iOS-使用Lame转码:PCM->MP3

你可能感兴趣的:(iOS 使用AUGraph录音同时播放(并转码成Mp3))