ios利用mic採集Pcm轉爲AAC,AudioQueue、AudioUnit(流式)


本例需求:將Mic採集的PCM轉成AAC,可獲得兩種不一樣數據,本例採用AudioQueue/AudioUnit兩種方式存儲,即: 可採集到兩種聲音數據,一種爲PCM,一種爲轉換後的AAC.

原理:因爲公司需求更改成Mic採集的pcm一路提供給WebRTC使用,另外一路將pcm轉爲aac,將aac提供給直播用的API。所以應該先讓Mic採集原始pcm數據,採用AudioQueue/AudioUnit兩種方式採集,而後在回調函數中將其轉換爲aac提供給C++API


本例中僅包含部分代碼,建議下載代碼詳細看,在關鍵代碼中都有註釋中能夠看到難理解的含義.


GitHub地址(附代碼) : PCM->AAC

簡書地址 : PCM->AAC

博客地址 : PCM->AAC

掘金地址 : PCM->AAC


實現方式:(下文兩種實現方式,挑選本身適合的)

1.AudioQueue : 若對延遲要求不高,可實現錄製,播放,暫停,回退,同步,轉換(PCM->AAC等)等功能可採用這種方式

2.AudioUnit : 比AudioQueue更加底層,可實現高性能,低延遲,而且包括去除回聲,混音等等功能。

AudioQueue爲何會出現波動的狀況?解決方法?這種波動的緣由是在Audio Queue的底層產生的,以前說過,Audio ToolBox是基於Audio Unit的,回調函數的波動要到底層才能解決。


一.本文須要基本知識點

C語言相關函數:

1.memset: 原型: void * memset(void * __b, int __c, size_t __len); 解釋:將s中當前位置後面的n個字節(typedef unsigned int size_t) 用ch替換並返回s 做用:在一段內存塊中填充某個特定的值,它是對較大的結構體或數組進行清零操做的一種最快方法。html

2.memcpy: 原型: void * memcpy(void * dest, const void * src, size_t n); 解釋:從源src所指的內存地址的起始位置開始拷貝n個字節到目標dest所指的內存地址的起始位置中ios

3.void free(void *); 解釋:釋放內存,須要將malloc出來的內存通通釋放掉,對於結構體要先將結構體中malloc出來的釋放掉最後再釋放掉結構體自己。git

OC 中部分知識點:

1.OSStaus:狀態碼,若是沒有錯誤返回0:(即noErr)github

2.AudioFormatGetPropertyInfo:express

原型: 
AudioFormatGetPropertyInfo(
					    	AudioFormatPropertyID   inPropertyID,
							UInt32                  inSpecifierSize,
		 					const void * __nullable inSpecifier,
							UInt32 *                outPropertyDataSize);
									
* 做用:檢索給定屬性的信息,好比編碼器目標格式的size等

複製代碼

3.AudioSessionGetProperty:編程

原型: 
extern OSStatus
AudioSessionGetProperty(    
							 	AudioSessionPropertyID     inID,
		            			UInt32                     *ioDataSize,
								void                       *outData);
									
* 做用:獲取指定AudioSession對象的inID屬性的值(好比採樣率,聲道數等等)
複製代碼

4.AudioUnitSetProperty數組

extern OSStatus
AudioUnitSetProperty(  AudioUnit               inUnit,
							AudioUnitPropertyID     inID, 
							AudioUnitScope	       inScope,
							AudioUnitElement	       inElement,
							const void * __nullable inData,
							UInt32				       inDataSize)				
* 做用:設置AudioUnit特定屬性的值,其中scope,element不理解可參考下文audio unit概念部分,這裏能夠設置音頻流的各類參數,好比採樣頻率、量化位數、通道個數、每包中幀的個數等等
複製代碼

音頻基礎知識

  1. AVFoundation框架中的AVAudioPlayer和AVAudioRecorder類,用法簡單,可是不支持流式,也就意味着在播放音頻前,必須等到整個音頻加載完成後,才能開始播放音頻;錄音時,也必須等到錄音結束後才能得到錄音數據。緩存

  2. 在iOS和Mac OS X中,音頻隊列Audio Queues是一個用來錄製和播放音頻的軟件對象,也就是說,能夠用來錄音和播放,錄音可以獲取實時的PCM原始音頻數據。bash

  3. 數據介紹cookie

(1)In CBR (constant bit rate) formats, such as linear PCM and IMA/ADPCM, all packets are the same size.

(2)In VBR (variable bit rate) formats, such as AAC, Apple Lossless, and MP3, all packets have the same number of frames but the number of bits in each sample value can vary.

(3)In VFR (variable frame rate) formats, packets have a varying number of frames. There are no commonly used formats of this type.

  1. 概念:

(1)音頻文件的組成:文件格式(或者音頻容器)+數據格式(或者音頻編碼)

知識點:

  • 文件格式是用於形容文件自己的格式,能夠經過多種不一樣方法爲真正的音頻數據編碼,例如CAF文件即是一種文件格式,它可以包含MP3格式,線性PCM以及其餘數據格式音頻 線性PCM:這是表示線性脈衝編碼機制,主要是描寫用於將模擬聲音數據轉換成數組格式的技術,簡單地說也就是未壓縮的數據。由於數據是未壓縮的,因此咱們即可以最快速地播放出音頻,而若是空間不是問題的話這即是iPhone 音頻的優先代碼選擇

(2).音頻文件計算大小 簡述:聲卡對聲音的處理質量能夠用三個基本參數來衡量,即採樣頻率,採樣位數和聲道數。

知識點:

  • 採樣頻率:單位時間內採樣次數。採樣頻率越大,採樣點之間的間隔就越小,數字化後獲得的聲音就越逼真,但相應的數據量就越大,聲卡通常提供11.025kHz,22.05kHz和44.1kHz等不一樣的採樣頻率。

  • 採樣位數:記錄每次採樣值數值大小的位數。採樣位數一般有8bits或16bits兩種,採樣位數越大,所能記錄的聲音變化度就越細膩,相應的數據量就越大。

  • 聲道數:處理的聲音是單聲道仍是立體聲。單聲道在聲音處理過程當中只有單數據流,而立體聲則須要左右聲道的兩個數據流。顯然,立體聲的效果要好,但相應數據量要比單聲道數據量加倍。

  • 聲音數據量的計算公式:數據量(字節 / 秒)=(採樣頻率(Hz)* 採樣位數(bit)* 聲道數)/ 8 單聲道的聲道數爲1,立體聲的聲道數爲2. 字節B,1MB=1024KB = 1024*1024B

(3)

  1. CoreAudio 介紹
    CoreAudio
    (1). CoreAudio分爲三層結構,如上圖 1.最底層的I/O Kit, MIDI, HAL等用於直接與硬件相關操做,通常來講用不到。 2.中間層服務是對數據格式的轉換,對硬盤執行讀寫操做,解析流,使用插件等。
  • 其中AudioConverter Services 可實現不一樣音頻格式的轉碼,如PCM->AAC等
  • Audio File Services支持讀寫音頻數據從硬盤
  • Audio Unit Services and Audio Processing Graph Services 可實現使應用程序處理數字信號,完成一些插件功能,如均衡器和混聲器等。
  • Audio File Stream Services 可使程序解析流,如播放一段來自網絡的音頻。
  • Audio Format Services 幫助應用程序管理音頻格式相關操做 3.最高層是用基於底層實現的部分功能,使用相對簡單。
  • Audio Queue Services 可實現錄音,播放,暫停,同步音頻等功能
  • AVAudioPlayer 提供簡單地OC接口對於音頻的播放與暫停,功能較爲侷限。
  • OpenAL 實現三維混音音頻單元頂部,適合開發遊戲

