本例需求:將Mic采集的PCM轉(zhuǎn)成AAC,可得到兩種不同數(shù)據(jù),本例采用AudioQueue/AudioUnit兩種方式存儲,即: 可采集到兩種聲音數(shù)據(jù),一種為PCM,一種為轉(zhuǎn)換后的AAC.
原理:由于公司需求更改為Mic采集的pcm一路提供給WebRTC使用,另一路將pcm轉(zhuǎn)為aac,將aac提供給直播用的API。因此應該先讓Mic采集原始pcm數(shù)據(jù),采用AudioQueue/AudioUnit兩種方式采集,然后在回調(diào)函數(shù)中將其轉(zhuǎn)換為aac提供給C++API

本例中僅包含部分代碼,建議下載代碼詳細看,在關(guān)鍵代碼中都有注釋中可以看到難理解的含義.
GitHub地址(附代碼) : PCM->AAC
簡書地址 : PCM->AAC
博客地址 : PCM->AAC
掘金地址 : PCM->AAC
實現(xiàn)方式:(下文兩種實現(xiàn)方式,挑選自己適合的)
1.AudioQueue : 若對延遲要求不高,可實現(xiàn)錄制,播放,暫停,回退,同步,轉(zhuǎn)換(PCM->AAC等)等功能可采用這種方式
2.AudioUnit : 比AudioQueue更加底層,可實現(xiàn)高性能,低延遲,并且包括去除回聲,混音等等功能。
AudioQueue為什么會出現(xiàn)波動的情況?解決方法?這種波動的原因是在Audio Queue的底層產(chǎn)生的,之前說過,Audio ToolBox是基于Audio Unit的,回調(diào)函數(shù)的波動要到底層才能解決。
一.本文需要基本知識點
C語言相關(guān)函數(shù):
1.memset:
原型: void * memset(void * __b, int __c, size_t __len);
解釋:將s中當前位置后面的n個字節(jié)(typedef unsigned int size_t) 用ch替換并返回s
作用:在一段內(nèi)存塊中填充某個特定的值,它是對較大的結(jié)構(gòu)體或數(shù)組進行清零操作的一種最快方法。
2.memcpy:
原型: void * memcpy(void * dest, const void * src, size_t n);
解釋:從源src所指的內(nèi)存地址的起始位置開始拷貝n個字節(jié)到目標dest所指的內(nèi)存地址的起始位置中
3.void free(void *);
解釋:釋放內(nèi)存,需要將malloc出來的內(nèi)存統(tǒng)統(tǒng)釋放掉,對于結(jié)構(gòu)體要先將結(jié)構(gòu)體中malloc出來的釋放掉最后再釋放掉結(jié)構(gòu)體本身。
OC 中部分知識點:
1.OSStaus:狀態(tài)碼,如果沒有錯誤返回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屬性的值(比如采樣率,聲道數(shù)等等)
4.AudioUnitSetProperty
extern OSStatus
AudioUnitSetProperty( AudioUnit inUnit,
AudioUnitPropertyID inID,
AudioUnitScope inScope,
AudioUnitElement inElement,
const void * __nullable inData,
UInt32 inDataSize)
* 作用:設置AudioUnit特定屬性的值,其中scope,element不理解可參考下文audio unit概念部分,這里可以設置音頻流的各種參數(shù),比如采樣頻率、量化位數(shù)、通道個數(shù)、每包中幀的個數(shù)等等
音頻基礎(chǔ)知識
AVFoundation框架中的AVAudioPlayer和AVAudioRecorder類,用法簡單,但是不支持流式,也就意味著在播放音頻前,必須等到整個音頻加載完成后,才能開始播放音頻;錄音時,也必須等到錄音結(jié)束后才能獲得錄音數(shù)據(jù)。
在iOS和Mac OS X中,音頻隊列Audio Queues是一個用來錄制和播放音頻的軟件對象,也就是說,可以用來錄音和播放,錄音能夠獲取實時的PCM原始音頻數(shù)據(jù)。
數(shù)據(jù)介紹
(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)音頻文件的組成:文件格式(或者音頻容器)+數(shù)據(jù)格式(或者音頻編碼)
知識點:
- 文件格式是用于形容文件本身的格式,可以通過多種不同方法為真正的音頻數(shù)據(jù)編碼,例如CAF文件便是一種文件格式,它能夠包含MP3格式,線性PCM以及其他數(shù)據(jù)格式音頻
線性PCM:這是表示線性脈沖編碼機制,主要是描寫用于將模擬聲音數(shù)據(jù)轉(zhuǎn)換成數(shù)組格式的技術(shù),簡單地說也就是未壓縮的數(shù)據(jù)。因為數(shù)據(jù)是未壓縮的,所以我們便可以最快速地播放出音頻,而如果空間不是問題的話這便是iPhone 音頻的優(yōu)先代碼選擇
(2).音頻文件計算大小
簡述:聲卡對聲音的處理質(zhì)量可以用三個基本參數(shù)來衡量,即采樣頻率,采樣位數(shù)和聲道數(shù)。
知識點:
采樣頻率:單位時間內(nèi)采樣次數(shù)。采樣頻率越大,采樣點之間的間隔就越小,數(shù)字化后得到的聲音就越逼真,但相應的數(shù)據(jù)量就越大,聲卡一般提供11.025kHz,22.05kHz和44.1kHz等不同的采樣頻率。
采樣位數(shù):記錄每次采樣值數(shù)值大小的位數(shù)。采樣位數(shù)通常有8bits或16bits兩種,采樣位數(shù)越大,所能記錄的聲音變化度就越細膩,相應的數(shù)據(jù)量就越大。
聲道數(shù):處理的聲音是單聲道還是立體聲。單聲道在聲音處理過程中只有單數(shù)據(jù)流,而立體聲則需要左右聲道的兩個數(shù)據(jù)流。顯然,立體聲的效果要好,但相應數(shù)據(jù)量要比單聲道數(shù)據(jù)量加倍。
聲音數(shù)據(jù)量的計算公式:數(shù)據(jù)量(字節(jié) / 秒)=(采樣頻率(Hz)* 采樣位數(shù)(bit)* 聲道數(shù))/ 8
單聲道的聲道數(shù)為1,立體聲的聲道數(shù)為2. 字節(jié)B,1MB=1024KB = 1024*1024B
(3)
-
CoreAudio 介紹
CoreAudio
(1). CoreAudio分為三層結(jié)構(gòu),如上圖
1.最底層的I/O Kit, MIDI, HAL等用于直接與硬件相關(guān)操作,一般來說用不到。
2.中間層服務是對數(shù)據(jù)格式的轉(zhuǎn)換,對硬盤執(zhí)行讀寫操作,解析流,使用插件等。
- 其中AudioConverter Services 可實現(xiàn)不同音頻格式的轉(zhuǎn)碼,如PCM->AAC等
- Audio File Services支持讀寫音頻數(shù)據(jù)從硬盤
- Audio Unit Services and Audio Processing Graph Services 可實現(xiàn)使應用程序處理數(shù)字信號,完成一些插件功能,如均衡器和混聲器等。
- Audio File Stream Services 可以使程序解析流,如播放一段來自網(wǎng)絡的音頻。
- Audio Format Services 幫助應用程序管理音頻格式相關(guān)操作
3.最高層是用基于底層實現(xiàn)的部分功能,使用相對簡單。 - Audio Queue Services 可實現(xiàn)錄音,播放,暫停,同步音頻等功能
- AVAudioPlayer 提供簡單地OC接口對于音頻的播放與暫停,功能較為局限。
- OpenAL 實現(xiàn)三維混音音頻單元頂部,適合開發(fā)游戲
(2).Audio Data Formats:通過設置一組屬性代碼可以和操作系統(tǒng)支持的任何格式一起工作。(包括采樣率,比特率),對于AudioQueue與AudioUnit設置略有不同。
struct AudioStreamBasicDescription
{
Float64 mSampleRate; // 采樣率 :Hz
AudioFormatID mFormatID; // 采樣數(shù)據(jù)的類型,PCM,AAC等
AudioFormatFlags mFormatFlags; // 每種格式特定的標志,無損編碼 ,0表示沒有
UInt32 mBytesPerPacket; // 一個數(shù)據(jù)包中的字節(jié)數(shù)
UInt32 mFramesPerPacket; // 一個數(shù)據(jù)包中的幀數(shù),每個packet的幀數(shù)。如果是未壓縮的音頻數(shù)據(jù),值是1。動態(tài)幀率格式,這個值是一個較大的固定數(shù)字,比如說AAC的1024。如果是動態(tài)大小幀數(shù)(比如Ogg格式)設置為0。
UInt32 mBytesPerFrame; // 每一幀中的字節(jié)數(shù)
UInt32 mChannelsPerFrame; // 每一幀數(shù)據(jù)中的通道數(shù),單聲道為1,立體聲為2
UInt32 mBitsPerChannel; // 每個通道中的位數(shù),1byte = 8bit
UInt32 mReserved; // 8字節(jié)對齊,填0
};
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
---------------------------- Audio Queue ---------------------------
二.AudioQueue
.音頻隊列 — 詳細請參考 Audio Queue,該文章中已有詳細描述,不再重復介紹,不懂請參考。
1.簡述:在iOS和Mac OS X中,音頻隊列是一個用來錄制和播放音頻的軟件對象,他用AudioQueueRef這個不透明數(shù)據(jù)類型來表示,該類型在AudioQueue.h頭文件中聲明。
2.工作:
- 連接音頻硬件
- 內(nèi)存管理
- 根據(jù)需要為已壓縮的音頻格式引入編碼器
- 媒體的錄制或播放
你可以將音頻隊列配合其他Core Audio的接口使用,再加上相對少量的自定義代碼就可以在你的應用程序中創(chuàng)建一套完整的數(shù)字音頻錄制或播放解決方案。
3.結(jié)構(gòu):
一組音頻隊列緩沖區(qū)(audio queue buffers),每個音頻隊列緩沖區(qū)都是一個存儲音頻數(shù)據(jù)的臨時倉庫
一個緩沖區(qū)隊列(buffer queue),一個包含音頻隊列緩沖區(qū)的有序列表
一個你自己編寫的音頻隊列回調(diào)函數(shù)(audio queue callback)
它的架構(gòu)很大程度上依賴于這個音頻隊列是用來錄制還是用來播放的。不同之處在于音頻隊列如何連接到它的輸入和輸入,還有它的回調(diào)函數(shù)所扮演的角色。
4.調(diào)用步驟,首先將項目設置為MRC,在控制器中配置audioSession基本設置(基本設置,不會谷歌),導入該頭文件,直接在需要時機調(diào)用該類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用來在捕捉聲音的回調(diào)中存儲累加的PCM原始數(shù)據(jù)
memset(pcm_buffer, 0, pcm_buffer_size);
pcm_buffer_size = 0;
frameCount = 0;
// 是否正在錄制
if (isRunning) {
// log4cplus_info("pcm", "Start recorder repeat");
return;
}
// 本例中采用log4打印log信息,若你沒有可以不用,刪除有關(guān)Log4的語句
// log4cplus_info("pcm", "starup PCM audio encoder");
// 設置采集的數(shù)據(jù)的類型為PCM
[self setUpRecoderWithFormatID:kAudioFormatLinearPCM];
OSStatus status = 0;
UInt32 size = sizeof(dataFormat);
// 編碼器轉(zhuǎn)碼設置
[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];
}
// 新建一個隊列,第二個參數(shù)注冊回調(diào)函數(shù),第三個防止內(nèi)存泄露
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);
// 這里將頭信息添加到寫入文件中,若文件數(shù)據(jù)為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);
// 設置三個音頻隊列緩沖區(qū)
for (int i = 0; i != kNumberQueueBuffers; i++) {
// 注意:為每個緩沖區(qū)分配大小,可根據(jù)具體需求進行修改,但是一定要注意必須滿足轉(zhuǎn)換器的需求,轉(zhuǎn)換器只有每次給1024幀數(shù)據(jù)才會完成一次轉(zhuǎn)換,如果需求為采集數(shù)據(jù)量較少則用本例提供的pcm_buffer對數(shù)據(jù)進行累加后再處理
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);
}
初始化輸出流的結(jié)構(gòu)體描述
struct AudioStreamBasicDescription
{
Float64 mSampleRate; // 采樣率 :Hz
AudioFormatID mFormatID; // 采樣數(shù)據(jù)的類型,PCM,AAC等
AudioFormatFlags mFormatFlags; // 每種格式特定的標志,無損編碼 ,0表示沒有
UInt32 mBytesPerPacket; // 一個數(shù)據(jù)包中的字節(jié)數(shù)
UInt32 mFramesPerPacket; // 一個數(shù)據(jù)包中的幀數(shù),每個packet的幀數(shù)。如果是未壓縮的音頻數(shù)據(jù),值是1。動態(tài)幀率格式,這個值是一個較大的固定數(shù)字,比如說AAC的1024。如果是動態(tài)大小幀數(shù)(比如Ogg格式)設置為0。
UInt32 mBytesPerFrame; // 每一幀中的字節(jié)數(shù)
UInt32 mChannelsPerFrame; // 每一幀數(shù)據(jù)中的通道數(shù),單聲道為1,立體聲為2
UInt32 mBitsPerChannel; // 每個通道中的位數(shù),1byte = 8bit
UInt32 mReserved; // 8字節(jié)對齊,填0
};
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
注意: kNumberQueueBuffers,音頻隊列可以使用任意數(shù)量的緩沖區(qū)。你的應用程序制定它的數(shù)量。一般情況下這個數(shù)字是3。這樣就可以讓給一個忙于將數(shù)據(jù)寫入磁盤,同時另一個在填充新的音頻數(shù)據(jù),第三個緩沖區(qū)在需要做磁盤I/O延遲補償?shù)臅r候可用
如何使用AudioQueue:
- 創(chuàng)建輸入隊列AudioQueueNewInput
- 分配buffers
- 入隊:AudioQueueEnqueueBuffer
- 回調(diào)函數(shù)采集音頻數(shù)據(jù)
- 出隊
AudioQueueNewInput
// 作用:創(chuàng)建一個音頻隊列為了錄制音頻數(shù)據(jù)
原型:extern OSStatus
AudioQueueNewInput( const AudioStreamBasicDescription *inFormat, 同上
AudioQueueInputCallback inCallbackProc, // 注冊回調(diào)函數(shù)
void * __nullable inUserData,
CFRunLoopRef __nullable inCallbackRunLoop,
CFStringRef __nullable inCallbackRunLoopMode,
UInt32 inFlags,
AudioQueueRef __nullable * __nonnull outAQ);
// 這個函數(shù)的第四個和第五個參數(shù)是有關(guān)于線程的,我設置成null,代表它默認使用內(nèi)部線程去錄音,而且還是異步的
(2).設置采集數(shù)據(jù)的格式,采集PCM必須按照如下設置,參考蘋果官方文檔,不同需求自己另行修改
-(void)setUpRecoderWithFormatID:(UInt32)formatID {
// Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設置為官方推薦設置,可根據(jù)具體需求修改部分設置
//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;
// 關(guān)于采集PCM數(shù)據(jù)是根據(jù)蘋果官方文檔給出的Demo設置,至于為什么這么設置可能與采集回調(diào)函數(shù)內(nèi)部實現(xiàn)有關(guān),修改的話請謹慎
if (formatID == kAudioFormatLinearPCM)
{
/*
為保存音頻數(shù)據(jù)的方式的說明,如可以根據(jù)大端字節(jié)序或小端字節(jié)序,
浮點數(shù)或整數(shù)以及不同體位去保存數(shù)據(jù)
例如對PCM格式通常我們?nèi)缦略O置:kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked等
*/
dataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
// 每個通道里,一幀采集的bit數(shù)目
dataFormat.mBitsPerChannel = 16;
// 8bit為1byte,即為1個通道里1幀需要采集2byte數(shù)據(jù),再*通道數(shù),即為所有通道采集的byte數(shù)目
dataFormat.mBytesPerPacket = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
// 每個包中的幀數(shù),采集PCM數(shù)據(jù)需要將dataFormat.mFramesPerPacket設置為1,否則回調(diào)不成功
dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket;
}
}
(3).將PCM轉(zhuǎn)成AAC一些基本設置
-(NSString *)convertBasicSetting {
// 此處目標格式其他參數(shù)均為默認,系統(tǒng)會自動計算,否則無法進入encodeConverterComplexInputDataProc回調(diào)
AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
AudioStreamBasicDescription targetDes; // 轉(zhuǎn)碼后格式
// 設置目標格式及基本信息
memset(&targetDes, 0, sizeof(targetDes));
targetDes.mFormatID = kAudioFormatMPEG4AAC;
targetDes.mSampleRate = kXDXAudioSampleRate;
targetDes.mChannelsPerFrame = dataFormat.mChannelsPerFrame;
targetDes.mFramesPerPacket = kXDXRecoderAACFramesPerPacket; // 采集的為AAC需要將targetDes.mFramesPerPacket設置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個樣點才開始編碼,這與回調(diào)函數(shù)中inNumPackets有關(guān),不可隨意更改
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);
// 用數(shù)組存放編碼器內(nèi)容
AudioClassDescription audioClassArr[numEncoders];
// 將編碼器屬性賦給數(shù)組
AudioFormatGetProperty(kAudioFormatProperty_Encoders,
sizeof(targetDes.mFormatID),
&targetDes.mFormatID,
&targetSize,
audioClassArr);
// log4cplus_info("pcm","wrirte audioClassArr status:%d",(int)status);
// 遍歷數(shù)組,設置軟編
for (int i = 0; i < numEncoders; i++) {
if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
break;
}
}
// 防止內(nèi)存泄露
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:
指的是一個能夠?qū)σ粋€信號或者一個數(shù)據(jù)流進行變換的設備或者程序。這里指的變換既包括將 信號或者數(shù)據(jù)流進行編碼(通常是為了傳輸、存儲或者加密)或者提取得到一個編碼流的操作,也包括為了觀察或者處理從這個編碼流中恢復適合觀察或操作的形式的操作。編解碼器經(jīng)常用在視頻會議和流媒體等應用中。
默認情況下,Apple會創(chuàng)建一個硬件編碼器,如果硬件不可用,會創(chuàng)建軟件編碼器。
經(jīng)過我的測試,硬件AAC編碼器的編碼時延很高,需要buffer大約2秒的數(shù)據(jù)才會開始編碼。而軟件編碼器的編碼時延就是正常的,只要喂給1024個樣點,就會開始編碼。
AudioConverterNewSpecific:
原型: extern OSStatus
AudioConverterNewSpecific( const AudioStreamBasicDescription * inSourceFormat,
const AudioStreamBasicDescription * inDestinationFormat,
UInt32 inNumberClassDescriptions,
const AudioClassDescription * inClassDescriptions,
AudioConverterRef __nullable * __nonnull outAudioConverter);
解釋:創(chuàng)建一個轉(zhuǎn)換器
作用:設置一些轉(zhuǎn)碼基本信息
AudioConverterSetProperty:
原型:extern OSStatus
AudioConverterSetProperty( AudioConverterRef inAudioConverter,
AudioConverterPropertyID inPropertyID,
UInt32 inPropertyDataSize,
const void * inPropertyData);
作用:設置碼率,需要注意,AAC并不是隨便的碼率都可以支持。比如如果PCM采樣率是44100KHz,那么碼率可以設置64000bps,如果是16K,可以設置為32000bps。
(4).設置最終音頻文件的頭部信息(此類寫法為將pcm轉(zhuǎn)為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 是一種不透明的數(shù)據(jù)格式,它和壓縮數(shù)據(jù)文件與流聯(lián)系密切,如果文件數(shù)據(jù)為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中注冊的回調(diào)函數(shù)
// AudioQueue中注冊的回調(diào)函數(shù)
static void inputBufferHandler(void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumPackets,
const AudioStreamPacketDescription* inPacketDesc) {
// 相當于本類對象實例
TVURecorder *recoder = (TVURecorder *)inUserData;
/*
inNumPackets 總包數(shù):音頻隊列緩沖區(qū)大小 (在先前估算緩存區(qū)大小為kXDXRecoderAACFramesPerPacket*2)/ (dataFormat.mFramesPerPacket (采集數(shù)據(jù)每個包中有多少幀,此處在初始化設置中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個字節(jié),此處在初始化設置中為每一幀中兩個字節(jié))),所以可以根據(jù)該公式計算捕捉PCM數(shù)據(jù)時inNumPackets。
注意:如果采集的數(shù)據(jù)是PCM需要將dataFormat.mFramesPerPacket設置為1,而本例中最終要的數(shù)據(jù)為AAC,因為本例中使用的轉(zhuǎn)換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設置為1024.也就是采集到的inNumPackets為1,在轉(zhuǎn)換器中傳入的inNumPackets應該為AAC格式下默認的1,在此后寫入文件中也應該傳的是轉(zhuǎn)換好的inNumPackets,如果有特殊需求需要將采集的數(shù)據(jù)量小于1024,那么需要將每次捕捉到的數(shù)據(jù)先預先存儲在一個buffer中,等到攢夠1024幀再進行轉(zhuǎn)換。
*/
// 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);
}
}
}
解析回調(diào)函數(shù):相當于中斷服務函數(shù),每次錄取到音頻數(shù)據(jù)就進入這個函數(shù)
注意:inNumPackets 總包數(shù):音頻隊列緩沖區(qū)大小 (在先前估算緩存區(qū)大小為2048)/ (dataFormat.mFramesPerPacket (采集數(shù)據(jù)每個包中有多少幀,此處在初始化設置中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個字節(jié),此處在初始化設置中為每一幀中兩個字節(jié)))
- inAQ 是調(diào)用回調(diào)函數(shù)的音頻隊列
- inBuffer 是一個被音頻隊列填充新的音頻數(shù)據(jù)的音頻隊列緩沖區(qū),它包含了回調(diào)函數(shù)寫入文件所需要的新數(shù)據(jù)
- inStartTime 是緩沖區(qū)中的一采樣的參考時間,對于基本的錄制,你的毀掉函數(shù)不會使用這個參數(shù)
- inNumPackets是inPacketDescs參數(shù)中包描述符(packet descriptions)的數(shù)量,如果你正在錄制一個VBR(可變比特率(variable bitrate))格式, 音頻隊列將會提供這個參數(shù)給你的回調(diào)函數(shù),這個參數(shù)可以讓你傳遞給AudioFileWritePackets函數(shù). CBR (常量比特率(constant bitrate)) 格式不使用包描述符。對于CBR錄制,音頻隊列會設置這個參數(shù)并且將inPacketDescs這個參數(shù)設置為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存儲數(shù)據(jù)
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表示編碼產(chǎn)生1幀數(shù)據(jù)即返回,官方: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. 在輸入表示輸出數(shù)據(jù)的最大容納能力 在轉(zhuǎn)換器的輸出格式上,在轉(zhuǎn)換完成時表示多少個包被寫入
*/
UInt32 inNumPackets = 1;
status = AudioConverterFillComplexBuffer(_encodeConvertRef,
encodeConverterComplexInputDataProc, // 填充數(shù)據(jù)的回調(diào)函數(shù)
pcm_buffer, // 音頻隊列緩沖區(qū)中數(shù)據(jù)
&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. 將轉(zhuǎn)換器轉(zhuǎn)換出來的包寫入文件中,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數(shù)組是每次轉(zhuǎn)換的AAC編碼后各個包的描述,但這里每次只轉(zhuǎn)換一包數(shù)據(jù)(由傳入的packetSize決定)。調(diào)用AudioConverterFillComplexBuffer觸發(fā)轉(zhuǎn)碼,他的第二個參數(shù)是填充原始音頻數(shù)據(jù)的回調(diào)。轉(zhuǎn)碼完成后,會將轉(zhuǎn)碼的數(shù)據(jù)存放在它的第五個參數(shù)中(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官方文檔, 優(yōu)秀博客1
1). AudioUnit是 iOS提供的為了支持混音,均衡,格式轉(zhuǎn)換,實時輸入輸出用于錄制,回放,離線渲染和實時回話(VOIP),這讓我們可以動態(tài)加載和使用,即從iOS應用程序中接收這些強大而靈活的插件。它是iOS音頻中最低層,所以除非你需要合成聲音的實時播放,低延遲的I/O,或特定聲音的特定特點。
Audio unit scopes and elements :

-
上圖是一個AudioUnit的組成結(jié)構(gòu),A scope 主要使用到的輸入kAudioUnitScope_Input和輸出kAudioUnitScope_Output。Element 是嵌套在audio unit scope的編程上下文。
IOUnit.png
- AudioUnit 的Remote IO有2個element,大部分代碼和文獻都用bus代替element,兩者同義,bus0就是輸出,bus 1代表輸入,播放音頻文件就是在bus 0傳送數(shù)據(jù),bus 1輸入在Remote IO 默認是關(guān)閉的,在錄音的狀態(tài)下 需要把bus 1設置成開啟狀態(tài)。
- 我們能使用(kAudioOutputUnitProperty_EnableIO)屬性獨立地開啟或禁用每個element,Element 1 直接與音頻輸入硬件相連(麥克風),Element 1 的input scope對我們是不透明的,來自輸入硬件的音頻數(shù)據(jù)只能在Element 1的output scope中訪問。
- 同樣的element 0直接和輸出硬件相連(揚聲器),我們可以將audio數(shù)據(jù)傳輸?shù)絜lement 0的input scope中,但是output scope對我們是不透明的。
- 注意:每個element本身都有一個輸入范圍和輸出范圍,因此在代碼中如果不理解可能會比較懵逼,比如你從input element的 output scope 中受到音頻,并將音頻發(fā)送到output element的intput scope中,如果代碼中不理解,可以再看看上圖。
2.相關(guān)概念解析
2 - 1. I/O Units : iOS提供了3種I/O Units.
- The Remote I/O unit 是最常用的,它連接音頻硬件的輸入和輸出并且提供單個傳入和傳出音頻樣本值得低延遲訪問。還支持硬件音頻格式和應用程序音頻格式的轉(zhuǎn)換,通過包含F(xiàn)ormat Converter unit來實現(xiàn)。
- The Voice-Processing I/O unit 繼承了the Remote I/O unit 并且增加回聲消除用于VOIP或語音聊天應用。它還提供了自動增益校正,語音處理的質(zhì)量調(diào)整和靜音的功能。(本例中用此完成回聲消除)
- The Generic Output unit 不連接音頻硬件,而是一共一種將處理鏈的輸出發(fā)送到應用程序的機制。通常用來進行脫機音頻處理。
3. 使用步驟:
1). 導入所需動態(tài)庫與頭文件(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). 結(jié)束后回收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,否則根據(jù)需求設置為其他,在博客中有介紹
audioDesc.componentSubType = kAudioUnitSubType_VoiceProcessingIO;//kAudioUnitSubType_VoiceProcessingIO;
// 蘋果自己的標志
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
// 新建一個AudioComponent對象,只有這步完成才能進行后續(xù)步驟,所以順序不可顛倒
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參數(shù)inComponent一般設置為NULL,從系統(tǒng)中找到第一個符合inDesc描述的Component,如果為其賦值,則從其之后進行尋找。AudioUnit實際上就是一個AudioComponentInstance實例對象
- componentSubType一般可設置為kAudioUnitSubType_RemoteIO,如果有特別需求,如本例中要去除回聲,則使用kAudioUnitSubType_VoiceProcessingIO,每種類型作用在2-1中均有描述,不再重復。
- (void)initBuffer {
// 禁用AudioUnit默認的buffer而使用我們自己寫的全局BUFFER,用來接收每次采集的PCM數(shù)據(jù),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數(shù)據(jù),Disable AU buffer allocation for the recorder, we allocate our own.還有一種寫法是可以使用回調(diào)中提供的ioData存儲采集的數(shù)據(jù),這里使用全局的buff是為了供其他地方使用,可根據(jù)需要自行決定采用哪種方式,若不采用全局buffer則不可采用上述禁用操作。
// 因為本例只做錄音功能,未實現(xiàn)播放功能,所以沒有設置播放相關(guān)設置。
- (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);
}
// 去除回聲開關(guān)
UInt32 echoCancellation;
AudioUnitSetProperty(_audioUnit,
kAUVoiceIOProperty_BypassVoiceProcessing,
kAudioUnitScope_Global,
0,
&echoCancellation,
sizeof(echoCancellation));
// AudioUnit輸入端默認是關(guān)閉,需要將他打開
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. 此處的設置為官方推薦設置,可根據(jù)具體需求修改部分設置
//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采集數(shù)據(jù)為PCM的格式,需要注意的是如果是使用AudioQueue與AudioUnit的dataFormat.mFormatFlags設置略有不同,經(jīng)測試必須這樣設置,原因暫不詳,設置完后使用AudioUnitSetProperty應用設置,這里只做錄音,所以對kAudioOutputUnitProperty_EnableIO 的 kAudioUnitScope_Input 開啟,而對kAudioUnitScope_Output 輸入端輸出的音頻格式進行設置,如果不理解可參照1中概念解析進行理解,kAUVoiceIOProperty_BypassVoiceProcessing則是回聲的開關(guān)。
-(NSString *)convertBasicSetting {
// 此處目標格式其他參數(shù)均為默認,系統(tǒng)會自動計算,否則無法進入encodeConverterComplexInputDataProc回調(diào)
AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
AudioStreamBasicDescription targetDes; // 轉(zhuǎn)碼后格式
// 設置目標格式及基本信息
memset(&targetDes, 0, sizeof(targetDes));
targetDes.mFormatID = kAudioFormatMPEG4AAC;
targetDes.mSampleRate = kXDXAudioSampleRate;
targetDes.mChannelsPerFrame = dataFormat.mChannelsPerFrame;
targetDes.mFramesPerPacket = kXDXRecoderAACFramesPerPacket; // 采集的為AAC需要將targetDes.mFramesPerPacket設置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個樣點才開始編碼,這與回調(diào)函數(shù)中inNumPackets有關(guān),不可隨意更改
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);
// 用數(shù)組存放編碼器內(nèi)容
AudioClassDescription audioClassArr[numEncoders];
// 將編碼器屬性賦給數(shù)組
AudioFormatGetProperty(kAudioFormatProperty_Encoders,
sizeof(targetDes.mFormatID),
&targetDes.mFormatID,
&targetSize,
audioClassArr);
// log4cplus_info("pcm","wrirte audioClassArr status:%d",(int)status);
// 遍歷數(shù)組,設置軟編
for (int i = 0; i < numEncoders; i++) {
if (audioClassArr[i].mSubType == kAudioFormatMPEG4AAC && audioClassArr[i].mManufacturer == kAppleSoftwareAudioCodecManufacturer) {
memcpy(&audioClassDes, &audioClassArr[i], sizeof(AudioClassDescription));
break;
}
}
// 防止內(nèi)存泄露
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;
}
解析
設置原格式與轉(zhuǎn)碼格式并創(chuàng)建_encodeConvertRef轉(zhuǎn)碼器對象完成相關(guān)初始化操作,值得注意的是targetDes.mFramesPerPacket設置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個樣點才開始編碼,不可隨意更改,原因如下圖,由AAC編碼器決定。

