前言: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,由於網上已經有大牛寫了很詳盡的文章去介紹,從最基本的音頻原理到實踐,文後我也會貼出相應的連接,建議參閱,固然貼出來的僅僅只是我看過文章的一小部分,也是我以爲比較有價值的一部分。