(2).Audio Data Formats:經過設置一組屬性代碼能夠和操做系統支持的任何格式一塊兒工做。(包括採樣率,比特率),對於AudioQueue與AudioUnit設置略有不一樣。

struct AudioStreamBasicDescription
{
   Float64          	mSampleRate;	    // 採樣率 :Hz
   AudioFormatID      	mFormatID;	        // 採樣數據的類型,PCM,AAC等
   AudioFormatFlags    mFormatFlags;	    // 每種格式特定的標誌,無損編碼 ,0表示沒有
   UInt32            	mBytesPerPacket;    // 一個數據包中的字節數
   UInt32              mFramesPerPacket;   // 一個數據包中的幀數,每一個packet的幀數。若是是未壓縮的音頻數據,值是1。動態幀率格式,這個值是一個較大的固定數字,好比說AAC的1024。若是是動態大小幀數(好比Ogg格式)設置爲0。
   UInt32            	mBytesPerFrame;     // 每一幀中的字節數
   UInt32            	mChannelsPerFrame;  // 每一幀數據中的通道數,單聲道爲1,立體聲爲2
   UInt32              mBitsPerChannel;    // 每一個通道中的位數,1byte = 8bit
   UInt32              mReserved; 		    // 8字節對齊,填0
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

複製代碼

###---------------------------- Audio Queue ---------------------------

二.AudioQueue

.音頻隊列 — 詳細請參考 Audio Queue,該文章中已有詳細描述,再也不重複介紹,不懂請參考。

1.簡述:在iOS和Mac OS X中,音頻隊列是一個用來錄製和播放音頻的軟件對象,他用AudioQueueRef這個不透明數據類型來表示,該類型在AudioQueue.h頭文件中聲明。

2.工做:

  • 鏈接音頻硬件
  • 內存管理
  • 根據須要爲已壓縮的音頻格式引入編碼器
  • 媒體的錄製或播放

你能夠將音頻隊列配合其餘Core Audio的接口使用,再加上相對少許的自定義代碼就能夠在你的應用程序中建立一套完整的數字音頻錄製或播放解決方案。

3.結構:

  • 一組音頻隊列緩衝區(audio queue buffers),每一個音頻隊列緩衝區都是一個存儲音頻數據的臨時倉庫

  • 一個緩衝區隊列(buffer queue),一個包含音頻隊列緩衝區的有序列表

  • 一個你本身編寫的音頻隊列回調函數(audio queue callback)

它的架構很大程度上依賴於這個音頻隊列是用來錄製仍是用來播放的。不一樣之處在於音頻隊列如何鏈接到它的輸入和輸入,還有它的回調函數所扮演的角色。

4.調用步驟,首先將項目設置爲MRC,在控制器中配置audioSession基本設置(基本設置,不會谷歌),導入該頭文件,直接在須要時機調用該類startRecord與stopRecord方法,另外還提供了生成錄音文件的功能,具體參考github中的代碼。

本例中涉及的一些宏定義,具體能夠下載代碼詳細看
#define kBufferDurationSeconds .5
#define kXDXRecoderAudioBytesPerPacket 2
#define kXDXRecoderAACFramesPerPacket 1024
#define kXDXRecoderPCMTotalPacket 512
#define kXDXRecoderPCMFramesPerPacket 1
#define kXDXRecoderConverterEncodeBitRate 64000
#define kXDXAudioSampleRate 48000.0
複製代碼

(1).設置AudioStreamBasicDescription 基本信息

-(void)startRecorder {
    // Reset pcm_buffer to save convert handle, 每次開始音頻會話前初始化pcm_buffer, pcm_buffer用來在捕捉聲音的回調中存儲累加的PCM原始數據
    memset(pcm_buffer, 0, pcm_buffer_size);
    pcm_buffer_size = 0;
    frameCount      = 0;

// 是否正在錄製
    if (isRunning) {
        // log4cplus_info("pcm", "Start recorder repeat");
        return;
    }
    
// 本例中採用log4打印log信息,若你沒有能夠不用,刪除有關Log4的語句
    // log4cplus_info("pcm", "starup PCM audio encoder");
    
// 設置採集的數據的類型爲PCM
    [self setUpRecoderWithFormatID:kAudioFormatLinearPCM];
    

    OSStatus status          = 0;
    UInt32   size            = sizeof(dataFormat);
    
    // 編碼器轉碼設置
    [self convertBasicSetting];
    
    // 這個if語句用來檢測是否初始化本例對象成功,若是不成功重啓三次,三次後若是失敗能夠進行其餘處理
    if (err != nil) {
        NSString *error = nil;
        for (int i = 0; i < 3; i++) {
            usleep(100*1000);
            error = [self convertBasicSetting];
            if (error == nil) break;
        }
        // if init this class failed then restart three times , if failed again,can handle at there
//        [self exitWithErr:error];
    }

    
    // 新建一個隊列,第二個參數註冊回調函數,第三個防止內存泄露
    status =  AudioQueueNewInput(&dataFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &mQueue);
    // log4cplus_info("pcm","AudioQueueNewInput status:%d",(int)status);
    
// 獲取隊列屬性
    status = AudioQueueGetProperty(mQueue, kAudioQueueProperty_StreamDescription, &dataFormat, &size);
    // log4cplus_info("pcm","AudioQueueNewInput status:%u",(unsigned int)dataFormat.mFormatID);
    
// 這裏將頭信息添加到寫入文件中,若文件數據爲CBR,不須要添加,爲VBR須要添加
    [self copyEncoderCookieToFile];
    
    //    能夠計算得到,在這裏使用的是固定大小
    //    bufferByteSize = [self computeRecordBufferSizeFrom:&dataFormat andDuration:kBufferDurationSeconds];
    
    // log4cplus_info("pcm","pcm raw data buff number:%d, channel number:%u",
                   kNumberQueueBuffers,
                   dataFormat.mChannelsPerFrame);
    
// 設置三個音頻隊列緩衝區
    for (int i = 0; i != kNumberQueueBuffers; i++) {
	// 注意:爲每一個緩衝區分配大小,可根據具體需求進行修改,可是必定要注意必須知足轉換器的需求,轉換器只有每次給1024幀數據纔會完成一次轉換,若是需求爲採集數據量較少則用本例提供的pcm_buffer對數據進行累加後再處理
        status = AudioQueueAllocateBuffer(mQueue, kXDXRecoderPCMTotalPacket*kXDXRecoderAudioBytesPerPacket*dataFormat.mChannelsPerFrame, &mBuffers[i]);
	// 入隊
        status = AudioQueueEnqueueBuffer(mQueue, mBuffers[i], 0, NULL);
    }
    
    isRunning  = YES;
    hostTime   = 0;
    
    status     =  AudioQueueStart(mQueue, NULL);
    log4cplus_info("pcm","AudioQueueStart status:%d",(int)status);
}
複製代碼

