AudioUnit是iOS底层音频框架,相比于AudioQueue和AVAudioRecorder,能够对音频数据进行更多的控制,可以用来进行混音、均衡、格式转换、实时IO录制、回放、离线渲染等音频处理。
1、AudioUnit录音:
先看下苹果官方的原理图(此处以I/O单元为例):
1、一个AudioUnit包含2个element。
2、每个element包含输入输入部分(scope)。
3、硬件到element的部分(即图中淡蓝色部分)我们无法介入,我们能控的就element与我们APP关联的部分(即图中淡黄色部分)。
4、实现录音就是主要关注element1到APP的过程,播放则是关注APP到element0的过程。
代码实现:
代码只实现录音功能,所以只使用了element1:
设置音频格式:
- (void)initRecordFormat {
_recordFormat.mSampleRate = 32000; //采样率
_recordFormat.mChannelsPerFrame = 1; //声道数量
//编码格式
_recordFormat.mFormatID = kAudioFormatLinearPCM;
_recordFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
//每采样点占用位数
_recordFormat.mBitsPerChannel = 16;
//每帧的字节数
_recordFormat.mBytesPerFrame = (_recordFormat.mBitsPerChannel / 8) * _recordFormat.mChannelsPerFrame;
//每包的字节数
_recordFormat.mBytesPerPacket = _recordFormat.mBytesPerFrame;
//每帧的字节数
_recordFormat.mFramesPerPacket = 1;
}
接着实例化AudioUnit,配置相关属性和方法
- (void)initConfig {
//配置描述
AudioComponentDescription acd;
acd.componentType = kAudioUnitType_Output;
acd.componentManufacturer = kAudioUnitManufacturer_Apple;
//remoteIO对应的就是(I/O)Unit
acd.componentSubType = kAudioUnitSubType_RemoteIO;
acd.componentFlags = 0;
acd.componentFlagsMask = 0;
//AudioComponent类似组件工厂,用于实例化audioUnit
AudioComponent comp = AudioComponentFindNext(nil, &acd);
OSStatus status = AudioComponentInstanceNew(comp, &_audioUnit);
if(status!= kAudioSessionNoError) {
NSLog(@"InstanceError");
return;
}
//开启麦克风到Element1的inputScope部分,参数1代表element1。
UInt32 flag=1;
OSStatus statusPerpotyIO = AudioUnitSetProperty(_audioUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, 1, &flag, sizeof(flag));
if(statusPerpotyIO!= kAudioSessionNoError) {
NSLog(@"SetPropertyIOError");
return;
}
//设置Element1到APP的outScope部分的流格式(即我们需要得到的音频数据格式)
OSStatus statusPerpotyFR = AudioUnitSetProperty(_audioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, &_recordFormat, sizeof(_recordFormat));
if(statusPerpotyFR!= kAudioSessionNoError) {
NSLog(@"SetPropertyFRError");
return;
}
//设置录制过程的回调函数
AURenderCallbackStruct callBackSt;
callBackSt.inputProc = inputCallBack;
//将对象指针传入回调函数内部,方便内部使用
callBackSt.inputProcRefCon = (__bridge void * _Nullable)(self);
OSStatus statusPerpotyCall = AudioUnitSetProperty(_audioUnit,
kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, 1, &callBackSt, sizeof(callBackSt));
if(statusPerpotyCall!= kAudioSessionNoError) {
NSLog(@"SetPropertyCallError");
return;
}
}
设置AudioSession模式,开启
- (void)setAudioSessionEnable:(BOOL)YesOrNo {
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord error:nil];
[[AVAudioSession sharedInstance] setActive:YesOrNo error:nil];
}
启动audioUnit,开始录音
- (void)startRecord {
OSStatus statusInit = AudioUnitInitialize(_audioUnit);
if(statusInit!= kAudioSessionNoError) {
NSLog(@"statusInitError");
return;
}
AudioOutputUnitStart(_audioUnit);
self.isRecording = YES;
}
回调函数实现,获取音频数据:
OSStatus inputCallBack (void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData)
{
YTAudioUnitManager *audioManager = [YTAudioUnitManager sharedManager];
//创建bufferlist
AudioBufferList bufferList;
bufferList.mNumberBuffers = 1;
bufferList.mBuffers[0].mDataByteSize = sizeof(SInt16)*inNumberFrames;
bufferList.mBuffers[0].mNumberChannels = 1;
bufferList.mBuffers[0].mData = (SInt16*) malloc(sizeof(SInt16)*inNumberFrames);
//将unit的数据渲染到bufferList
OSStatus status = AudioUnitRender(audioManager.audioUnit,
ioActionFlags,
inTimeStamp,
1,
inNumberFrames,
&bufferList);
//这里是将数据写入文件
// ExtAudioFileWrite(audioManager.fileInfo->extAudioFileRef, inNumberFrames, &bufferList);
return status;
}
停止录音:
- (void)stopRecord {
[self setAudioSessionEnable:NO];
AudioOutputUnitStop(_audioUnit);
AudioUnitUninitialize(_audioUnit);
ExtAudioFileDispose(_fileInfo->extAudioFileRef);
self.isRecording = NO;
}
2、AudioUnit混音:
如果只是使用AudioUnit实现播放和录音功能,未免太大材小用,我们来看看混音是如何实现的。
混音是把多种来源的声音,整合至一个立体音轨(Stereo)或单音音轨(Mono)中,此处是读取两段音频,由左右声道同时进行播放。
官方也很贴心的给出了demo:Apple官方Demo地址。
原理:
首先我们看下官方的使用原理图:
1、整体是AudioGraph,用来管理多个AudioUnit,此处涉及到是I/O单元和Mix单元。
2、两个音频数据源,提供数据给Mix Unit,Mix Unit具有多个input通道,但是只有一个输出通道。
3、Mix Unit将处理完的数据传输给I/O单元,I/O单元负责播放。
具体实现:
变量定义:
//定义结构体用来保存音频数据的信息
typedef struct {
AudioStreamBasicDescription asbd;
Float32 *data;
UInt32 numFrames;
UInt32 startFrameNum;
} SoundBuffer;
@interface YTAudioMixManager() {
SoundBuffer mSoundBuffer[2]; //两个音频文件
}
@property (nonatomic,assign)AUGraph auGraph;
//针对音频文件的格式
@property (nonatomic,strong)AVAudioFormat *fileFormat;
//针对unit的格式
@property (nonatomic,strong)AVAudioFormat *unitFormat;
@end
初始化Format:
- (void)initRecordFormat {
//Audiounit的描述 ,
//声道为2,
//interleaved为NO,使左右声道的数据分别存储在AudioBufferList的两个AudioBuffer中。
_unitFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
sampleRate:44100
channels:2
interleaved:NO];
//文件音数据的描述 ,
//声道为1,
//interleaved为YES
_fileFormat = [[AVAudioFormat alloc] initWithCommonFormat:AVAudioPCMFormatFloat32
sampleRate:44100
channels:1
interleaved:YES];
}
读取两个音频文件数据:
//读取音频文件数据
- (void)loadDataFromURLS:(NSArray *)urlNames {
for (int i =0;i<urlNames.count;i++) {
NSString *urlName = urlNames[i];
CFURLRef url = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)urlName, NULL);
ExtAudioFileRef audioFileRef;
ExtAudioFileOpenURL(url, &audioFileRef);
OSStatus status;
UInt32 propSize = sizeof(AudioStreamBasicDescription);
//设置我们想要要获取的音频文件格式,ExtAudioFile是自带转码功能的。
status = ExtAudioFileSetProperty(audioFileRef, kExtAudioFileProperty_ClientDataFormat, propSize, _fileFormat.streamDescription);
if(status!=kAudioSessionNoError) {
NSLog(@"FileGetProperty Error");
return;
}
AudioStreamBasicDescription fileFormat;
UInt32 formSize = sizeof(fileFormat);
//读取文件格式属性
status = ExtAudioFileGetProperty(audioFileRef, kAudioFileStreamProperty_FileFormat, &formSize, &fileFormat);
if(status!=kAudioSessionNoError) {
NSLog(@"FileGetProperty Error");
return;
}
//读取文件帧数
UInt64 numFrames;
UInt32 numFramesSize = sizeof(numFrames);
status = ExtAudioFileGetProperty(audioFileRef, kExtAudioFileProperty_FileLengthFrames, &numFramesSize, &numFrames);
if(status!=kAudioSessionNoError) {
NSLog(@"FileGetProperty Error");
return;
}
mSoundBuffer[i].numFrames = (UInt32)numFrames;
mSoundBuffer[i].asbd = fileFormat;
UInt64 samples = numFrames * mSoundBuffer[i].asbd.mChannelsPerFrame;
mSoundBuffer[i].data = (Float32 *)calloc(samples, sizeof(Float32));
mSoundBuffer[i].sampleNum = 0;
//将文件数据读取传入BufferList
AudioBufferList bufList;
bufList.mNumberBuffers = 1;
bufList.mBuffers[0].mNumberChannels = 1;
bufList.mBuffers[0].mData = (Float32*)malloc(sizeof(Float32)*samples);
bufList.mBuffers[0].mDataByteSize = (Float32)samples * sizeof(Float32);
UInt32 numPackets = (UInt32)numFrames;
status = ExtAudioFileRead(audioFileRef, &numPackets, &bufList);
//将bufferList数据复制给mSoundBuffer
memcpy(mSoundBuffer[i].data, bufList.mBuffers[0].mData , bufList.mBuffers[0].mDataByteSize);
if(status!=kAudioSessionNoError) {
// printf("ExtAudioFileRead Error");
free(mSoundBuffer[i].data);
mSoundBuffer[i].data = 0;
}
ExtAudioFileDispose(audioFileRef);
}
}
设置AudioUnit相关对象
- (void)initAudioUnit {
//IO单元描述
AudioComponentDescription IOacd;
IOacd.componentType = kAudioUnitType_Output;
IOacd.componentManufacturer = kAudioUnitManufacturer_Apple;
//remoteIO对应的就是(I/O)Unit
IOacd.componentSubType = kAudioUnitSubType_RemoteIO;
IOacd.componentFlags = 0;
IOacd.componentFlagsMask = 0;
//mix单元描述
AudioComponentDescription mixacd;
mixacd.componentType = kAudioUnitType_Mixer;
mixacd.componentManufacturer = kAudioUnitManufacturer_Apple;
mixacd.componentSubType = kAudioUnitSubType_MultiChannelMixer;
mixacd.componentFlags = 0;
mixacd.componentFlagsMask = 0;
OSStatus status;
status= NewAUGraph (&_auGraph);
if(status!=kAudioSessionNoError) {
NSLog(@"NewAUGraph Error");
return;
}
AUNode ioNode;
AUNode mixNode;
AudioUnit ioUnit;
AudioUnit mixUnit;
//添加node
AUGraphAddNode (_auGraph,&IOacd,&ioNode);
AUGraphAddNode (_auGraph,&mixacd, &mixNode);
//建立两个node的输入和输出连接
status = AUGraphConnectNodeInput(_auGraph, mixNode, 0, ioNode, 0);
if (status!=kAudioSessionNoError) {
printf("AUGraphConnect Error");
return;
}
AUGraphOpen (_auGraph);
if(status!=kAudioSessionNoError) {
NSLog(@"AUGraphOpen Error");
return;
}
//获取node对应的Aunit
status = AUGraphNodeInfo(_auGraph, mixNode, NULL, &mixUnit);
if(status!=kAudioSessionNoError) {
NSLog(@"AUGraphNodeMixInfo Error");
return;
}
status = AUGraphNodeInfo(_auGraph, ioNode, NULL, &ioUnit);
if(status!=kAudioSessionNoError) {
NSLog(@"AUGraphOpen Error");
return;
}
//设置输入数量
int elementCount = 2;
status = AudioUnitSetProperty(mixUnit, kAudioUnitProperty_ElementCount, kAudioUnitScope_Input, 0, &elementCount, sizeof(elementCount));
if(status!=kAudioSessionNoError) {
NSLog(@"AudioUnitSetProperty Error");
return;
}
for (int i = 0; i < elementCount;i++) {
// setup render callback struct
AURenderCallbackStruct rcbs;
rcbs.inputProc = &mixInputCallBack;
rcbs.inputProcRefCon = mSoundBuffer;
//给mix的两个element设置回调
status = AUGraphSetNodeInputCallback(_auGraph, mixNode, i, &rcbs);
if (status) { printf("AUGraphSetNodeInputCallback result %ld %08lX %4.4s\n", (long)status, (long)status, (char*)&status); return; }
//设置输入格式
status = AudioUnitSetProperty(mixUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, i,_unitFormat.streamDescription , sizeof(AudioStreamBasicDescription));
if(status!=kAudioSessionNoError) {
NSLog(@"AudioUnitSetProperty Error");
return;
}
}
status = AudioUnitSetProperty(mixUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 0, _unitFormat.streamDescription, sizeof(AudioStreamBasicDescription));
if (status) {
NSLog(@"AudioUnitSetProperty Error");
return;
}
status = AudioUnitSetProperty(ioUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, 1, _unitFormat.streamDescription, sizeof(AudioStreamBasicDescription));
if (status) {
NSLog(@"AudioUnitSetProperty Error");
return;
}
status = AUGraphInitialize(_auGraph);
if (status) {
NSLog(@"AudioUnitSetProperty Error");
return;
}
}
实现回调的方法,填充数据供输出:
OSStatus mixInputCallBack (void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList * __nullable ioData) {
SoundBuffer *sndbuf = (SoundBuffer *)inRefCon;
UInt32 startFrame = sndbuf[inBusNumber].startFrameNum; // 从哪一帧开始
UInt32 numFrames = sndbuf[inBusNumber].numFrames; // 总的帧数
Float32 *bufferData = sndbuf[inBusNumber].data; // audio data buffer
Float32 *outA = (Float32 *)ioData->mBuffers[0].mData; // 第一声道数据
Float32 *outB = (Float32 *)ioData->mBuffers[1].mData; // 第二声道数据
for (UInt32 i = 0; i < inNumberFrames; ++i) {
if (inBusNumber == 0) {
outA[i] = bufferData[startFrame++]; //填充第一声道数据(用的是第一个SoundBuffer)
} else {
outB[i] = bufferData[startFrame++]; //填充第二声道数据(用的是第二个SoundBuffer)
}
if (startFrame > numFrames) {
// 结束了,再从0开始循环
printf("looping data for bus %d after %ld source frames rendered\n", (unsigned int)inBusNumber, (long)startFrame-1);
startFrame = 0;
}
}
sndbuf[inBusNumber].startFrameNum = startFrame; // 记录帧数
return noErr;
}
总结:AudioUnit更像是个看图编程的过程ㄟ( ▔, ▔ )ㄏ。