Core Audio
Core Audio是iOS和OS X中處理音頻的框架集合,具有高性能,低延遲的優(yōu)點(diǎn)澄阳。Core Audio在iOS中的框架有:Audio Toolbox,Audio Unit,AV Foundation窑多,OpenAL
錄音方案
AVFoundation:提供AVAudioPlayer,AVAudioRecorder類洼滚,以及簡(jiǎn)單的OC接口埂息,錄音過(guò)程是把音頻錄制成音頻文件,播放過(guò)程是播放音頻文件,適合處理非實(shí)時(shí)的場(chǎng)景千康。
Audio Unit:Audio Unit在音頻開(kāi)發(fā)中處于最底層享幽,可以實(shí)時(shí)獲取和播放PCM數(shù)據(jù),具有響應(yīng)快拾弃,低延遲的優(yōu)點(diǎn)值桩,適用于低延遲實(shí)時(shí)場(chǎng)景。
Audio ToolBox:基于Audio Unit豪椿,提供Core Audio中層和高層服務(wù)的接口奔坟,包括Audio Session Services,AudioQueueService(音頻隊(duì)列)搭盾。音頻隊(duì)列是另一種錄音方案咳秉,將錄制的音頻放置在隊(duì)列中,取出播放鸯隅。
OpenAL:基于Audio Unit澜建,主要提供跨平臺(tái)的接口。
可以看到蝌以,實(shí)時(shí)錄音方案有兩種炕舵,本文主要講述這兩種方式的特點(diǎn)。
Audio Queue
關(guān)于Audio Queue的知識(shí)饼灿,網(wǎng)上有很多比較好的總結(jié)幕侠,如果英文閱讀無(wú)障礙,可以閱讀官方文檔的詳細(xì)說(shuō)明Audio Queue Services Programming Guide碍彭。其錄制和播放示意圖如下:
大致原理就是晤硕,使用緩存隊(duì)列來(lái)達(dá)到實(shí)時(shí)錄音和播放的效果,以錄音為例庇忌,麥克風(fēng)采集的PCM數(shù)據(jù)首先填充到隊(duì)首的緩存中舞箍,緩存充滿時(shí)就會(huì)出隊(duì),觸發(fā)回調(diào)函數(shù)皆疹,可以在回調(diào)的時(shí)候做修音處理疏橄,寫入文件,播放等操作略就,然后就清空改緩存捎迫,并將該緩存加入到隊(duì)尾,等待填充表牢,此過(guò)程一直循環(huán)窄绒,播放的過(guò)程同理。
注意:我們可以通過(guò)設(shè)置緩存的大小崔兴,來(lái)控制回調(diào)的時(shí)間彰导,從而實(shí)時(shí)處理音頻蛔翅。其計(jì)算如下:
回調(diào)時(shí)間 ≈ 采樣率 * 采樣位數(shù) / 緩存大小(注意是近似值位谋!)
Audio Queue的錄音方案使用比較簡(jiǎn)單山析,能夠?qū)崟r(shí)處理音頻,但是也有其局限性掏父,它的實(shí)時(shí)性不夠準(zhǔn)確笋轨,有一定的延遲,即回調(diào)函數(shù)的時(shí)間不穩(wěn)定赊淑。當(dāng)采樣率為44100翩腐,位數(shù)為16,緩存大小為8820膏燃,根據(jù)公式回調(diào)時(shí)間約等于100ms,準(zhǔn)確值為92.9ms(稍后解釋)時(shí)何什,回調(diào)時(shí)間如下:
可以看到回調(diào)間隔多數(shù)是93ms组哩,也有一些波動(dòng),第三次到第四次是105ms处渣,而且回調(diào)間隔越小伶贰,波動(dòng)就越大,比如將緩存大小設(shè)置為4410罐栈,回調(diào)時(shí)間如下:
這個(gè)時(shí)候波動(dòng)已經(jīng)很明顯了黍衙,第二次到第三次甚至出現(xiàn)了7ms的情況。在實(shí)時(shí)場(chǎng)景中荠诬,每次調(diào)用表示一幀琅翻,在幀大小要求精細(xì)的時(shí)候,這樣的誤差是難以接受的柑贞,需要更穩(wěn)定的錄音方式方椎。
思考:為什么會(huì)出現(xiàn)波動(dòng)的情況?解決方法钧嘶?
這種波動(dòng)的原因是在Audio Queue的底層產(chǎn)生的棠众,之前說(shuō)過(guò),Audio ToolBox是基于Audio Unit的有决,回調(diào)函數(shù)的波動(dòng)要到底層才能解決闸拿。
[圖片上傳失敗...(image-7780d3-1522826744091)]
可以猜想一下,底層可能有并發(fā)的線程书幕,并發(fā)使得回調(diào)函數(shù)時(shí)間出現(xiàn)隨機(jī)性新荤,就會(huì)產(chǎn)生波動(dòng),甚至出現(xiàn)例子中7ms調(diào)用兩次的情況按咒。關(guān)于這一點(diǎn)迟隅,可以參考stackoverflow的討論AudioQueueNewInput callback latency中的回答:
The Audio Queue API looks like it is built on top of the Audio Unit RemoteIO API. Small Audio Queue buffers are probably being used to fill a larger RemoteIO buffer behind the scenes. Perhaps even some rate resampling might be taking place (on the original 2G phone).
For lower latency, try using the RemoteIO Audio Unit API directly, and then requesting the audio session to provide your app a smaller lower latency buffer size.
可以看到但骨,使用低延遲的錄音方式,需要使用更底層的Audio Unit智袭。
Audio Unit
關(guān)于Audio Unit的介紹奔缠,官方文檔Audio Unit Hosting Guide for iOS解釋的很詳細(xì),Audio Unit通常工作在一個(gè)封閉的上下文中吼野,稱之為audio processing graph校哎,如下:
麥克風(fēng)采集到的音頻輸送到audio processing graph中,音頻數(shù)據(jù)經(jīng)過(guò)兩路EQ unit(均衡)瞳步,然后Mixer unit(混合)闷哆,最終到與輸出設(shè)備直接相連的I/O unit。這個(gè)過(guò)程可以看到单起,Audio Unit是對(duì)音頻的直接處理抱怔,甚至可以將unit輸出到外設(shè),相比于音頻隊(duì)列的配置嘀倒,Audio Unit要更復(fù)雜屈留,下面詳細(xì)介紹使用Audio Unit實(shí)現(xiàn)實(shí)時(shí)錄音的例子。
Audio Unit的構(gòu)建方式分為兩種测蘑,一種是直接使用Unit API灌危,一種是使用Audio Unit Graph,下面采用第一種方式碳胳。
AudioUnit audioUnit;
關(guān)于AudioUnit的解釋:
The type used to represent an instance of a particular audio component
表示的結(jié)構(gòu)如下:
[圖片上傳失敗...(image-ee41ac-1522826744092)]
接下來(lái)就要構(gòu)建Unit的結(jié)構(gòu)勇蝙,在不同的音頻應(yīng)用中,可以構(gòu)建各種不同的結(jié)構(gòu)挨约,一個(gè)簡(jiǎn)單的結(jié)構(gòu)如下:
[圖片上傳失敗...(image-dc3dac-1522826744092)]
確定了結(jié)構(gòu)味混,開(kāi)始配置工作了。
配置AudioSession
和其他錄音播放一樣烫罩,需要配置錄音播放的環(huán)境惜傲,響應(yīng)耳機(jī)事件等。
NSError *error;
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setCategory:AVAudioSessionCategoryPlayAndRecord error:&error];
[audioSession setPreferredSampleRate:44100 error:&error];
[audioSession setPreferredInputNumberOfChannels:1 error:&error];
[audioSession setPreferredIOBufferDuration:0.05 error:&error];
配置AudioComponentDescription
AudioComponentDescription是用來(lái)描述unit 的類型贝攒,包括均衡器盗誊,3D混音,多路混音隘弊,遠(yuǎn)端輸入輸出哈踱,VoIP輸入輸出,通用輸出梨熙,格式轉(zhuǎn)換等开镣,在這里使用遠(yuǎn)端輸入輸出。
AudioComponentDescription audioDesc;
audioDesc.componentType = kAudioUnitType_Output;
audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;
AudioComponent inputComponent = AudioComponentFindNext(NULL, &audioDesc);
AudioComponentInstanceNew(inputComponent, &audioUnit);
配置輸入輸出的數(shù)據(jù)格式
設(shè)置采樣率為44100咽扇,單聲道邪财,16位的格式陕壹,注意輸入輸出都要設(shè)置。
AudioStreamBasicDescription audioFormat;
audioFormat.mSampleRate = 44100;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioFormat.mFramesPerPacket = 1;
audioFormat.mChannelsPerFrame = 1;
audioFormat.mBitsPerChannel = 16;
audioFormat.mBytesPerPacket = 2;
audioFormat.mBytesPerFrame = 2;
AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
INPUT_BUS,
&audioFormat,
sizeof(audioFormat));
AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
OUTPUT_BUS,
&audioFormat,
sizeof(audioFormat));
打開(kāi)輸入輸出端口
在默認(rèn)情況下树埠,輸入是關(guān)閉的糠馆,輸出是打開(kāi)的。在unit的Element中怎憋,Input用“1”(和I很像)表示又碌,Output用“0”(和O很像)表示。
UInt32 flag = 1;
AudioUnitSetProperty(audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input,
INPUT_BUS,
&flag,
sizeof(flag));
AudioUnitSetProperty(audioUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input,
OUTPUT_BUS,
&flag,
sizeof(flag));
配置回調(diào)
根據(jù)應(yīng)用的場(chǎng)景需求绊袋,可以在輸入輸出設(shè)置回調(diào)毕匀,以輸入回調(diào)為例:
AURenderCallbackStruct recordCallback;
recordCallback.inputProc = RecordCallback;
recordCallback.inputProcRefCon = (__bridge void *)self;
AudioUnitSetProperty(audioUnit,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global,
INPUT_BUS,
&recordCallback,
sizeof(recordCallback));
需要定義回調(diào)函數(shù),回調(diào)函數(shù)是AURenderCallback類型的癌别,按照AUComponent.h中定義的參數(shù)類型皂岔,定義出輸入回調(diào)函數(shù):
static OSStatus RecordCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData)
{
AudioUnitRender(audioUnit, ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, buffList);
return noErr;
}
分配緩存
這是獲取錄音數(shù)據(jù)很重要的一步,需要分配緩存來(lái)存儲(chǔ)實(shí)時(shí)的錄音數(shù)據(jù)展姐。如果不這樣做凤薛,錄音數(shù)據(jù)也可以在輸出的時(shí)候獲取,但意義不一樣诞仓,獲取錄音數(shù)據(jù)應(yīng)該在輸入回調(diào)中完成,而不是輸出回調(diào)速兔。
UInt32 flag = 0;
AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output,
INPUT_BUS,
&flag,
sizeof(flag));
buffList = (AudioBufferList*)malloc(sizeof(AudioBufferList));
buffList->mNumberBuffers = 1;
buffList->mBuffers[0].mNumberChannels = 1;
buffList->mBuffers[0].mDataByteSize = 2048 * sizeof(short);
buffList->mBuffers[0].mData = (short *)malloc(sizeof(short) * 2048);
通過(guò)以上設(shè)置墅拭,可以實(shí)時(shí)錄音,并實(shí)時(shí)播放(本例中涣狗,輸入輸出都打開(kāi)了)谍婉。
幾個(gè)問(wèn)題
- 在真機(jī)上運(yùn)行的時(shí)候,會(huì)報(bào)錯(cuò)镀钓,錯(cuò)誤信息如下:
這是因?yàn)闆](méi)有開(kāi)啟錄音權(quán)限穗熬,以source code的方式打開(kāi)Info.plist文件,在dict標(biāo)簽中加入以下屬性:
<key>NSMicrophoneUsageDescription</key>
<string>microphoneDesciption</string>
再次運(yùn)行丁溅,就OK了唤蔗。
2.回調(diào)時(shí)間間隔問(wèn)題。
Audio Unit的延遲很低窟赏,回調(diào)時(shí)間非常穩(wěn)定妓柜,很適合嚴(yán)格地實(shí)時(shí)處理音頻,即使把時(shí)間設(shè)置成0.000725623582766秒涯穷,回調(diào)時(shí)間依然很準(zhǔn):
事實(shí)上棍掐,Audio Unit沒(méi)有回調(diào)間隔的配置,但是我們可以通過(guò)上下文環(huán)境配置拷况,即:
[audioSession setPreferredIOBufferDuration:0.05 error:&error];
這樣設(shè)置duration為0.05秒作煌,表示每隔0.05秒就去讀取緩存數(shù)據(jù)掘殴。假設(shè)采樣率為44100,采樣位數(shù)16粟誓,這時(shí)buffer大小應(yīng)該為44100 * 0.05 * 16 / 8 = 4410奏寨,但是,Audio Unit 的buffer的大小是2的冪次方努酸,那么就不可能有4410服爷,這時(shí)buffer實(shí)際大小為4096,反過(guò)來(lái)計(jì)算時(shí)間就是0.0464秒获诈,這也就解釋了在Audio Queue中近似計(jì)算回調(diào)時(shí)間的原因了仍源。
除此之外,如果不用AudioSession設(shè)置時(shí)間的話舔涎,會(huì)有一個(gè)默認(rèn)大小的buffer笼踩,這個(gè)大小在模擬器和真機(jī)上不相同,所以為了程序可控亡嫌,這個(gè)設(shè)置很有必要嚎于。
3.關(guān)于播放問(wèn)題
測(cè)試發(fā)現(xiàn),用耳機(jī)的效果更好挟冠,不用耳機(jī)在播放的時(shí)候會(huì)有噪聲于购。如果想獲得清晰的效果,可以將每次的PCM數(shù)據(jù)寫入到文件知染,然后回放肋僧。推薦使用Lame,這個(gè)可以將PCM轉(zhuǎn)換成MP3控淡。
4.讀取PCM數(shù)據(jù)
PCM數(shù)據(jù)存放在AudioBuffer的結(jié)構(gòu)體中嫌吠,音頻數(shù)據(jù)是void *類型的數(shù)據(jù):
/*!
@struct AudioBuffer
@abstract A structure to hold a buffer of audio data.
@field mNumberChannels
The number of interleaved channels in the buffer.
@field mDataByteSize
The number of bytes in the buffer pointed at by mData.
@field mData
A pointer to the buffer of audio data.
*/
struct AudioBuffer
{
UInt32 mNumberChannels;
UInt32 mDataByteSize;
void* __nullable mData;
};
typedef struct AudioBuffer AudioBuffer;
如果采樣位數(shù)是16位,即2Byte掺炭,即mData中每2Byte是一個(gè)PCM數(shù)據(jù)辫诅,以獲取第一個(gè)數(shù)據(jù)為例:
short *data = (short *)buffList->mBuffers[0].mData;
NSLog(@"%d", data[0]);
這里需要注意的就是類型轉(zhuǎn)換的時(shí)候位數(shù)要一致。