初始化輸出流的結構體描述

struct AudioStreamBasicDescription
{
   Float64          	mSampleRate;	    // 採樣率 :Hz
   AudioFormatID      	mFormatID;	        // 採樣數據的類型,PCM,AAC等
   AudioFormatFlags    mFormatFlags;	    // 每種格式特定的標誌,無損編碼 ,0表示沒有
   UInt32            	mBytesPerPacket;    // 一個數據包中的字節數
   UInt32              mFramesPerPacket;   // 一個數據包中的幀數,每一個packet的幀數。若是是未壓縮的音頻數據,值是1。動態幀率格式,這個值是一個較大的固定數字,好比說AAC的1024。若是是動態大小幀數(好比Ogg格式)設置爲0。
   UInt32            	mBytesPerFrame;     // 每一幀中的字節數
   UInt32            	mChannelsPerFrame;  // 每一幀數據中的通道數,單聲道爲1,立體聲爲2
   UInt32              mBitsPerChannel;    // 每一個通道中的位數,1byte = 8bit
   UInt32              mReserved; 		    // 8字節對齊,填0
};
typedef struct AudioStreamBasicDescription  AudioStreamBasicDescription;

複製代碼

注意: kNumberQueueBuffers,音頻隊列可使用任意數量的緩衝區。你的應用程序制定它的數量。通常狀況下這個數字是3。這樣就可讓給一個忙於將數據寫入磁盤,同時另外一個在填充新的音頻數據,第三個緩衝區在須要作磁盤I/O延遲補償的時候可用

如何使用AudioQueue:

  1. 建立輸入隊列AudioQueueNewInput
  2. 分配buffers
  3. 入隊:AudioQueueEnqueueBuffer
  4. 回調函數採集音頻數據
  5. 出隊

AudioQueueNewInput

// 做用:建立一個音頻隊列爲了錄製音頻數據
原型:extern OSStatus             
		AudioQueueNewInput( const AudioStreamBasicDescription   *inFormat, 同上
                            AudioQueueInputCallback             inCallbackProc, // 註冊回調函數
                            void * __nullable               	inUserData,		
                            CFRunLoopRef __nullable         	inCallbackRunLoop,
                            CFStringRef __nullable          	inCallbackRunLoopMode,
                            UInt32                          	inFlags,
                            AudioQueueRef __nullable        	* __nonnull outAQ);

// 這個函數的第四個和第五個參數是有關於線程的,我設置成null,表明它默認使用內部線程去錄音,並且仍是異步的
複製代碼

(2).設置採集數據的格式,採集PCM必須按照以下設置,參考蘋果官方文檔,不一樣需求本身另行修改

-(void)setUpRecoderWithFormatID:(UInt32)formatID {
	 // Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設置爲官方推薦設置,可根據具體需求修改部分設置
    //setup auido sample rate, channel number, and format ID
    memset(&dataFormat, 0, sizeof(dataFormat));
    
    UInt32 size = sizeof(dataFormat.mSampleRate);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
                            &size,
                            &dataFormat.mSampleRate);
    dataFormat.mSampleRate = kXDXAudioSampleRate; // 設置採樣率
    
    size = sizeof(dataFormat.mChannelsPerFrame);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
                            &size,
                            &dataFormat.mChannelsPerFrame);
    dataFormat.mFormatID = formatID;
    
    // 關於採集PCM數據是根據蘋果官方文檔給出的Demo設置,至於爲何這麼設置可能與採集回調函數內部實現有關,修改的話請謹慎
    if (formatID == kAudioFormatLinearPCM)
    {
    	 /*
    	  爲保存音頻數據的方式的說明,如能夠根據大端字節序或小端字節序,
    	  浮點數或整數以及不一樣體位去保存數據
          例如對PCM格式一般咱們以下設置:kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked等
          */
        dataFormat.mFormatFlags     = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        // 每一個通道里,一幀採集的bit數目
        dataFormat.mBitsPerChannel  = 16;
        // 8bit爲1byte,即爲1個通道里1幀須要採集2byte數據,再*通道數,即爲全部通道採集的byte數目
        dataFormat.mBytesPerPacket  = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
        // 每一個包中的幀數,採集PCM數據須要將dataFormat.mFramesPerPacket設置爲1,不然回調不成功
        dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket;
    }
}
複製代碼

(3).將PCM轉成AAC一些基本設置

-(NSString *)convertBasicSetting {
    // 此處目標格式其餘參數均爲默認,系統會自動計算,不然沒法進入encodeConverterComplexInputDataProc回調

    AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
    AudioStreamBasicDescription targetDes;              // 轉碼後格式
    
    // 設置目標格式及基本信息
    memset(&targetDes, 0, sizeof(targetDes));
    targetDes.mFormatID           = kAudioFormatMPEG4AAC;
    targetDes.mSampleRate         = kXDXAudioSampleRate;
    targetDes.mChannelsPerFrame   = dataFormat.mChannelsPerFrame;
    targetDes.mFramesPerPacket    = kXDXRecoderAACFramesPerPacket; // 採集的爲AAC須要將targetDes.mFramesPerPacket設置爲1024,AAC軟編碼須要餵給轉換器1024個樣點纔開始編碼,這與回調函數中inNumPackets有關,不可隨意更改
    
    OSStatus status     = 0;
    UInt32 targetSize   = sizeof(targetDes);
    status              = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &targetSize, &targetDes);
    // log4cplus_info("pcm", "create target data format status:%d",(int)status);
	
    memset(&_targetDes, 0, sizeof(_targetDes));
    // 賦給全局變量
    memcpy(&_targetDes, &targetDes, targetSize);
    
    // 選擇軟件編碼
    AudioClassDescription audioClassDes;
    status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                        sizeof(targetDes.mFormatID),
                                        &targetDes.mFormatID,
                                        &targetSize);
    // log4cplus_info("pcm","get kAudioFormatProperty_Encoders status:%d",(int)status);
    
    // 計算編碼器容量
    UInt32 numEncoders = targetSize/sizeof(AudioClassDescription);
    // 用數組存放編碼器內容
    AudioClassDescription audioClassArr[numEncoders];
	// 將編碼器屬性賦給數組
    AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                           sizeof(targetDes.mFormatID),
                           &targetDes.mFormatID,
                           &targetSize,
                           audioClassArr);
    // log4cplus_info("pcm","wrirte audioClassArr status:%d",(int)status);
    
 // 遍歷數組,設置軟編
    for (int i = 0; i < numEncoders; i++) {
        if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
            memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
            break;
        }
    }
    
    // 防止內存泄露	
	if (_encodeConvertRef == NULL) {
		// 新建一個編碼對象,設置原,目標格式
        status          = AudioConverterNewSpecific(&sourceDes, &targetDes, 1,
                                                    &audioClassDes, &_encodeConvertRef);
        
        if (status != noErr) {
//            log4cplus_info("Audio Recoder","new convertRef failed status:%d \n",(int)status);
            return @"Error : New convertRef failed \n";
        }
    }    
    
// 獲取原始格式大小
    targetSize      = sizeof(sourceDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentInputStreamDescription, &targetSize, &sourceDes);
    // log4cplus_info("pcm","get sourceDes status:%d",(int)status);
    
