前言:node
这算是我进公司实习期间完成的第一个比较完整的项目吧,耗时大约2个月,也是我第一次接触iOS音频开发,目前还未接触过视频开发,但之后我也应该会往音视频方向发展,不得不认可于我我的而言,音视频开发确实有必定难度,直到如今我感受本身对iOS的音频也是只知其一;不知其二,因此写这篇东西仅仅是想要分享与交流,本身也有一些问题但愿能获得解决。文后会放上demo源代码的地址以及我在学习音频开发过程当中参考过的大牛的文章供参考。web
首先分享ObjC中国上一篇关于iOS全部音频API的简介https://objccn.io/issue-24-4/,相信你们看完这篇简介后结合本身的项目需求就大概知道本身须要使用哪个API了吧。xcode
再说回我本身的项目需求,其实光是录音+耳返这个需求,AudioUnit并非最简单的选择,使用AVAudioEngine会更简单,至于能不能使用更简单的API实现我目前还不得而知。那为何我要使用AudioUnit呢?由于其实我公司的项目需求远不止是录音+耳返,还牵扯到音效处理和混声相似于唱吧或者全民k歌这种软件,因此只能使用最底层的AudioUnit。但该篇文章暂时只讨论录音+耳返这个较为简单的需求。bash
上面iOS全部音频API的简介里面并无提到AUGraph,因此就简单介绍一下AUGraph。app
AUGraph链接一组 audio unit 之间的输入和输出,构成一张图,同时也为audio unit 的输入提供了回调。AUGraph抽象了音频流的处理过程,子结构能够做为一个AUNode嵌入到更大的结构里面进行处理。AUGraph能够遍历整个图的信息,每一个节点都是一个或者多个AUNode,音频数据在点与点之间流通,而且每一个图都有一个输出节点。输出节点能够用来启动、中止整个处理过程。less
虽然实际工程中更多使用的是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 ”的错误。gradle
这张图对于理解输入输出通道会有很大的帮助,就比如如我一开始不理解为何这里kAudioUnitScope_Output对应的倒是kInputBus(1),为何不该该是kOutputBus(0),结合上图就会发现它其实就是想设置浅黄色部分也就是输出音频的格式。lua
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,由于网上已经有大牛写了很详尽的文章去介绍,从最基本的音频原理到实践,文后我也会贴出相应的连接,建议参阅,固然贴出来的仅仅只是我看过文章的一小部分,也是我以为比较有价值的一部分。