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

小東邪發表於2019-03-04

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

原理:由於公司需求更改為Mic採集的pcm一路提供給WebRTC使用,另一路將pcm轉為aac,將aac提供給直播用的API。因此應該先讓Mic採集原始pcm資料,採用AudioQueue/AudioUnit兩種方式採集,然後在回撥函式中將其轉換為aac提供給C++API


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

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


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 作用:在一段記憶體塊中填充某個特定的值,它是對較大的結構體或陣列進行清零操作的一種最快方法。

2.memcpy: 原型: void * memcpy(void * dest, const void * src, size_t n); 解釋:從源src所指的記憶體地址的起始位置開始拷貝n個位元組到目標dest所指的記憶體地址的起始位置中

3.void free(void *); 解釋:釋放記憶體,需要將malloc出來的記憶體統統釋放掉,對於結構體要先將結構體中malloc出來的釋放掉最後再釋放掉結構體本身。

OC 中部分知識點:

1.OSStaus:狀態碼,如果沒有錯誤返回0:(即noErr)

2.AudioFormatGetPropertyInfo:

原型: 
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原始音訊資料。

  3. 資料介紹

(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 :

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

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

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

  • 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編碼器決定。

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

- (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, 直播基礎

相關文章