// 獲取目標格式大小
    targetSize      = sizeof(targetDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &targetSize, &targetDes);;
    // log4cplus_info("pcm","get targetDes status:%d",(int)status);
    
    // 設置碼率,須要和採樣率對應
    UInt32 bitRate  = kXDXRecoderConverterEncodeBitRate;
    targetSize      = sizeof(bitRate);
    status          = AudioConverterSetProperty(_encodeConvertRef,
                                                kAudioConverterEncodeBitRate,
                                                targetSize, &bitRate);
    // log4cplus_info("pcm","set covert property bit rate status:%d",(int)status);
        if (status != noErr) {
//        log4cplus_info("Audio Recoder","set covert property bit rate status:%d",(int)status);
        return @"Error : Set covert property bit rate failed";
    }
    
    return nil;
    
}
複製代碼

AudioFormatGetProperty:

原型: 
extern OSStatus
	 
AudioFormatGetProperty(	AudioFormatPropertyID    inPropertyID,
							UInt32				        inSpecifierSize,
							const void * __nullable  inSpecifier,
							UInt32 	 * __nullable  ioPropertyDataSize,
							void * __nullabl         outPropertyData);
做用:檢索某個屬性的值
複製代碼

AudioClassDescription:

指的是一個可以對一個信號或者一個數據流進行變換的設備或者程序。這裏指的變換既包括將 信號或者數據流進行編碼(一般是爲了傳輸、存儲或者加密)或者提取獲得一個編碼流的操做,也包括爲了觀察或者處理從這個編碼流中恢復適合觀察或操做的形式的操做。編解碼器常常用在視頻會議和流媒體等應用中。

默認狀況下,Apple會建立一個硬件編碼器,若是硬件不可用,會建立軟件編碼器。

通過個人測試,硬件AAC編碼器的編碼時延很高,須要buffer大約2秒的數據纔會開始編碼。而軟件編碼器的編碼時延就是正常的,只要餵給1024個樣點,就會開始編碼。

AudioConverterNewSpecific:
原型: extern OSStatus
AudioConverterNewSpecific(  const AudioStreamBasicDescription * inSourceFormat,
                            const AudioStreamBasicDescription * inDestinationFormat,
                            UInt32                              inNumberClassDescriptions,
                            const AudioClassDescription *       inClassDescriptions,
                            AudioConverterRef __nullable * __nonnull outAudioConverter);
      
解釋:建立一個轉換器
做用:設置一些轉碼基本信息          
複製代碼
AudioConverterSetProperty:
原型:extern OSStatus 
AudioConverterSetProperty(  AudioConverterRef           inAudioConverter,
                            AudioConverterPropertyID    inPropertyID,
                            UInt32                      inPropertyDataSize,
                            const void *                inPropertyData);
做用:設置碼率,須要注意,AAC並非隨便的碼率均可以支持。好比若是PCM採樣率是44100KHz,那麼碼率能夠設置64000bps,若是是16K,能夠設置爲32000bps。
複製代碼

(4).設置最終音頻文件的頭部信息(此類寫法爲將pcm轉爲AAC的寫法)