- (void)initRecordeCallback {
// 設置回調(diào),有兩種方式,一種是采集pcm的BUFFER使用系統(tǒng)回調(diào)中的參數(shù),另一種是使用我們自己的,本例中使用的是自己的,所以回調(diào)中的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);
}
}
解析
以上為設置采集回調(diào),有兩種方式,1種為使用我們自己的buffer,這樣需要先在上述initBuffer中禁用系統(tǒng)的buffer,則回調(diào)函數(shù)中每次渲染的為我們自己的buffer,另一種則是使用系統(tǒng)的buffer,對應需要在回調(diào)函數(shù)中將ioData放進渲染的函數(shù)中。
static OSStatus RecordCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
/*
注意:如果采集的數(shù)據(jù)是PCM需要將dataFormat.mFramesPerPacket設置為1,而本例中最終要的數(shù)據(jù)為AAC,因為本例中使用的轉(zhuǎn)換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設置為1024.也就是采集到的inNumPackets為1,在轉(zhuǎn)換器中傳入的inNumPackets應該為AAC格式下默認的1,在此后寫入文件中也應該傳的是轉(zhuǎn)換好的inNumPackets,如果有特殊需求需要將采集的數(shù)據(jù)量小于1024,那么需要將每次捕捉到的數(shù)據(jù)先預先存儲在一個buffer中,等到攢夠1024幀再進行轉(zhuǎn)換。
*/
XDXRecorder *recorder = (XDXRecorder *)inRefCon;
// 將回調(diào)數(shù)據(jù)傳給_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轉(zhuǎn)成AAC的轉(zhuǎn)換器每次需要有1024個采樣點(每一幀2個字節(jié))才能完成一次轉(zhuǎn)換,所以每次需要2048大小的數(shù)據(jù),這里定義的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,剩下的跟著下一次采集一起送給轉(zhuǎn)換器
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;
}
解析
在該回調(diào)中如果采用我們自己定義的全局buffer,則回調(diào)函數(shù)參數(shù)中的ioData為NULL,不再使用,如果想使用ioData按照上述設置并將其放入AudioUnitRender函數(shù)中進行渲染,回調(diào)函數(shù)中采用pcm_buffer存儲滿2048個字節(jié)的數(shù)組傳給轉(zhuǎn)換器,這是編碼器的特性,所以如果采集的數(shù)據(jù)小于2048先取pcm_buffer的前2048個字節(jié),后面的數(shù)據(jù)與下次采集的PCM數(shù)據(jù)累加在一起。上述轉(zhuǎn)換過程在AudioQueue中已經(jīng)有介紹,邏輯完全相同,可在上文中閱讀。
-(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 是一種不透明的數(shù)據(jù)格式,它和壓縮數(shù)據(jù)文件與流聯(lián)系密切,如果文件數(shù)據(jù)為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的初始化在本類中初始化方法中完成,所以只需要調(diào)用start,stop方法即可控制錄制轉(zhuǎn)碼過程。切記不可在start方法中完成audio unit對象的創(chuàng)建和初始化,否則會發(fā)生異常。

