本例需求:將Mic采集的PCM轉(zhuǎn)成AAC线椰,可得到兩種不同數(shù)據(jù),本例采用AudioQueue/AudioUnit兩種方式存儲(chǔ),即: 可采集到兩種聲音數(shù)據(jù),一種為PCM,一種為轉(zhuǎn)換后的AAC.
原理:由于公司需求更改為Mic采集的pcm一路提供給WebRTC使用,另一路將pcm轉(zhuǎn)為aac尘盼,將aac提供給直播用的API憨愉。因此應(yīng)該先讓Mic采集原始pcm數(shù)據(jù),采用AudioQueue/AudioUnit兩種方式采集卿捎,然后在回調(diào)函數(shù)中將其轉(zhuǎn)換為aac提供給C++API
本例中僅包含部分代碼,建議下載代碼詳細(xì)看,在關(guān)鍵代碼中都有注釋中可以看到難理解的含義.
GitHub地址(附代碼) : PCM->AAC
簡(jiǎn)書地址 : PCM->AAC
博客地址 : PCM->AAC
掘金地址 : PCM->AAC
實(shí)現(xiàn)方式:(下文兩種實(shí)現(xiàn)方式配紫,挑選自己適合的)
1.AudioQueue : 若對(duì)延遲要求不高,可實(shí)現(xiàn)錄制娇澎,播放笨蚁,暫停睹晒,回退趟庄,同步括细,轉(zhuǎn)換(PCM->AAC等)等功能可采用這種方式
2.AudioUnit : 比AudioQueue更加底層,可實(shí)現(xiàn)高性能戚啥,低延遲奋单,并且包括去除回聲,混音等等功能猫十。
AudioQueue為什么會(huì)出現(xiàn)波動(dòng)的情況览濒?解決方法?這種波動(dòng)的原因是在Audio Queue的底層產(chǎn)生的拖云,之前說過贷笛,Audio ToolBox是基于Audio Unit的,回調(diào)函數(shù)的波動(dòng)要到底層才能解決宙项。
一.本文需要基本知識(shí)點(diǎn)
C語言相關(guān)函數(shù):
1.memset:
原型: void * memset(void * __b, int __c, size_t __len);
解釋:將s中當(dāng)前位置后面的n個(gè)字節(jié)(typedef unsigned int size_t) 用ch替換并返回s
作用:在一段內(nèi)存塊中填充某個(gè)特定的值乏苦,它是對(duì)較大的結(jié)構(gòu)體或數(shù)組進(jìn)行清零操作的一種最快方法。
2.memcpy:
原型: void * memcpy(void * dest, const void * src, size_t n);
解釋:從源src所指的內(nèi)存地址的起始位置開始拷貝n個(gè)字節(jié)到目標(biāo)dest所指的內(nèi)存地址的起始位置中
3.void free(void *);
解釋:釋放內(nèi)存尤筐,需要將malloc出來的內(nèi)存統(tǒng)統(tǒng)釋放掉汇荐,對(duì)于結(jié)構(gòu)體要先將結(jié)構(gòu)體中malloc出來的釋放掉最后再釋放掉結(jié)構(gòu)體本身。
OC 中部分知識(shí)點(diǎn):
1.OSStaus:狀態(tài)碼盆繁,如果沒有錯(cuò)誤返回0:(即noErr)
2.AudioFormatGetPropertyInfo:
原型:
AudioFormatGetPropertyInfo(
AudioFormatPropertyID inPropertyID,
UInt32 inSpecifierSize,
const void * __nullable inSpecifier,
UInt32 * outPropertyDataSize);
* 作用:檢索給定屬性的信息掀淘,比如編碼器目標(biāo)格式的size等
3.AudioSessionGetProperty:
原型:
extern OSStatus
AudioSessionGetProperty(
AudioSessionPropertyID inID,
UInt32 *ioDataSize,
void *outData);
* 作用:獲取指定AudioSession對(duì)象的inID屬性的值(比如采樣率,聲道數(shù)等等)
4.AudioUnitSetProperty
extern OSStatus
AudioUnitSetProperty( AudioUnit inUnit,
AudioUnitPropertyID inID,
AudioUnitScope inScope,
AudioUnitElement inElement,
const void * __nullable inData,
UInt32 inDataSize)
* 作用:設(shè)置AudioUnit特定屬性的值油昂,其中scope,element不理解可參考下文audio unit概念部分,這里可以設(shè)置音頻流的各種參數(shù)革娄,比如采樣頻率、量化位數(shù)冕碟、通道個(gè)數(shù)稠腊、每包中幀的個(gè)數(shù)等等
音頻基礎(chǔ)知識(shí)
AVFoundation框架中的AVAudioPlayer和AVAudioRecorder類,用法簡(jiǎn)單鸣哀,但是不支持流式架忌,也就意味著在播放音頻前,必須等到整個(gè)音頻加載完成后我衬,才能開始播放音頻叹放;錄音時(shí),也必須等到錄音結(jié)束后才能獲得錄音數(shù)據(jù)挠羔。
在iOS和Mac OS X中井仰,音頻隊(duì)列Audio Queues是一個(gè)用來錄制和播放音頻的軟件對(duì)象,也就是說破加,可以用來錄音和播放俱恶,錄音能夠獲取實(shí)時(shí)的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í)點(diǎn):
- 文件格式是用于形容文件本身的格式,可以通過多種不同方法為真正的音頻數(shù)據(jù)編碼合是,例如CAF文件便是一種文件格式了罪,它能夠包含MP3格式,線性PCM以及其他數(shù)據(jù)格式音頻
線性PCM:這是表示線性脈沖編碼機(jī)制聪全,主要是描寫用于將模擬聲音數(shù)據(jù)轉(zhuǎn)換成數(shù)組格式的技術(shù)泊藕,簡(jiǎn)單地說也就是未壓縮的數(shù)據(jù)。因?yàn)閿?shù)據(jù)是未壓縮的难礼,所以我們便可以最快速地播放出音頻娃圆,而如果空間不是問題的話這便是iPhone 音頻的優(yōu)先代碼選擇
(2).音頻文件計(jì)算大小
簡(jiǎn)述:聲卡對(duì)聲音的處理質(zhì)量可以用三個(gè)基本參數(shù)來衡量,即采樣頻率蛾茉,采樣位數(shù)和聲道數(shù)讼呢。
知識(shí)點(diǎn):
采樣頻率:?jiǎn)挝粫r(shí)間內(nèi)采樣次數(shù)。采樣頻率越大谦炬,采樣點(diǎn)之間的間隔就越小吝岭,數(shù)字化后得到的聲音就越逼真,但相應(yīng)的數(shù)據(jù)量就越大吧寺,聲卡一般提供11.025kHz,22.05kHz和44.1kHz等不同的采樣頻率窜管。
采樣位數(shù):記錄每次采樣值數(shù)值大小的位數(shù)。采樣位數(shù)通常有8bits或16bits兩種稚机,采樣位數(shù)越大幕帆,所能記錄的聲音變化度就越細(xì)膩,相應(yīng)的數(shù)據(jù)量就越大赖条。
聲道數(shù):處理的聲音是單聲道還是立體聲失乾。單聲道在聲音處理過程中只有單數(shù)據(jù)流,而立體聲則需要左右聲道的兩個(gè)數(shù)據(jù)流纬乍。顯然碱茁,立體聲的效果要好,但相應(yīng)數(shù)據(jù)量要比單聲道數(shù)據(jù)量加倍仿贬。
聲音數(shù)據(jù)量的計(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.中間層服務(wù)是對(duì)數(shù)據(jù)格式的轉(zhuǎn)換队伟,對(duì)硬盤執(zhí)行讀寫操作穴吹,解析流,使用插件等嗜侮。
- 其中AudioConverter Services 可實(shí)現(xiàn)不同音頻格式的轉(zhuǎn)碼港令,如PCM->AAC等
- Audio File Services支持讀寫音頻數(shù)據(jù)從硬盤
- Audio Unit Services and Audio Processing Graph Services 可實(shí)現(xiàn)使應(yīng)用程序處理數(shù)字信號(hào)啥容,完成一些插件功能,如均衡器和混聲器等顷霹。
- Audio File Stream Services 可以使程序解析流咪惠,如播放一段來自網(wǎng)絡(luò)的音頻。
- Audio Format Services 幫助應(yīng)用程序管理音頻格式相關(guān)操作
3.最高層是用基于底層實(shí)現(xiàn)的部分功能泼返,使用相對(duì)簡(jiǎn)單。 - Audio Queue Services 可實(shí)現(xiàn)錄音姨拥,播放绅喉,暫停,同步音頻等功能
- AVAudioPlayer 提供簡(jiǎn)單地OC接口對(duì)于音頻的播放與暫停叫乌,功能較為局限柴罐。
- OpenAL 實(shí)現(xiàn)三維混音音頻單元頂部,適合開發(fā)游戲
(2).Audio Data Formats:通過設(shè)置一組屬性代碼可以和操作系統(tǒng)支持的任何格式一起工作憨奸。(包括采樣率革屠,比特率),對(duì)于AudioQueue與AudioUnit設(shè)置略有不同排宰。
struct AudioStreamBasicDescription
{
Float64 mSampleRate; // 采樣率 :Hz
AudioFormatID mFormatID; // 采樣數(shù)據(jù)的類型似芝,PCM,AAC等
AudioFormatFlags mFormatFlags; // 每種格式特定的標(biāo)志,無損編碼 板甘,0表示沒有
UInt32 mBytesPerPacket; // 一個(gè)數(shù)據(jù)包中的字節(jié)數(shù)
UInt32 mFramesPerPacket; // 一個(gè)數(shù)據(jù)包中的幀數(shù)党瓮,每個(gè)packet的幀數(shù)。如果是未壓縮的音頻數(shù)據(jù)盐类,值是1寞奸。動(dòng)態(tài)幀率格式,這個(gè)值是一個(gè)較大的固定數(shù)字在跳,比如說AAC的1024枪萄。如果是動(dòng)態(tài)大小幀數(shù)(比如Ogg格式)設(shè)置為0。
UInt32 mBytesPerFrame; // 每一幀中的字節(jié)數(shù)
UInt32 mChannelsPerFrame; // 每一幀數(shù)據(jù)中的通道數(shù)猫妙,單聲道為1瓷翻,立體聲為2
UInt32 mBitsPerChannel; // 每個(gè)通道中的位數(shù),1byte = 8bit
UInt32 mReserved; // 8字節(jié)對(duì)齊割坠,填0
};
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
---------------------------- Audio Queue ---------------------------
二.AudioQueue
.音頻隊(duì)列 — 詳細(xì)請(qǐng)參考 Audio Queue,該文章中已有詳細(xì)描述逻悠,不再重復(fù)介紹,不懂請(qǐng)參考韭脊。
1.簡(jiǎn)述:在iOS和Mac OS X中童谒,音頻隊(duì)列是一個(gè)用來錄制和播放音頻的軟件對(duì)象,他用AudioQueueRef這個(gè)不透明數(shù)據(jù)類型來表示沪羔,該類型在AudioQueue.h頭文件中聲明饥伊。
2.工作:
- 連接音頻硬件
- 內(nèi)存管理
- 根據(jù)需要為已壓縮的音頻格式引入編碼器
- 媒體的錄制或播放
你可以將音頻隊(duì)列配合其他Core Audio的接口使用象浑,再加上相對(duì)少量的自定義代碼就可以在你的應(yīng)用程序中創(chuàng)建一套完整的數(shù)字音頻錄制或播放解決方案。
3.結(jié)構(gòu):
一組音頻隊(duì)列緩沖區(qū)(audio queue buffers)琅豆,每個(gè)音頻隊(duì)列緩沖區(qū)都是一個(gè)存儲(chǔ)音頻數(shù)據(jù)的臨時(shí)倉庫
一個(gè)緩沖區(qū)隊(duì)列(buffer queue)愉豺,一個(gè)包含音頻隊(duì)列緩沖區(qū)的有序列表
一個(gè)你自己編寫的音頻隊(duì)列回調(diào)函數(shù)(audio queue callback)
它的架構(gòu)很大程度上依賴于這個(gè)音頻隊(duì)列是用來錄制還是用來播放的。不同之處在于音頻隊(duì)列如何連接到它的輸入和輸入茫因,還有它的回調(diào)函數(shù)所扮演的角色蚪拦。
4.調(diào)用步驟,首先將項(xiàng)目設(shè)置為MRC,在控制器中配置audioSession基本設(shè)置(基本設(shè)置冻押,不會(huì)谷歌)驰贷,導(dǎo)入該頭文件,直接在需要時(shí)機(jī)調(diào)用該類startRecord與stopRecord方法洛巢,另外還提供了生成錄音文件的功能括袒,具體參考github中的代碼。
本例中涉及的一些宏定義,具體可以下載代碼詳細(xì)看
#define kBufferDurationSeconds .5
#define kXDXRecoderAudioBytesPerPacket 2
#define kXDXRecoderAACFramesPerPacket 1024
#define kXDXRecoderPCMTotalPacket 512
#define kXDXRecoderPCMFramesPerPacket 1
#define kXDXRecoderConverterEncodeBitRate 64000
#define kXDXAudioSampleRate 48000.0
(1).設(shè)置AudioStreamBasicDescription 基本信息
-(void)startRecorder {
// Reset pcm_buffer to save convert handle, 每次開始音頻會(huì)話前初始化pcm_buffer, pcm_buffer用來在捕捉聲音的回調(diào)中存儲(chǔ)累加的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è)置采集的數(shù)據(jù)的類型為PCM
[self setUpRecoderWithFormatID:kAudioFormatLinearPCM];
OSStatus status = 0;
UInt32 size = sizeof(dataFormat);
// 編碼器轉(zhuǎn)碼設(shè)置
[self convertBasicSetting];
// 這個(gè)if語句用來檢測(cè)是否初始化本例對(duì)象成功,如果不成功重啟三次,三次后如果失敗可以進(jìn)行其他處理
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];
}
// 新建一個(gè)隊(duì)列,第二個(gè)參數(shù)注冊(cè)回調(diào)函數(shù)资盅,第三個(gè)防止內(nèi)存泄露
status = AudioQueueNewInput(&dataFormat, inputBufferHandler, (__bridge void *)(self), NULL, NULL, 0, &mQueue);
// log4cplus_info("pcm","AudioQueueNewInput status:%d",(int)status);
// 獲取隊(duì)列屬性
status = AudioQueueGetProperty(mQueue, kAudioQueueProperty_StreamDescription, &dataFormat, &size);
// log4cplus_info("pcm","AudioQueueNewInput status:%u",(unsigned int)dataFormat.mFormatID);
// 這里將頭信息添加到寫入文件中,若文件數(shù)據(jù)為CBR,不需要添加恃慧,為VBR需要添加
[self copyEncoderCookieToFile];
// 可以計(jì)算獲得,在這里使用的是固定大小
// bufferByteSize = [self computeRecordBufferSizeFrom:&dataFormat andDuration:kBufferDurationSeconds];
// log4cplus_info("pcm","pcm raw data buff number:%d, channel number:%u",
kNumberQueueBuffers,
dataFormat.mChannelsPerFrame);
// 設(shè)置三個(gè)音頻隊(duì)列緩沖區(qū)
for (int i = 0; i != kNumberQueueBuffers; i++) {
// 注意:為每個(gè)緩沖區(qū)分配大小渺蒿,可根據(jù)具體需求進(jìn)行修改,但是一定要注意必須滿足轉(zhuǎn)換器的需求,轉(zhuǎn)換器只有每次給1024幀數(shù)據(jù)才會(huì)完成一次轉(zhuǎn)換,如果需求為采集數(shù)據(jù)量較少則用本例提供的pcm_buffer對(duì)數(shù)據(jù)進(jìn)行累加后再處理
status = AudioQueueAllocateBuffer(mQueue, kXDXRecoderPCMTotalPacket*kXDXRecoderAudioBytesPerPacket*dataFormat.mChannelsPerFrame, &mBuffers[i]);
// 入隊(duì)
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; // 每種格式特定的標(biāo)志,無損編碼 蘸嘶,0表示沒有
UInt32 mBytesPerPacket; // 一個(gè)數(shù)據(jù)包中的字節(jié)數(shù)
UInt32 mFramesPerPacket; // 一個(gè)數(shù)據(jù)包中的幀數(shù)良瞧,每個(gè)packet的幀數(shù)。如果是未壓縮的音頻數(shù)據(jù)训唱,值是1褥蚯。動(dòng)態(tài)幀率格式,這個(gè)值是一個(gè)較大的固定數(shù)字况增,比如說AAC的1024赞庶。如果是動(dòng)態(tài)大小幀數(shù)(比如Ogg格式)設(shè)置為0。
UInt32 mBytesPerFrame; // 每一幀中的字節(jié)數(shù)
UInt32 mChannelsPerFrame; // 每一幀數(shù)據(jù)中的通道數(shù)澳骤,單聲道為1歧强,立體聲為2
UInt32 mBitsPerChannel; // 每個(gè)通道中的位數(shù),1byte = 8bit
UInt32 mReserved; // 8字節(jié)對(duì)齊为肮,填0
};
typedef struct AudioStreamBasicDescription AudioStreamBasicDescription;
注意: kNumberQueueBuffers摊册,音頻隊(duì)列可以使用任意數(shù)量的緩沖區(qū)。你的應(yīng)用程序制定它的數(shù)量颊艳。一般情況下這個(gè)數(shù)字是3茅特。這樣就可以讓給一個(gè)忙于將數(shù)據(jù)寫入磁盤忘分,同時(shí)另一個(gè)在填充新的音頻數(shù)據(jù),第三個(gè)緩沖區(qū)在需要做磁盤I/O延遲補(bǔ)償?shù)臅r(shí)候可用
如何使用AudioQueue:
- 創(chuàng)建輸入隊(duì)列AudioQueueNewInput
- 分配buffers
- 入隊(duì):AudioQueueEnqueueBuffer
- 回調(diào)函數(shù)采集音頻數(shù)據(jù)
- 出隊(duì)
AudioQueueNewInput
// 作用:創(chuàng)建一個(gè)音頻隊(duì)列為了錄制音頻數(shù)據(jù)
原型:extern OSStatus
AudioQueueNewInput( const AudioStreamBasicDescription *inFormat, 同上
AudioQueueInputCallback inCallbackProc, // 注冊(cè)回調(diào)函數(shù)
void * __nullable inUserData,
CFRunLoopRef __nullable inCallbackRunLoop,
CFStringRef __nullable inCallbackRunLoopMode,
UInt32 inFlags,
AudioQueueRef __nullable * __nonnull outAQ)白修;
// 這個(gè)函數(shù)的第四個(gè)和第五個(gè)參數(shù)是有關(guān)于線程的妒峦,我設(shè)置成null,代表它默認(rèn)使用內(nèi)部線程去錄音兵睛,而且還是異步的
(2).設(shè)置采集數(shù)據(jù)的格式肯骇,采集PCM必須按照如下設(shè)置,參考蘋果官方文檔祖很,不同需求自己另行修改
-(void)setUpRecoderWithFormatID:(UInt32)formatID {
// Notice : The settings here are official recommended settings,can be changed according to specific requirements. 此處的設(shè)置為官方推薦設(shè)置,可根據(jù)具體需求修改部分設(shè)置
//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; // 設(shè)置采樣率
size = sizeof(dataFormat.mChannelsPerFrame);
AudioSessionGetProperty(kAudioSessionProperty_CurrentHardwareInputNumberChannels,
&size,
&dataFormat.mChannelsPerFrame);
dataFormat.mFormatID = formatID;
// 關(guān)于采集PCM數(shù)據(jù)是根據(jù)蘋果官方文檔給出的Demo設(shè)置笛丙,至于為什么這么設(shè)置可能與采集回調(diào)函數(shù)內(nèi)部實(shí)現(xiàn)有關(guān),修改的話請(qǐng)謹(jǐn)慎
if (formatID == kAudioFormatLinearPCM)
{
/*
為保存音頻數(shù)據(jù)的方式的說明突琳,如可以根據(jù)大端字節(jié)序或小端字節(jié)序若债,
浮點(diǎn)數(shù)或整數(shù)以及不同體位去保存數(shù)據(jù)
例如對(duì)PCM格式通常我們?nèi)缦略O(shè)置:kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked等
*/
dataFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
// 每個(gè)通道里符相,一幀采集的bit數(shù)目
dataFormat.mBitsPerChannel = 16;
// 8bit為1byte拆融,即為1個(gè)通道里1幀需要采集2byte數(shù)據(jù),再*通道數(shù)啊终,即為所有通道采集的byte數(shù)目
dataFormat.mBytesPerPacket = dataFormat.mBytesPerFrame = (dataFormat.mBitsPerChannel / 8) * dataFormat.mChannelsPerFrame;
// 每個(gè)包中的幀數(shù)镜豹,采集PCM數(shù)據(jù)需要將dataFormat.mFramesPerPacket設(shè)置為1,否則回調(diào)不成功
dataFormat.mFramesPerPacket = kXDXRecoderPCMFramesPerPacket;
}
}
(3).將PCM轉(zhuǎn)成AAC一些基本設(shè)置
-(NSString *)convertBasicSetting {
// 此處目標(biāo)格式其他參數(shù)均為默認(rèn)蓝牲,系統(tǒng)會(huì)自動(dòng)計(jì)算趟脂,否則無法進(jìn)入encodeConverterComplexInputDataProc回調(diào)
AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
AudioStreamBasicDescription targetDes; // 轉(zhuǎn)碼后格式
// 設(shè)置目標(biāo)格式及基本信息
memset(&targetDes, 0, sizeof(targetDes));
targetDes.mFormatID = kAudioFormatMPEG4AAC;
targetDes.mSampleRate = kXDXAudioSampleRate;
targetDes.mChannelsPerFrame = dataFormat.mChannelsPerFrame;
targetDes.mFramesPerPacket = kXDXRecoderAACFramesPerPacket; // 采集的為AAC需要將targetDes.mFramesPerPacket設(shè)置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個(gè)樣點(diǎn)才開始編碼例衍,這與回調(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);
// 計(jì)算編碼器容量
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ù)組,設(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) {
// 新建一個(gè)編碼對(duì)象佛玄,設(shè)置原硼一,目標(biāo)格式
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);
// 獲取目標(biāo)格式大小
targetSize = sizeof(targetDes);
status = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &targetSize, &targetDes);;
// log4cplus_info("pcm","get targetDes status:%d",(int)status);
// 設(shè)置碼率,需要和采樣率對(duì)應(yīng)
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);
作用:檢索某個(gè)屬性的值
AudioClassDescription:
指的是一個(gè)能夠?qū)σ粋€(gè)信號(hào)或者一個(gè)數(shù)據(jù)流進(jìn)行變換的設(shè)備或者程序梦抢。這里指的變換既包括將 信號(hào)或者數(shù)據(jù)流進(jìn)行編碼(通常是為了傳輸般贼、存儲(chǔ)或者加密)或者提取得到一個(gè)編碼流的操作,也包括為了觀察或者處理從這個(gè)編碼流中恢復(fù)適合觀察或操作的形式的操作奥吩。編解碼器經(jīng)常用在視頻會(huì)議和流媒體等應(yīng)用中哼蛆。
默認(rèn)情況下,Apple會(huì)創(chuàng)建一個(gè)硬件編碼器霞赫,如果硬件不可用腮介,會(huì)創(chuàng)建軟件編碼器。
經(jīng)過我的測(cè)試端衰,硬件AAC編碼器的編碼時(shí)延很高萤厅,需要buffer大約2秒的數(shù)據(jù)才會(huì)開始編碼橄抹。而軟件編碼器的編碼時(shí)延就是正常的,只要喂給1024個(gè)樣點(diǎn)惕味,就會(huì)開始編碼楼誓。
AudioConverterNewSpecific:
原型: extern OSStatus
AudioConverterNewSpecific( const AudioStreamBasicDescription * inSourceFormat,
const AudioStreamBasicDescription * inDestinationFormat,
UInt32 inNumberClassDescriptions,
const AudioClassDescription * inClassDescriptions,
AudioConverterRef __nullable * __nonnull outAudioConverter);
解釋:創(chuàng)建一個(gè)轉(zhuǎn)換器
作用:設(shè)置一些轉(zhuǎn)碼基本信息
AudioConverterSetProperty:
原型:extern OSStatus
AudioConverterSetProperty( AudioConverterRef inAudioConverter,
AudioConverterPropertyID inPropertyID,
UInt32 inPropertyDataSize,
const void * inPropertyData)名挥;
作用:設(shè)置碼率疟羹,需要注意,AAC并不是隨便的碼率都可以支持禀倔。比如如果PCM采樣率是44100KHz榄融,那么碼率可以設(shè)置64000bps,如果是16K救湖,可以設(shè)置為32000bps愧杯。
(4).設(shè)置最終音頻文件的頭部信息(此類寫法為將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中注冊(cè)的回調(diào)函數(shù)
// AudioQueue中注冊(cè)的回調(diào)函數(shù)
static void inputBufferHandler(void * inUserData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer,
const AudioTimeStamp * inStartTime,
UInt32 inNumPackets,
const AudioStreamPacketDescription* inPacketDesc) {
// 相當(dāng)于本類對(duì)象實(shí)例
TVURecorder *recoder = (TVURecorder *)inUserData;
/*
inNumPackets 總包數(shù):音頻隊(duì)列緩沖區(qū)大小 (在先前估算緩存區(qū)大小為kXDXRecoderAACFramesPerPacket*2)/ (dataFormat.mFramesPerPacket (采集數(shù)據(jù)每個(gè)包中有多少幀邑闺,此處在初始化設(shè)置中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個(gè)字節(jié)跌前,此處在初始化設(shè)置中為每一幀中兩個(gè)字節(jié))),所以可以根據(jù)該公式計(jì)算捕捉PCM數(shù)據(jù)時(shí)inNumPackets陡舅。
注意:如果采集的數(shù)據(jù)是PCM需要將dataFormat.mFramesPerPacket設(shè)置為1抵乓,而本例中最終要的數(shù)據(jù)為AAC,因?yàn)楸纠惺褂玫霓D(zhuǎn)換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設(shè)置為1024.也就是采集到的inNumPackets為1,在轉(zhuǎn)換器中傳入的inNumPackets應(yīng)該為AAC格式下默認(rèn)的1靶衍,在此后寫入文件中也應(yīng)該傳的是轉(zhuǎn)換好的inNumPackets,如果有特殊需求需要將采集的數(shù)據(jù)量小于1024,那么需要將每次捕捉到的數(shù)據(jù)先預(yù)先存儲(chǔ)在一個(gè)buffer中,等到攢夠1024幀再進(jìn)行轉(zhuǎn)換灾炭。
*/
// collect pcm data,可以在此存儲(chǔ)
// 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
// 出隊(duì)
AudioQueueRef queue = recoder.mQueue;
if (recoder.isRunning) {
AudioQueueEnqueueBuffer(queue, inBuffer, 0, NULL);
}
}
}
解析回調(diào)函數(shù):相當(dāng)于中斷服務(wù)函數(shù)颅眶,每次錄取到音頻數(shù)據(jù)就進(jìn)入這個(gè)函數(shù)
注意:inNumPackets 總包數(shù):音頻隊(duì)列緩沖區(qū)大小 (在先前估算緩存區(qū)大小為2048)/ (dataFormat.mFramesPerPacket (采集數(shù)據(jù)每個(gè)包中有多少幀蜈出,此處在初始化設(shè)置中為1) * dataFormat.mBytesPerFrame(每一幀中有多少個(gè)字節(jié),此處在初始化設(shè)置中為每一幀中兩個(gè)字節(jié)))
- inAQ 是調(diào)用回調(diào)函數(shù)的音頻隊(duì)列
- inBuffer 是一個(gè)被音頻隊(duì)列填充新的音頻數(shù)據(jù)的音頻隊(duì)列緩沖區(qū)帚呼,它包含了回調(diào)函數(shù)寫入文件所需要的新數(shù)據(jù)
- inStartTime 是緩沖區(qū)中的一采樣的參考時(shí)間掏缎,對(duì)于基本的錄制,你的毀掉函數(shù)不會(huì)使用這個(gè)參數(shù)
- inNumPackets是inPacketDescs參數(shù)中包描述符(packet descriptions)的數(shù)量煤杀,如果你正在錄制一個(gè)VBR(可變比特率(variable bitrate))格式, 音頻隊(duì)列將會(huì)提供這個(gè)參數(shù)給你的回調(diào)函數(shù)眷蜈,這個(gè)參數(shù)可以讓你傳遞給AudioFileWritePackets函數(shù). CBR (常量比特率(constant bitrate)) 格式不使用包描述符。對(duì)于CBR錄制沈自,音頻隊(duì)列會(huì)設(shè)置這個(gè)參數(shù)并且將inPacketDescs這個(gè)參數(shù)設(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);
// 初始化一個(gè)bufferList存儲(chǔ)數(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設(shè)置為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)換完成時(shí)表示多少個(gè)包被寫入
*/
UInt32 inNumPackets = 1;
status = AudioConverterFillComplexBuffer(_encodeConvertRef,
encodeConverterComplexInputDataProc, // 填充數(shù)據(jù)的回調(diào)函數(shù)
pcm_buffer, // 音頻隊(duì)列緩沖區(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編碼后各個(gè)包的描述,但這里每次只轉(zhuǎn)換一包數(shù)據(jù)(由傳入的packetSize決定)籍滴。調(diào)用AudioConverterFillComplexBuffer觸發(fā)轉(zhuǎn)碼,他的第二個(gè)參數(shù)是填充原始音頻數(shù)據(jù)的回調(diào)榴啸。轉(zhuǎn)碼完成后孽惰,會(huì)將轉(zhuǎn)碼的數(shù)據(jù)存放在它的第五個(gè)參數(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)換,實(shí)時(shí)輸入輸出用于錄制库说,回放狂鞋,離線渲染和實(shí)時(shí)回話(VOIP),這讓我們可以動(dòng)態(tài)加載和使用潜的,即從iOS應(yīng)用程序中接收這些強(qiáng)大而靈活的插件骚揍。它是iOS音頻中最低層,所以除非你需要合成聲音的實(shí)時(shí)播放啰挪,低延遲的I/O信不,或特定聲音的特定特點(diǎn)。
Audio unit scopes and elements :
-
上圖是一個(gè)AudioUnit的組成結(jié)構(gòu)脐供,A scope 主要使用到的輸入kAudioUnitScope_Input和輸出kAudioUnitScope_Output浑塞。Element 是嵌套在audio unit scope的編程上下文借跪。
IOUnit.png
- AudioUnit 的Remote IO有2個(gè)element政己,大部分代碼和文獻(xiàn)都用bus代替element,兩者同義掏愁,bus0就是輸出,bus 1代表輸入歇由,播放音頻文件就是在bus 0傳送數(shù)據(jù),bus 1輸入在Remote IO 默認(rèn)是關(guān)閉的果港,在錄音的狀態(tài)下 需要把bus 1設(shè)置成開啟狀態(tài)沦泌。
- 我們能使用(kAudioOutputUnitProperty_EnableIO)屬性獨(dú)立地開啟或禁用每個(gè)element,Element 1 直接與音頻輸入硬件相連(麥克風(fēng))辛掠,Element 1 的input scope對(duì)我們是不透明的谢谦,來自輸入硬件的音頻數(shù)據(jù)只能在Element 1的output scope中訪問。
- 同樣的element 0直接和輸出硬件相連(揚(yáng)聲器)萝衩,我們可以將audio數(shù)據(jù)傳輸?shù)絜lement 0的input scope中回挽,但是output scope對(duì)我們是不透明的。
- 注意:每個(gè)element本身都有一個(gè)輸入范圍和輸出范圍猩谊,因此在代碼中如果不理解可能會(huì)比較懵逼千劈,比如你從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 是最常用的,它連接音頻硬件的輸入和輸出并且提供單個(gè)傳入和傳出音頻樣本值得低延遲訪問喜滨。還支持硬件音頻格式和應(yīng)用程序音頻格式的轉(zhuǎn)換捉捅,通過包含F(xiàn)ormat Converter unit來實(shí)現(xiàn)。
- The Voice-Processing I/O unit 繼承了the Remote I/O unit 并且增加回聲消除用于VOIP或語音聊天應(yīng)用虽风。它還提供了自動(dòng)增益校正锯梁,語音處理的質(zhì)量調(diào)整和靜音的功能。(本例中用此完成回聲消除)
- The Generic Output unit 不連接音頻硬件焰情,而是一共一種將處理鏈的輸出發(fā)送到應(yīng)用程序的機(jī)制陌凳。通常用來進(jìn)行脫機(jī)音頻處理。
3. 使用步驟:
1). 導(dǎo)入所需動(dòng)態(tài)庫與頭文件(At runtime, obtain a reference to the dynamically-linkable library that defines an audio unit you want to use.)
2). 實(shí)例化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;
// 如果你的應(yīng)用程序需要去除回聲將componentSubType設(shè)置為kAudioUnitSubType_VoiceProcessingIO内舟,否則根據(jù)需求設(shè)置為其他合敦,在博客中有介紹
audioDesc.componentSubType = kAudioUnitSubType_VoiceProcessingIO;//kAudioUnitSubType_VoiceProcessingIO;
// 蘋果自己的標(biāo)志
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
// 新建一個(gè)AudioComponent對(duì)象,只有這步完成才能進(jìn)行后續(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.
- 要在運(yùn)行時(shí)找到AudioUnit充岛,首先要在AudioComponentDescription中指定它的類型,子類型和制作商,AudioComponentFindNext參數(shù)inComponent一般設(shè)置為NULL耕蝉,從系統(tǒng)中找到第一個(gè)符合inDesc描述的Component崔梗,如果為其賦值,則從其之后進(jìn)行尋找垒在。AudioUnit實(shí)際上就是一個(gè)AudioComponentInstance實(shí)例對(duì)象
- componentSubType一般可設(shè)置為kAudioUnitSubType_RemoteIO蒜魄,如果有特別需求,如本例中要去除回聲场躯,則使用kAudioUnitSubType_VoiceProcessingIO谈为,每種類型作用在2-1中均有描述,不再重復(fù)踢关。
- (void)initBuffer {
// 禁用AudioUnit默認(rèn)的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默認(rèn)的buffer而使用我們自己寫的全局BUFFER,用來接收每次采集的PCM數(shù)據(jù),Disable AU buffer allocation for the recorder, we allocate our own.還有一種寫法是可以使用回調(diào)中提供的ioData存儲(chǔ)采集的數(shù)據(jù)签舞,這里使用全局的buff是為了供其他地方使用秕脓,可根據(jù)需要自行決定采用哪種方式,若不采用全局buffer則不可采用上述禁用操作儒搭。
// 因?yàn)楸纠蛔鲣浺艄δ芊图埽磳?shí)現(xiàn)播放功能,所以沒有設(shè)置播放相關(guān)設(shè)置师妙。
- (void)setAudioUnitPropertyAndFormat {
OSStatus status;
[self setUpRecoderWithFormatID:kAudioFormatLinearPCM];
// 應(yīng)用audioUnit設(shè)置的格式
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輸入端默認(rèn)是關(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. 此處的設(shè)置為官方推薦設(shè)置,可根據(jù)具體需求修改部分設(shè)置
//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需要這么設(shè)置
}
}
解析
上述操作針對(duì)錄音功能需要對(duì)Audio Unit做出對(duì)應(yīng)設(shè)置,首先設(shè)置ASBD采集數(shù)據(jù)為PCM的格式,需要注意的是如果是使用AudioQueue與AudioUnit的dataFormat.mFormatFlags設(shè)置略有不同怔檩,經(jīng)測(cè)試必須這樣設(shè)置褪秀,原因暫不詳,設(shè)置完后使用AudioUnitSetProperty應(yīng)用設(shè)置薛训,這里只做錄音媒吗,所以對(duì)kAudioOutputUnitProperty_EnableIO 的 kAudioUnitScope_Input 開啟,而對(duì)kAudioUnitScope_Output 輸入端輸出的音頻格式進(jìn)行設(shè)置乙埃,如果不理解可參照1中概念解析進(jìn)行理解闸英,kAUVoiceIOProperty_BypassVoiceProcessing則是回聲的開關(guān)。
-(NSString *)convertBasicSetting {
// 此處目標(biāo)格式其他參數(shù)均為默認(rèn)介袜,系統(tǒng)會(huì)自動(dòng)計(jì)算甫何,否則無法進(jìn)入encodeConverterComplexInputDataProc回調(diào)
AudioStreamBasicDescription sourceDes = dataFormat; // 原始格式
AudioStreamBasicDescription targetDes; // 轉(zhuǎn)碼后格式
// 設(shè)置目標(biāo)格式及基本信息
memset(&targetDes, 0, sizeof(targetDes));
targetDes.mFormatID = kAudioFormatMPEG4AAC;
targetDes.mSampleRate = kXDXAudioSampleRate;
targetDes.mChannelsPerFrame = dataFormat.mChannelsPerFrame;
targetDes.mFramesPerPacket = kXDXRecoderAACFramesPerPacket; // 采集的為AAC需要將targetDes.mFramesPerPacket設(shè)置為1024,AAC軟編碼需要喂給轉(zhuǎn)換器1024個(gè)樣點(diǎn)才開始編碼遇伞,這與回調(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);
// 計(jì)算編碼器容量
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ù)組,設(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) {
// 新建一個(gè)編碼對(duì)象鸠珠,設(shè)置原巍耗,目標(biāo)格式
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);
// 獲取目標(biāo)格式大小
targetSize = sizeof(targetDes);
status = AudioConverterGetProperty(_encodeConvertRef, kAudioConverterCurrentOutputStreamDescription, &targetSize, &targetDes);;
// log4cplus_info("pcm","get targetDes status:%d",(int)status);
// 設(shè)置碼率,需要和采樣率對(duì)應(yīng)
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;
}
解析
設(shè)置原格式與轉(zhuǎn)碼格式并創(chuàng)建_encodeConvertRef轉(zhuǎn)碼器對(duì)象完成相關(guān)初始化操作渐排,值得注意的是targetDes.mFramesPerPacket設(shè)置為1024炬太,AAC軟編碼需要喂給轉(zhuǎn)換器1024個(gè)樣點(diǎn)才開始編碼,不可隨意更改驯耻,原因如下圖,由AAC編碼器決定亲族。
- (void)initRecordeCallback {
// 設(shè)置回調(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);
}
}
解析
以上為設(shè)置采集回調(diào)杏慰,有兩種方式测柠,1種為使用我們自己的buffer,這樣需要先在上述initBuffer中禁用系統(tǒng)的buffer缘滥,則回調(diào)函數(shù)中每次渲染的為我們自己的buffer轰胁,另一種則是使用系統(tǒng)的buffer,對(duì)應(yīng)需要在回調(diào)函數(shù)中將ioData放進(jìn)渲染的函數(shù)中朝扼。
static OSStatus RecordCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
/*
注意:如果采集的數(shù)據(jù)是PCM需要將dataFormat.mFramesPerPacket設(shè)置為1赃阀,而本例中最終要的數(shù)據(jù)為AAC,因?yàn)楸纠惺褂玫霓D(zhuǎn)換器只有每次傳入1024幀才能開始工作,所以在AAC格式下需要將mFramesPerPacket設(shè)置為1024.也就是采集到的inNumPackets為1,在轉(zhuǎn)換器中傳入的inNumPackets應(yīng)該為AAC格式下默認(rèn)的1擎颖,在此后寫入文件中也應(yīng)該傳的是轉(zhuǎn)換好的inNumPackets,如果有特殊需求需要將采集的數(shù)據(jù)量小于1024,那么需要將每次捕捉到的數(shù)據(jù)先預(yù)先存儲(chǔ)在一個(gè)buffer中,等到攢夠1024幀再進(jìn)行轉(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個(gè)采樣點(diǎn)(每一幀2個(gè)字節(jié))才能完成一次轉(zhuǎn)換观游,所以每次需要2048大小的數(shù)據(jù),這里定義的pcm_buffer用來累加每次存儲(chǔ)的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);
// 因?yàn)椴蓸硬豢赡苊看味季珳?zhǔn)的采集到1024個(gè)樣點(diǎn)驮俗,所以如果大于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按照上述設(shè)置并將其放入AudioUnitRender函數(shù)中進(jìn)行渲染搪柑,回調(diào)函數(shù)中采用pcm_buffer存儲(chǔ)滿2048個(gè)字節(jié)的數(shù)組傳給轉(zhuǎn)換器,這是編碼器的特性索烹,所以如果采集的數(shù)據(jù)小于2048先取pcm_buffer的前2048個(gè)字節(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對(duì)象的創(chuàng)建和初始化仑嗅,否則會(huì)發(fā)生異常宴倍。