-(void)copyEncoderCookieToFile
{
    // Grab the cookie from the converter and write it to the destination file.
    UInt32 cookieSize = 0;
    OSStatus error = AudioConverterGetPropertyInfo(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);
    
    // If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not. // log4cplus_info("cookie","cookie status:%d %d",(int)error, cookieSize); if (error == noErr && cookieSize != 0) { char *cookie = (char *)malloc(cookieSize * sizeof(char)); // UInt32 *cookie = (UInt32 *)malloc(cookieSize * sizeof(UInt32)); error = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, cookie); // log4cplus_info("cookie","cookie size status:%d",(int)error); if (error == noErr) { error = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie); // log4cplus_info("cookie","set cookie status:%d ",(int)error); if (error == noErr) { UInt32 willEatTheCookie = false; error = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie); printf("Writing magic cookie to destination file: %u\n cookie:%d \n", (unsigned int)cookieSize, willEatTheCookie); } else { printf("Even though some formats have cookies, some files don't take them and that's OK\n"); } } else { printf("Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n"); } free(cookie); } } 複製代碼

Magic cookie 是一種不透明的數據格式,它和壓縮數據文件與流聯繫密切,若是文件數據爲CBR格式(無損),則不須要添加頭部信息,若是爲VBR須要添加,// if collect CBR needn't set magic cookie , if collect VBR should set magic cookie, if needn't to convert format that can be setting by audio queue directly.

(5).AudioQueue中註冊的回調函數

// AudioQueue中註冊的回調函數
static void inputBufferHandler(void *                                 inUserData,
                               AudioQueueRef                          inAQ,
                               AudioQueueBufferRef                    inBuffer,
                               const AudioTimeStamp *                 inStartTime,
                               UInt32                                 inNumPackets,
                               const AudioStreamPacketDescription*	  inPacketDesc) {
    // 至關於本類對象實例
    TVURecorder *recoder        = (TVURecorder *)inUserData;
    
 /*
     inNumPackets 總包數:音頻隊列緩衝區大小 (在先前估算緩存區大小爲kXDXRecoderAACFramesPerPacket*2)/ (dataFormat.mFramesPerPacket (採集數據每一個包中有多少幀,此處在初始化設置中爲1) * dataFormat.mBytesPerFrame(每一幀中有多少個字節,此處在初始化設置中爲每一幀中兩個字節)),因此能夠根據該公式計算捕捉PCM數據時inNumPackets。
     注意:若是採集的數據是PCM須要將dataFormat.mFramesPerPacket設置爲1,而本例中最終要的數據爲AAC,由於本例中使用的轉換器只有每次傳入1024幀才能開始工做,因此在AAC格式下須要將mFramesPerPacket設置爲1024.也就是採集到的inNumPackets爲1,在轉換器中傳入的inNumPackets應該爲AAC格式下默認的1,在此後寫入文件中也應該傳的是轉換好的inNumPackets,若是有特殊需求須要將採集的數據量小於1024,那麼須要將每次捕捉到的數據先預先存儲在一個buffer中,等到攢夠1024幀再進行轉換。
     */
    
    // collect pcm data,能夠在此存儲
    
    // First case : collect data not is 1024 frame, if collect data not is 1024 frame, we need to save data to pcm_buffer untill 1024 frame
    memcpy(pcm_buffer+pcm_buffer_size, inBuffer->mAudioData, inBuffer->mAudioDataByteSize);
    pcm_buffer_size = pcm_buffer_size + inBuffer->mAudioDataByteSize;
    if(inBuffer->mAudioDataByteSize != kXDXRecoderAACFramesPerPacket*2)
        NSLog(@"write pcm buffer size:%d, totoal buff size:%d", inBuffer->mAudioDataByteSize, pcm_buffer_size);

    frameCount++;
    
     // Second case : If the size of the data collection is not required, we can let mic collect 1024 frame so that don't need to write firtst case, but it is recommended to write the above code because of agility // if collect data is added to 1024 frame if(frameCount == totalFrames) { AudioBufferList *bufferList = convertPCMToAAC(recoder); pcm_buffer_size = 0; frameCount = 0; // free memory free(bufferList->mBuffers[0].mData); free(bufferList); // begin write audio data for record audio only // 出隊 AudioQueueRef queue = recoder.mQueue; if (recoder.isRunning) { AudioQueueEnqueueBuffer(queue, inBuffer, 0, NULL); } } } 複製代碼

解析回調函數:至關於中斷服務函數,每次錄取到音頻數據就進入這個函數

注意:inNumPackets 總包數:音頻隊列緩衝區大小 (在先前估算緩存區大小爲2048)/ (dataFormat.mFramesPerPacket (採集數據每一個包中有多少幀,此處在初始化設置中爲1) * dataFormat.mBytesPerFrame(每一幀中有多少個字節,此處在初始化設置中爲每一幀中兩個字節))

  • inAQ 是調用回調函數的音頻隊列
  • inBuffer 是一個被音頻隊列填充新的音頻數據的音頻隊列緩衝區,它包含了回調函數寫入文件所須要的新數據
  • inStartTime 是緩衝區中的一採樣的參考時間,對於基本的錄製,你的毀掉函數不會使用這個參數
  • inNumPackets是inPacketDescs參數中包描述符(packet descriptions)的數量,若是你正在錄製一個VBR(可變比特率(variable bitrate))格式, 音頻隊列將會提供這個參數給你的回調函數,這個參數可讓你傳遞給AudioFileWritePackets函數. CBR (常量比特率(constant bitrate)) 格式不使用包描述符。對於CBR錄製,音頻隊列會設置這個參數而且將inPacketDescs這個參數設置爲NULL,官方解釋爲The number of packets of audio data sent to the callback in the inBuffer parameter.
// PCM -> AAC
AudioBufferList* convertPCMToAAC (AudioQueueBufferRef inBuffer, XDXRecorder *recoder) {
    
    UInt32   maxPacketSize    = 0;
    UInt32   size             = sizeof(maxPacketSize);
    OSStatus status;
    
    status = AudioConverterGetProperty(_encodeConvertRef,
                                       kAudioConverterPropertyMaximumOutputPacketSize,
                                       &size,
                                       &maxPacketSize);
    // log4cplus_info("AudioConverter","kAudioConverterPropertyMaximumOutputPacketSize status:%d \n",(int)status);
    
// 初始化一個bufferList存儲數據
    AudioBufferList *bufferList             = (AudioBufferList *)malloc(sizeof(AudioBufferList));
    bufferList->mNumberBuffers              = 1;
    bufferList->mBuffers[0].mNumberChannels = _targetDes.mChannelsPerFrame;
    bufferList->mBuffers[0].mData           = malloc(maxPacketSize);
    bufferList->mBuffers[0].mDataByteSize   = pcm_buffer_size;

    AudioStreamPacketDescription outputPacketDescriptions;
    
    /*     
    inNumPackets設置爲1表示編碼產生1幀數據即返回,官方:On entry, the capacity of outOutputData expressed in packets in the converter's output format. On exit, the number of packets of converted data that were written to outOutputData. 在輸入表示輸出數據的最大容納能力 在轉換器的輸出格式上,在轉換完成時表示多少個包被寫入 */ UInt32 inNumPackets = 1; status = AudioConverterFillComplexBuffer(_encodeConvertRef, encodeConverterComplexInputDataProc, // 填充數據的回調函數 pcm_buffer, // 音頻隊列緩衝區中數據 &inNumPackets, bufferList, // 成功後將值賦給bufferList &outputPacketDescriptions); // 輸出包包含的一些信息 log4cplus_info("AudioConverter","set AudioConverterFillComplexBuffer status:%d",(int)status); if (recoder.needsVoiceDemo) { // if inNumPackets set not correct, file will not normally play. 將轉換器轉換出來的包寫入文件中,inNumPackets表示寫入文件的起始位置 OSStatus status = AudioFileWritePackets(recoder.mRecordFile, FALSE, bufferList->mBuffers[0].mDataByteSize, &outputPacketDescriptions, recoder.mRecordPacket, &inNumPackets, bufferList->mBuffers[0].mData); // log4cplus_info("write file","write file status = %d",(int)status); recoder.mRecordPacket += inNumPackets; // Used to record the location of the write file,用於記錄寫入文件的位置 } return bufferList; } 複製代碼

解析

outputPacketDescriptions數組是每次轉換的AAC編碼後各個包的描述,但這裏每次只轉換一包數據(由傳入的packetSize決定)。調用AudioConverterFillComplexBuffer觸發轉碼,他的第二個參數是填充原始音頻數據的回調。轉碼完成後,會將轉碼的數據存放在它的第五個參數中(bufferList).

// 錄製聲音功能
-(void)startVoiceDemo
{
   NSArray *searchPaths    = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
   NSString *documentPath  = [[searchPaths objectAtIndex:0] stringByAppendingPathComponent:@"VoiceDemo"];
   OSStatus status;
   
   // Get the full path to our file.
   NSString *fullFileName  = [NSString stringWithFormat:@"%@.%@",[[XDXDateTool shareXDXDateTool] getDateWithFormat_yyyy_MM_dd_HH_mm_ss],@"caf"];
   NSString *filePath      = [documentPath stringByAppendingPathComponent:fullFileName];
   [mRecordFilePath release];
   mRecordFilePath         = [filePath copy];;
   CFURLRef url            = CFURLCreateWithString(kCFAllocatorDefault, (CFStringRef)filePath, NULL);
   
   // create the audio file
   status                  = AudioFileCreateWithURL(url, kAudioFileMPEG4Type, &_targetDes, kAudioFileFlags_EraseFile, &mRecordFile);
   if (status != noErr) {
       // log4cplus_info("Audio Recoder","AudioFileCreateWithURL Failed, status:%d",(int)status);
   }
   
   CFRelease(url);
   
   // add magic cookie contain header file info for VBR data
   [self copyEncoderCookieToFile];
   
   mNeedsVoiceDemo         = YES;
   NSLog(@"%s",__FUNCTION__);
}
複製代碼

##--------------------------- Audio Unit -----------------------------

1. What is Audio Unit ? AudioUnit官方文檔, 優秀博客1

1). AudioUnit是 iOS提供的爲了支持混音,均衡,格式轉換,實時輸入輸出用於錄製,回放,離線渲染和實時回話(VOIP),這讓咱們能夠動態加載和使用,即從iOS應用程序中接收這些強大而靈活的插件。它是iOS音頻中最低層,因此除非你須要合成聲音的實時播放,低延遲的I/O,或特定聲音的特定特色。 Audio unit scopes and elements :

  • 上圖是一個AudioUnit的組成結構,A scope 主要使用到的輸入kAudioUnitScope_Input和輸出kAudioUnitScope_Output。Element 是嵌套在audio unit scope的編程上下文。

  • AudioUnit 的Remote IO有2個element,大部分代碼和文獻都用bus代替element,二者同義,bus0就是輸出,bus 1表明輸入,播放音頻文件就是在bus 0傳送數據,bus 1輸入在Remote IO 默認是關閉的,在錄音的狀態下 須要把bus 1設置成開啓狀態。

  • 咱們能使用(kAudioOutputUnitProperty_EnableIO)屬性獨立地開啓或禁用每一個element,Element 1 直接與音頻輸入硬件相連(麥克風),Element 1 的input scope對咱們是不透明的,來自輸入硬件的音頻數據只能在Element 1的output scope中訪問。

  • 一樣的element 0直接和輸出硬件相連(揚聲器),咱們能夠將audio數據傳輸到element 0的input scope中,可是output scope對咱們是不透明的。

  • 注意:每一個element自己都有一個輸入範圍和輸出範圍,所以在代碼中若是不理解可能會比較懵逼,好比你從input element的 output scope 中受到音頻,並將音頻發送到output element的intput scope中,若是代碼中不理解,能夠再看看上圖。

2.相關概念解析

2 - 1. I/O Units : iOS提供了3種I/O Units.

  • The Remote I/O unit 是最經常使用的,它鏈接音頻硬件的輸入和輸出而且提供單個傳入和傳出音頻樣本值得低延遲訪問。還支持硬件音頻格式和應用程序音頻格式的轉換,經過包含Format Converter unit來實現。
  • The Voice-Processing I/O unit 繼承了the Remote I/O unit 而且增長回聲消除用於VOIP或語音聊天應用。它還提供了自動增益校訂,語音處理的質量調整和靜音的功能。(本例中用此完成回聲消除)
  • The Generic Output unit 不鏈接音頻硬件,而是一共一種將處理鏈的輸出發送到應用程序的機制。一般用來進行脫機音頻處理。

3. 使用步驟:

1). 導入所需動態庫與頭文件(At runtime, obtain a reference to the dynamically-linkable library that defines an audio unit you want to use.)

2). 實例化audio unit(Instantiate the audio unit.)

3). 配置audioUnit的類型去完成特定的需求(Configure the audio unit as required for its type and to accomodate the intent of your app.)

