如果只需要完成简单的录音功能,苹果有更高级、方便的接口供开发者使用: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:
声明一个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相连,如下图所示:
其中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