前言:
这算是我进公司实习期间完成的第一个比较完整的项目吧,耗时大约2个月,也是我第一次接触iOS音频开发,目前还未接触过视频开发,但以后我也应该会往音视频方向发展,不得不承认于我个人而言,音视频开发确实有一定难度,直到现在我感觉自己对iOS的音频也是一知半解,所以写这篇东西仅仅是想要分享与交流,自己也有一些问题希望能得到解决。文后会放上demo源代码的地址以及我在学习音频开发过程中参考过的大牛的文章供参考。
首先分享ObjC中国上一篇关于iOS所有音频API的简介https://objccn.io/issue-24-4/,相信大家看完这篇简介后结合自己的项目需求就大概知道自己需要使用哪一个API了吧。
再说回我自己的项目需求,其实光是录音+耳返这个需求,AudioUnit并不是最简单的选择,使用AVAudioEngine会更简单,至于能不能使用更简单的API实现我目前还不得而知。那为什么我要使用AudioUnit呢?因为其实我公司的项目需求远不止是录音+耳返,还牵扯到音效处理和混声类似于唱吧或者全民k歌这种软件,所以只能使用最底层的AudioUnit。但该篇文章暂时只讨论录音+耳返这个较为简单的需求。
上面iOS所有音频API的简介里面并没有提到AUGraph,所以就简单介绍一下AUGraph。
AUGraph连接一组 audio unit 之间的输入和输出,构成一张图,同时也为audio unit 的输入提供了回调。AUGraph抽象了音频流的处理过程,子结构可以作为一个AUNode嵌入到更大的结构里面进行处理。AUGraph可以遍历整个图的信息,每个节点都是一个或者多个AUNode,音频数据在点与点之间流通,并且每个图都有一个输出节点。输出节点可以用来启动、停止整个处理过程。
虽然实际工程中更多使用的是AUGraph的方式进行AudioUnit的初始化,但其实光使用AudioUnit同样可以实现录音+耳返的功能,但是我在实际项目中出现了问题,导致我不得不配合AUGraph使用,这个问题将在后文详述。
另外,苹果官方已经声称将要淘汰AUGraph这个API并在源码中备注API_TO_BE_DEPRECATED,而且建议开发者改为使用AVAudioEngine,AVAudioEngine同样可以配合AudioUnit使用但我还未深入研究,在网上搜索了一下AVAudioEngine的教程资料也是比较少的,如果有机会的话我以后会出一些关于AVAudioEngine的教程,其实要想实现复杂的例如混音功能,我相信重点依然是AudioUnit而不是AUGraph,AUGraph和现在的AVAudioEngine仅仅只是起到辅助管理作用。
#define kInputBus 1 #define kOutputBus 0 FILE *file = NULL; @implementation GSNAudioUnitManager { AVAudioSession *audioSession; AUGraph auGraph; AudioUnit remoteIOUnit; AUNode remoteIONode; AURenderCallbackStruct inputProc; } 复制代码
- (void)initAudioSession { audioSession = [AVAudioSession sharedInstance]; NSError *error; // set Category for Play and Record // [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error]; [audioSession setCategory:AVAudioSessionCategoryPlayAndRecord withOptions:AVAudioSessionCategoryOptionAllowBluetoothA2DP error:&error]; // [audioSession setPreferredIOBufferDuration:0.01 error:&error]; } 复制代码
- (void)newAndOpenAUGraph { CheckError(NewAUGraph(&auGraph),"couldn't NewAUGraph"); CheckError(AUGraphOpen(auGraph),"couldn't AUGraphOpen"); } 复制代码
- (void)initAudioComponent { AudioComponentDescription componentDesc; componentDesc.componentType = kAudioUnitType_Output; componentDesc.componentSubType = kAudioUnitSubType_RemoteIO; componentDesc.componentManufacturer = kAudioUnitManufacturer_Apple; componentDesc.componentFlags = 0; componentDesc.componentFlagsMask = 0; 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"); } 复制代码
- (void)initFormat { //set BUS 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.mReserved = 0; mAudioFormat.mChannelsPerFrame = 1;//1单声道,2立体声,但不是改为2就是立体声 mAudioFormat.mBitsPerChannel = 16;//语音每采样点占用位数 mAudioFormat.mFramesPerPacket = 1;//每个数据包多少帧 mAudioFormat.mBytesPerFrame = (mAudioFormat.mBitsPerChannel / 8) * mAudioFormat.mChannelsPerFrame; // 每帧的bytes数 mAudioFormat.mBytesPerPacket = mAudioFormat.mBytesPerFrame;//每个数据包的bytes总数,每帧的bytes数*每个数据包的帧数 UInt32 size = sizeof(mAudioFormat); CheckError(AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &mAudioFormat, size),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Output"); CheckError(AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &mAudioFormat, size),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Input"); } 复制代码
- (void)initInputCallBack { inputProc.inputProc = inputCallBack; inputProc.inputProcRefCon = (__bridge void *)(self); CheckError(AUGraphSetNodeInputCallback(auGraph, remoteIONode, 0, &inputProc),"Error setting io input callback"); } 复制代码
- (void)initAndUpdateAUGraph { CheckError(AUGraphInitialize(auGraph),"couldn't AUGraphInitialize" ); CheckError(AUGraphUpdate(auGraph, NULL),"couldn't AUGraphUpdate" ); } 复制代码
- (void)audioUnitInit { // 设置需要生成pcm的文件路径 self.pathStr = [self documentsPath:@"/mixRecord.pcm"]; [self initAudioSession]; [self newAndOpenAUGraph]; [self initAudioComponent]; [self initFormat]; [self initInputCallBack]; [self initAndUpdateAUGraph]; } 复制代码
- (void)audioUnitStartRecordAndPlay { CheckError(AUGraphStart(auGraph),"couldn't AUGraphStart"); CAShow(auGraph); } - (void)audioUnitStop { CheckError(AUGraphStop(auGraph), "couldn't AUGraphStop"); } 复制代码
static void CheckError(OSStatus error, const char *operation) { if (error == noErr) return; char str[20]; // see if it appears to be a 4-char-code *(UInt32 *)(str + 1) = CFSwapInt32HostToBig(error); if (isprint(str[1]) && isprint(str[2]) && isprint(str[3]) && isprint(str[4])) { str[0] = str[5] = '\''; str[6] = '\0'; } else // no, format it as an integer sprintf(str, "%d", (int)error); fprintf(stderr, "Error: %s (%s)\n", operation, str); exit(1); } 复制代码
- (void)writePCMData:(char *)buffer size:(int)size { if (!file) { file = fopen(self.pathStr.UTF8String, "w"); } fwrite(buffer, size, 1, file); } 复制代码
static OSStatus inputCallBack( void *inRefCon, AudioUnitRenderActionFlags *ioActionFlags, const AudioTimeStamp *inTimeStamp, UInt32 inBusNumber, UInt32 inNumberFrames, AudioBufferList *ioData) { GSNAudioUnitManager *THIS=(__bridge GSNAudioUnitManager*)inRefCon; OSStatus renderErr = AudioUnitRender(THIS->remoteIOUnit, ioActionFlags, inTimeStamp, 1, inNumberFrames, ioData); [THIS writePCMData:ioData->mBuffers->mData size:ioData->mBuffers->mDataByteSize]; return renderErr; } 复制代码
- (NSString *)documentsPath:(NSString *)fileName { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths objectAtIndex:0]; return [documentsDirectory stringByAppendingPathComponent:fileName]; } 复制代码
上文中有提到仅使用AudioUnit同样可以实现录音+耳返,但其中出现了一个很大的问题导致我不得不使用AUGraph,这个问题就是在仍保留有3.5mm耳机接口的iPhone(苹果从iPhone7开始取消3.5mm耳机接口,仅能通过lightning接口使用有线耳机)上默认(即不改变preferredIOBufferDuration)情况下每一次回调的mDataByteSize是2048,而在使用lightning耳机接口的iPhone上默认情况下每一次回调的mDataByteSize是1880,居然不是2的整数幂!因为仅使用AudioUnit的情况下必须要指明音频buffer的大小,而且必须是2的整数次幂,不然就会报“ AudioUnitRender error:-50 ”的错误。
这张图对于理解输入输出通道会有很大的帮助,就好比如我一开始不理解为什么这里kAudioUnitScope_Output对应的却是kInputBus(1),为什么不应该是kOutputBus(0),结合上图就会发现它其实就是想设置浅黄色部分也就是输出音频的格式。
CheckError(AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &mAudioFormat, size),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Output"); CheckError(AudioUnitSetProperty(remoteIOUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &mAudioFormat, size),"couldn't set kAudioUnitProperty_StreamFormat with kAudioUnitScope_Input"); 复制代码
其实公司项目需求远不止这么简单,只是其它功能或多或少调用了公司内部的SDK所以不太好说,另外在我学习的过程中我觉得网上关于录音+耳返的通俗易懂的资料还是比较少的,但我并没有详细介绍AudioUnit或者AUGraph,因为网上已经有大牛写了很详尽的文章去介绍,从最基本的音频原理到实践,文后我也会贴出相应的链接,建议参阅,当然贴出来的仅仅只是我看过文章的一小部分,也是我觉得比较有价值的一部分。