4). 初始化uandio unit(Initialize the audio unit to prepare it to handle audio. )

5). 開始audio flow(Start audio flow.)

6). 控制audio unit(Control the audio unit.)

7). 結束後回收audio unit(When finished, deallocate the audio unit.)

4.代碼解析

  • 1). init.
- (void)initAudioComponent {
   OSStatus status;
   // 配置AudioUnit基本信息
   AudioComponentDescription audioDesc;
   audioDesc.componentType         = kAudioUnitType_Output;
   // 若是你的應用程序須要去除回聲將componentSubType設置爲kAudioUnitSubType_VoiceProcessingIO,不然根據需求設置爲其餘,在博客中有介紹
   audioDesc.componentSubType      = kAudioUnitSubType_VoiceProcessingIO;//kAudioUnitSubType_VoiceProcessingIO;
   // 蘋果本身的標誌
   audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
   audioDesc.componentFlags        = 0;
   audioDesc.componentFlagsMask    = 0;
   
   AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
   // 新建一個AudioComponent對象,只有這步完成才能進行後續步驟,因此順序不可顛倒
   status = AudioComponentInstanceNew(inputComponent, &_audioUnit);
   if (status != noErr)  {
       _audioUnit = NULL;
//        log4cplus_info("Audio Recoder", "couldn't create a new instance of AURemoteIO, status : %d \n",status);
   }
}
複製代碼

解析

  • To find an audio unit at runtime, start by specifying its type, subtype, and manufacturer keys in an audio component description data structure. You do this whether using the audio unit or audio processing graph API.
  • 要在運行時找到AudioUnit,首先要在AudioComponentDescription中指定它的類型,子類型和製做商,AudioComponentFindNext參數inComponent通常設置爲NULL,從系統中找到第一個符合inDesc描述的Component,若是爲其賦值,則從其以後進行尋找。AudioUnit實際上就是一個AudioComponentInstance實例對象
  • componentSubType通常可設置爲kAudioUnitSubType_RemoteIO,若是有特別需求,如本例中要去除回聲,則使用kAudioUnitSubType_VoiceProcessingIO,每種類型做用在2-1中均有描述,再也不重複。
- (void)initBuffer {
   // 禁用AudioUnit默認的buffer而使用咱們本身寫的全局BUFFER,用來接收每次採集的PCM數據,Disable AU buffer allocation for the recorder, we allocate our own.
   UInt32 flag     = 0;
   OSStatus status = AudioUnitSetProperty(_audioUnit,
                                          kAudioUnitProperty_ShouldAllocateBuffer,
                                          kAudioUnitScope_Output,
                                          INPUT_BUS,
                                          &flag,
                                          sizeof(flag));
   if (status != noErr) {
//        log4cplus_info("Audio Recoder", "couldn't AllocateBuffer of AudioUnitCallBack, status : %d \n",status);
   }
   _buffList = (AudioBufferList*)malloc(sizeof(AudioBufferList));
   _buffList->mNumberBuffers               = 1;
   _buffList->mBuffers[0].mNumberChannels  = dataFormat.mChannelsPerFrame;
   _buffList->mBuffers[0].mDataByteSize    = kTVURecoderPCMMaxBuffSize * sizeof(short);
   _buffList->mBuffers[0].mData            = (short *)malloc(sizeof(short) * kTVURecoderPCMMaxBuffSize);
}
複製代碼

解析

本例經過禁用AudioUnit默認的buffer而使用咱們本身寫的全局BUFFER,用來接收每次採集的PCM數據,Disable AU buffer allocation for the recorder, we allocate our own.還有一種寫法是可使用回調中提供的ioData存儲採集的數據,這裏使用全局的buff是爲了供其餘地方使用,可根據須要自行決定採用哪一種方式,若不採用全局buffer則不可採用上述禁用操做。

// 由於本例只作錄音功能,未實現播放功能,因此沒有設置播放相關設置。
- (void)setAudioUnitPropertyAndFormat {
    OSStatus status;
    [self setUpRecoderWithFormatID:kAudioFormatLinearPCM];
    
    // 應用audioUnit設置的格式
    status = AudioUnitSetProperty(_audioUnit,
                                  kAudioUnitProperty_StreamFormat,
                                  kAudioUnitScope_Output,
                                  INPUT_BUS,
                                  &dataFormat,
                                  sizeof(dataFormat));
    if (status != noErr) {
//        log4cplus_info("Audio Recoder", "couldn't set the input client format on AURemoteIO, status : %d \n",status);
    }
    // 去除回聲開關
    UInt32 echoCancellation;
    AudioUnitSetProperty(_audioUnit,
                         kAUVoiceIOProperty_BypassVoiceProcessing,
                         kAudioUnitScope_Global,
                         0,
                         &echoCancellation,
                         sizeof(echoCancellation));
    
    // AudioUnit輸入端默認是關閉,須要將他打開
    UInt32 flag = 1;
    status      = AudioUnitSetProperty(_audioUnit,
                                       kAudioOutputUnitProperty_EnableIO,
                                       kAudioUnitScope_Input,
                                       INPUT_BUS,
                                       &flag,
                                       sizeof(flag));
    if (status != noErr) {
//        log4cplus_info("Audio Recoder", "could not enable input on AURemoteIO, status : %d \n",status);
    }
}

-(void)setUpRecoderWithFormatID:(UInt32)formatID {
    // Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設置爲官方推薦設置,可根據具體需求修改部分設置
    //setup auido sample rate, channel number, and format ID
    memset(&dataFormat, 0, sizeof(dataFormat));
    
    UInt32 size = sizeof(dataFormat.mSampleRate);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareSampleRate,
                            &size,
                            &dataFormat.mSampleRate);
    dataFormat.mSampleRate = kXDXAudioSampleRate;
    
    size = sizeof(dataFormat.mChannelsPerFrame);
    AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
                            &size,
                            &dataFormat.mChannelsPerFrame);
    dataFormat.mFormatID = formatID;
    dataFormat.mChannelsPerFrame = 1;
    
    if (formatID == kAudioFormatLinearPCM) {
        if (self.releaseMethod == XDXRecorderReleaseMethodAudioQueue) {
            dataFormat.mFormatFlags     = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
        }else if (self.releaseMethod == XDXRecorderReleaseMethodAudioQueue) {
            dataFormat.mFormatFlags     = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
        }
        
        dataFormat.mBitsPerChannel  = 16;
        dataFormat.mBytesPerPacket  = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
        dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket; // 用AudioQueue採集pcm須要這麼設置
    }
}


複製代碼

解析

上述操做針對錄音功能須要對Audio Unit作出對應設置,首先設置ASBD採集數據爲PCM的格式,須要注意的是若是是使用AudioQueue與AudioUnit的dataFormat.mFormatFlags設置略有不一樣,經測試必須這樣設置,緣由暫不詳,設置完後使用AudioUnitSetProperty應用設置,這裏只作錄音,因此對kAudioOutputUnitProperty_EnableIO 的 kAudioUnitScope_Input 開啓,而對kAudioUnitScope_Output 輸入端輸出的音頻格式進行設置,若是不理解可參照1中概念解析進行理解,kAUVoiceIOProperty_BypassVoiceProcessing則是回聲的開關。

-(NSString *)convertBasicSetting {
    // 此處目標格式其餘參數均爲默認,系統會自動計算,不然沒法進入encodeConverterComplexInputDataProc回調

    AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
    AudioStreamBasicDescription targetDes;              // 轉碼後格式
    
    // 設置目標格式及基本信息
    memset(&targetDes, 0, sizeof(targetDes));
    targetDes.mFormatID           = kAudioFormatMPEG4AAC;
    targetDes.mSampleRate         = kXDXAudioSampleRate;
    targetDes.mChannelsPerFrame   = dataFormat.mChannelsPerFrame;
    targetDes.mFramesPerPacket    = kXDXRecoderAACFramesPerPacket; // 採集的爲AAC須要將targetDes.mFramesPerPacket設置爲1024,AAC軟編碼須要餵給轉換器1024個樣點纔開始編碼,這與回調函數中inNumPackets有關,不可隨意更改
    
    OSStatus status     = 0;
    UInt32 targetSize   = sizeof(targetDes);
    status              = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL, &targetSize, &targetDes);
    // log4cplus_info("pcm", "create target data format status:%d",(int)status);
	
    memset(&_targetDes, 0, sizeof(_targetDes));
    // 賦給全局變量
    memcpy(&_targetDes, &targetDes, targetSize);
    
    // 選擇軟件編碼
    AudioClassDescription audioClassDes;
    status = AudioFormatGetPropertyInfo(kAudioFormatProperty_Encoders,
                                        sizeof(targetDes.mFormatID),
                                        &targetDes.mFormatID,
                                        &targetSize);
    // log4cplus_info("pcm","get kAudioFormatProperty_Encoders status:%d",(int)status);
    
    // 計算編碼器容量
    UInt32 numEncoders = targetSize/sizeof(AudioClassDescription);
    // 用數組存放編碼器內容
    AudioClassDescription audioClassArr[numEncoders];
	// 將編碼器屬性賦給數組
    AudioFormatGetProperty(kAudioFormatProperty_Encoders,
                           sizeof(targetDes.mFormatID),
                           &targetDes.mFormatID,
                           &targetSize,
                           audioClassArr);
    // log4cplus_info("pcm","wrirte audioClassArr status:%d",(int)status);
    
 // 遍歷數組,設置軟編
    for (int i = 0; i < numEncoders; i++) {
        if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
            memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
            break;
        }
    }
    
    // 防止內存泄露	
	if (_encodeConvertRef == NULL) {
		// 新建一個編碼對象,設置原,目標格式
        status          = AudioConverterNewSpecific(&sourceDes, &targetDes, 1,
                                                    &audioClassDes, &_encodeConvertRef);
        
        if (status != noErr) {
//            log4cplus_info("Audio Recoder","new convertRef failed status:%d \n",(int)status);
            return @"Error : New convertRef failed \n";
        }
    }    
    
// 獲取原始格式大小
    targetSize      = sizeof(sourceDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentInputStreamDescription, &targetSize, &sourceDes);
    // log4cplus_info("pcm","get sourceDes status:%d",(int)status);
    
// 獲取目標格式大小
    targetSize      = sizeof(targetDes);
    status          = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &targetSize, &targetDes);;
    // log4cplus_info("pcm","get targetDes status:%d",(int)status);
    
    // 設置碼率,須要和採樣率對應
    UInt32 bitRate  = kXDXRecoderConverterEncodeBitRate;
    targetSize      = sizeof(bitRate);
    status          = AudioConverterSetProperty(_encodeConvertRef,
                                                kAudioConverterEncodeBitRate,
                                                targetSize, &bitRate);
    // log4cplus_info("pcm","set covert property bit rate status:%d",(int)status);
        if (status != noErr) {
//        log4cplus_info("Audio Recoder","set covert property bit rate status:%d",(int)status);
        return @"Error : Set covert property bit rate failed";
    }
    
    return nil;
    
}

複製代碼

解析

設置原格式與轉碼格式並建立_encodeConvertRef轉碼器對象完成相關初始化操做,值得注意的是targetDes.mFramesPerPacket設置爲1024,AAC軟編碼須要餵給轉換器1024個樣點纔開始編碼,不可隨意更改,緣由以下圖,由AAC編碼器決定。

- (void)initRecordeCallback {
    // 設置回調,有兩種方式,一種是採集pcm的BUFFER使用系統回調中的參數,另外一種是使用咱們本身的,本例中使用的是本身的,因此回調中的ioData爲空。
    
    // 方法1:
    AURenderCallbackStruct recordCallback;
    recordCallback.inputProc        = RecordCallback;
    recordCallback.inputProcRefCon  = (__bridge void *)self;
    OSStatus status                 = AudioUnitSetProperty(_audioUnit,
                                                           kAudioOutputUnitProperty_SetInputCallback,
                                                           kAudioUnitScope_Global,
                                                           INPUT_BUS,
                                                           &recordCallback,
                                                           sizeof(recordCallback));
                                                           
    	// 方法2:
      AURenderCallbackStruct renderCallback;
      renderCallback.inputProc		  = RecordCallback;
      renderCallback.inputProcRefCon   = (__bridge void *)self;
      AudioUnitSetProperty(_rioUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, 0, & RecordCallback, sizeof(RecordCallback));

    
    if (status != noErr) {
//        log4cplus_info("Audio Recoder", "Audio Unit set record Callback failed, status : %d \n",status);
    }
}

複製代碼

解析

以上爲設置採集回調,有兩種方式,1種爲使用咱們本身的buffer,這樣須要先在上述initBuffer中禁用系統的buffer,則回調函數中每次渲染的爲咱們本身的buffer,另外一種則是使用系統的buffer,對應須要在回調函數中將ioData放進渲染的函數中。

static OSStatus RecordCallback(void *inRefCon,
                               AudioUnitRenderActionFlags *ioActionFlags,
                               const AudioTimeStamp *inTimeStamp,
                               UInt32 inBusNumber,
                               UInt32 inNumberFrames,
                               AudioBufferList *ioData) {
/*
      注意:若是採集的數據是PCM須要將dataFormat.mFramesPerPacket設置爲1,而本例中最終要的數據爲AAC,由於本例中使用的轉換器只有每次傳入1024幀才能開始工做,因此在AAC格式下須要將mFramesPerPacket設置爲1024.也就是採集到的inNumPackets爲1,在轉換器中傳入的inNumPackets應該爲AAC格式下默認的1,在此後寫入文件中也應該傳的是轉換好的inNumPackets,若是有特殊需求須要將採集的數據量小於1024,那麼須要將每次捕捉到的數據先預先存儲在一個buffer中,等到攢夠1024幀再進行轉換。
 */
    
    XDXRecorder *recorder = (XDXRecorder *)inRefCon;
    
    // 將回調數據傳給_buffList
    AudioUnitRender(recorder->_audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, recorder->_buffList);
    
    void    *bufferData = recorder->_buffList->mBuffers[0].mData;
    UInt32   bufferSize = recorder->_buffList->mBuffers[0].mDataByteSize;
    //    printf("Audio Recoder Render dataSize : %d \n",bufferSize);
    
    // 因爲PCM轉成AAC的轉換器每次須要有1024個採樣點(每一幀2個字節)才能完成一次轉換,因此每次須要2048大小的數據,這裏定義的pcm_buffer用來累加每次存儲的bufferData
    memcpy(pcm_buffer+pcm_buffer_size, bufferData, bufferSize);
    pcm_buffer_size = pcm_buffer_size + bufferSize;
    
    if(pcm_buffer_size >= kTVURecoderPCMMaxBuffSize) {
        AudioBufferList *bufferList = convertPCMToAAC(recorder);
        
        // 由於採樣不可能每次都精準的採集到1024個樣點,因此若是大於2048大小就先填滿2048,剩下的跟着下一次採集一塊兒送給轉換器
        memcpy(pcm_buffer, pcm_buffer + kTVURecoderPCMMaxBuffSize, pcm_buffer_size - kTVURecoderPCMMaxBuffSize);
        pcm_buffer_size = pcm_buffer_size - kTVURecoderPCMMaxBuffSize;
        
        // free memory
        if(bufferList) {
            free(bufferList->mBuffers[0].mData);
            free(bufferList);
        }
    }
    return noErr;
}

複製代碼

解析

在該回調中若是採用咱們本身定義的全局buffer,則回調函數參數中的ioData爲NULL,再也不使用,若是想使用ioData按照上述設置並將其放入AudioUnitRender函數中進行渲染,回調函數中採用pcm_buffer存儲滿2048個字節的數組傳給轉換器,這是編碼器的特性,因此若是採集的數據小於2048先取pcm_buffer的前2048個字節,後面的數據與下次採集的PCM數據累加在一塊兒。上述轉換過程在AudioQueue中已經有介紹,邏輯徹底相同,可在上文中閱讀。

-(void)copyEncoderCookieToFile
{
    // Grab the cookie from the converter and write it to the destination file.
    UInt32 cookieSize = 0;
    OSStatus error = AudioConverterGetPropertyInfo(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, NULL);
    
    // If there is an error here, then the format doesn't have a cookie - this is perfectly fine as som formats do not. // log4cplus_info("cookie","cookie status:%d %d",(int)error, cookieSize); if (error == noErr && cookieSize != 0) { char *cookie = (char *)malloc(cookieSize * sizeof(char)); // UInt32 *cookie = (UInt32 *)malloc(cookieSize * sizeof(UInt32)); error = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCompressionMagicCookie, &cookieSize, cookie); // log4cplus_info("cookie","cookie size status:%d",(int)error); if (error == noErr) { error = AudioFileSetProperty(mRecordFile, kAudioFilePropertyMagicCookieData, cookieSize, cookie); // log4cplus_info("cookie","set cookie status:%d ",(int)error); if (error == noErr) { UInt32 willEatTheCookie = false; error = AudioFileGetPropertyInfo(mRecordFile, kAudioFilePropertyMagicCookieData, NULL, &willEatTheCookie); printf("Writing magic cookie to destination file: %u\n cookie:%d \n", (unsigned int)cookieSize, willEatTheCookie); } else { printf("Even though some formats have cookies, some files don't take them and that's OK\n"); } } else { printf("Could not Get kAudioConverterCompressionMagicCookie from Audio Converter!\n"); } free(cookie); } } 複製代碼

解析

Magic cookie 是一種不透明的數據格式,它和壓縮數據文件與流聯繫密切,若是文件數據爲CBR格式(無損),則不須要添加頭部信息,若是爲VBR須要添加,// if collect CBR needn't set magic cookie , if collect VBR should set magic cookie, if needn't to convert format that can be setting by audio queue directly.

- (void)startAudioUnitRecorder {
    OSStatus status;
    
    if (isRunning) {
//        log4cplus_info("Audio Recoder", "Start recorder repeat \n");
        return;
    }
    
    [self initGlobalVar];
    
//    log4cplus_info("Audio Recoder", "starup PCM audio encoder \n");
    
    status = AudioOutputUnitStart(_audioUnit);
//    log4cplus_info("Audio Recoder", "AudioOutputUnitStart status : %d \n",status);
    if (status == noErr) {
        isRunning  = YES;
        hostTime   = 0;
    }
}

-(void)stopAudioUnitRecorder {
    if (isRunning == NO) {
//        log4cplus_info("Audio Recoder", "Stop recorder repeat \n");
        return;
    }
    
//    log4cplus_info("Audio Recoder","stop pcm encoder \n");
    
    isRunning = NO;
    
    [self copyEncoderCookieToFile];
    OSStatus status = AudioOutputUnitStop(_audioUnit);
    if (status != noErr){
//        log4cplus_info("Audio Recoder", "stop AudioUnit failed. \n");
    }
    
    AudioFileClose(mRecordFile);
}

複製代碼

解析

因爲AudioUnit的初始化在本類中初始化方法中完成,因此只須要調用start,stop方法便可控制錄製轉碼過程。切記不可在start方法中完成audio unit對象的建立和初始化,不然會發生異常。

總結:開始寫這篇文章是在三月初剛剛接觸音頻相關項目,當時直接使用AudioQueue來進行操做,可慢慢發現因爲公司項目對直播要求很高,AudioQueue中有些致命缺點好比:回調時間沒法精確控制,採集出來的數據大小問題,以及沒法消除回聲問題,因此二次從新開發採用AudioUnit,在本例中我已經將兩種寫法都總結出來,可根據需求決定到底使用哪一種,Demo中也有兩套API的封裝,轉碼邏輯基本相同,但也有略微差異,後續若是有問題也能夠問我,簡信我就好,若是幫到你能夠幫忙在gitHub裏點顆星星,歡迎轉載。

參考:CoreAudio, Audio Unit, 轉碼操做, AudioUnit, Audio Unit, 回聲消除, AudioQueue, 直播基礎

相關文章
相關標籤/搜索