最近一直在做iOS音頻相關技術的項目,期間在官方及網上的資料文檔也學習了很多,當然,iOS平台中音頻相關技術還是有很多方面的,這裡我先總體概述下,然後以I/O Audio Unit為例對其概念,基本用法和思路進行講解,可能不夠全面,一些細節需要自行查找相關文檔。後面我會對github上一個開源的音頻引擎框架進行源碼分析,來展現在更複雜的音頻技術應用場景下可能的設計及實現方式。
本文圖片及大部分技術概念闡述均來自apple官網
1、Core Audio
Core Audio 是iOS和MAC系統中的關於數字音頻處理的基礎設施,它是應用程式用來處理音頻的一組軟體框架,所有關於iOS音頻開發的接口都是由Core Audio來提供或者經過它提供的接口來進行封裝的。Apple官方對Core Audio的框架分層圖示如下:
2、Low-Level
該主要在MAC上的音頻APP實現中並且需要最大限度的實時性能的情況下使用,大部分音頻APP不需要使用該層的服務。而且,在iOS上也提供了具備較高實時性能的高層API達到你的需求。例如OpenAL,在遊戲中具備與I/O直接調用的實時音頻處理能力 I/O Kit, 與硬體驅動交互 Audio HAL, 音頻硬體抽象層,使API調用與實際硬體相分離,保持獨立 Core MidI, 為MIDI流和設備提供軟體抽象工作層 Host Time Services, 訪問電腦硬體時鐘
3、Mid-Level
該層功能比較齊全,包括音頻數據格式轉換,音頻文件讀寫,音頻流解析,插件工作支持等 Audio Convert Services 負責音頻數據格式的轉換 Audio File Services 負責音頻數據的讀寫 Audio Unit Services 和 Audio Processing Graph Services 支持均衡器和混音器等數位訊號處理的插件 Audio File Scream Services 負責流解析 Core Audio Clock Services 負責音頻音頻時鐘同步
4、High-Level
是一組從低層接口組合起來的高層應用,基本上我們很多關於音頻開發的工作在這一層就可以完成 Audio Queue Services 提供錄製、播放、暫停、循環、和同步音頻它自動採用必要的編解碼器處理壓縮的音頻格式 AVAudioPlayer 是專為IOS平台提供的基於Objective-C接口的音頻播放類,可以支持iOS所支持的所有音頻的播放 Extended Audio File Services 由Audio File與Audio Converter組合而成,提供壓縮及無壓縮音頻文件的讀寫能力 OpenAL 是CoreAudio對OpenAL標準的實現,可以播放3D混音效果
5、不同場景所需要的API Service
只實現音頻的播放,沒有其他需求,AVAudioPlayer就可以滿足需求。它的接口使用簡單,不用關心其中的細節,通常只提供給它一個播放源的URL地址,並且調用其play、pause、stop等方法進行控制,observer其播放狀態更新UI即可
APP需要對音頻進行流播放,就需要AudioFileStreamer加Audio Queue,將網絡或者本地的流讀取到內存,提交給AudioFileStreamer解析分離音頻幀,分離出來的音頻幀可以送給AudioQueue進行解碼和播放 可參考 AudioStreamer FreeStreamer AFSoundManager
APP需要需要對音頻施加音效(均衡器、混響器),就是除了數據的讀取和解析以外還需要用到AudioConverter或者Codec來把音頻數據轉換成PCM數據,再由AudioUnit+AUGraph來進行音效處理和播放 可參考 DouAudioStreamer TheAmazingAudioEngine AudioKit
6、Audio Unit
iOS提供了混音、均衡、格式轉換、實時IO錄製、回放、離線渲染、語音對講(VoIP)等音頻處理插件,它們都屬於不同的AudioUnit,支持動態載入和使用。AudioUnit可以單獨創建使用,但更多的是被組合使用在Audio Processing Graph容器中以達到多樣的處理需要,例如下面的一種場景:
APP持有的Audio Processing Graph容器中包含兩個EQ Unit、一個Mixer Unit、一個I/O Unit,APP將磁碟或者網絡中的兩路流數據分別通過EQ Unit進行均衡處理,然後在Mixer Unit經過混音處理為一路,進入I/O Unit將此路數據送往硬體去播放。在這整個流程中,APP隨時可以調整設置AU Graph及其中每個Unit的工作狀態及參數,動態性的接入或者移出指定的Unit,並且保證線程安全。
C++音視頻學習資料免費獲取方法:關注音視頻開發T哥,點擊「連結」即可免費獲取2023年最新C++音視頻開發進階獨家免費學習大禮包!
6.1 Audio Unit類型:
I/O: Remote I/O、Voice-Processing I/O、Generic Output Mixing: 3D Mixer、Mutichannel Mixer Effect: iPod Equalizer Format Conversion: Format Converter
6.2 AudioUnit構建方式
創建Audio Unit有兩種途徑,以I/O Unit為例,一種是直接調用unit接口創建,一種是通過Audio Unit Graph創建,下面是兩種創建方式的基本流程和相關代碼:
6.3 Unit API方式(Remote IO Unit)
// create IO Unit
BOOL result = NO;
AudioComponentDescription outputDescription = {0};
outputDescription.componentType = kAudioUnitType_Output;
outputDescription.componentSubType = kAudioUnitSubType_RemoteIO;
outputDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
outputDescription.componentFlags = 0;
outputDescription.componentFlagsMask = 0;
AudioComponent comp = AudioComponentFindNext(NULL, &outputDescription);
result = CheckOSStatus(AudioComponentInstanceNew(comp, &mVoipUnit), @"couldn't create a new instance of RemoteIO");
if (!result) return result;
// config IO Enable status
UInt32 flag = 1;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, kOutputBus, &flag, sizeof(flag)), @"could not enable output on RemoteIO");
if (!result) return result;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, kInputBus, &flag, sizeof(flag)), @"AudioUnitSetProperty EnableIO");
if (!result) return result;
// Config default format
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, kInputBus, &inputAudioDescription, sizeof(inputAudioDescription)), @"couldn't set the input client format on RemoteIO");
if (!result) return result;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, kOutputBus, &outputAudioDescription, sizeof(outputAudioDescription)), @"couldn't set the output client format on RemoteIO");
if (!result) return result;
// Set the MaximumFramesPerSlice property. This property is used to describe to an audio unit the maximum number
// of samples it will be asked to produce on any single given call to AudioUnitRender
UInt32 maxFramesPerSlice = 4096;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, 0, &maxFramesPerSlice, sizeof(UInt32)), @"couldn't set max frames per slice on RemoteIO");
if (!result) return result;
// Set the record callback
AURenderCallbackStruct recordCallback;
recordCallback.inputProc = recordCallbackFunc;
recordCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioOutputUnitProperty_SetInputCallback, kAudioUnitScope_Global, kInputBus, &recordCallback, sizeof(recordCallback)), @"couldn't set record callback on RemoteIO");
if (!result) return result;
// Set the playback callback
AURenderCallbackStruct playbackCallback;
playbackCallback.inputProc = playbackCallbackFunc;
playbackCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Global, kOutputBus, &playbackCallback, sizeof(playbackCallback)), @"couldn't set playback callback on RemoteIO");
if (!result) return result;
// set buffer allocate
flag = 0;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output,
kInputBus,
&flag,
sizeof(flag)), @"couldn't set property for ShouldAllocateBuffer");
if (!result) return result;
// Initialize the output IO instance
result = CheckOSStatus(AudioUnitInitialize(mVoipUnit), @"couldn't initialize VoiceProcessingIO instance");
if (!result) return result;
return YES;
6.4 AU Graph方式(MultiChannelMixer Unit + Remote IO Unit)
// create AUGraph
BOOL result = NO;
result = CheckOSStatus(NewAUGraph (&processingGraph), @"couldn't create a new instance of AUGraph");
if (!result) return result;
// I/O unit
AudioComponentDescription iOUnitDescription;
iOUnitDescription.componentType = kAudioUnitType_Output;
iOUnitDescription.componentSubType = kAudioUnitSubType_RemoteIO;
iOUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
iOUnitDescription.componentFlags = 0;
iOUnitDescription.componentFlagsMask = 0;
// Multichannel mixer unit
AudioComponentDescription MixerUnitDescription;
MixerUnitDescription.componentType = kAudioUnitType_Mixer;
MixerUnitDescription.componentSubType = kAudioUnitSubType_MultiChannelMixer;
MixerUnitDescription.componentManufacturer = kAudioUnitManufacturer_Apple;
MixerUnitDescription.componentFlags = 0;
MixerUnitDescription.componentFlagsMask = 0;
AUNode iONode; // node for I/O unit
AUNode mixerNode; // node for Multichannel Mixer unit
result = CheckOSStatus(AUGraphAddNode (
processingGraph,
&iOUnitDescription,
&iONode), @"couldn't add a node instance of kAudioUnitSubType_RemoteIO");
if (!result) return result;
result = CheckOSStatus(AUGraphAddNode (
processingGraph,
&MixerUnitDescription,
&mixerNode), @"couldn't add a node instance of mixer unit");
if (!result) return result;
// open the AUGraph
result = CheckOSStatus(AUGraphOpen (processingGraph), @"couldn't get instance of mixer unit");
if (!result) return result;
// Obtain unit instance
result = CheckOSStatus(AUGraphNodeInfo (
processingGraph,
mixerNode,
NULL,
&mMixerUnit
), @"couldn't get instance of mixer unit");
if (!result) return result;
result = CheckOSStatus(AUGraphNodeInfo (
processingGraph,
iONode,
NULL,
&mVoipUnit
), @"couldn't get a new instance of remoteio unit");
if (!result) return result;
/////////////////////////////////////////////////////////////////////////////////////////
UInt32 busCount = 2; // bus count for mixer unit input
UInt32 guitarBus = 0; // mixer unit bus 0 will be stereo and will take the guitar sound
UInt32 beatsBus = 1; // mixer unit bus 1 will be mono and will take the beats sound
result = CheckOSStatus(AudioUnitSetProperty (
mMixerUnit,
kAudioUnitProperty_ElementCount,
kAudioUnitScope_Input,
0,
&busCount,
sizeof (busCount)
), @"could not set mixer unit input bus count");
if (!result) return result;
UInt32 maximumFramesPerSlice = 4096;
result = CheckOSStatus(AudioUnitSetProperty (
mMixerUnit,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global,
0,
&maximumFramesPerSlice,
sizeof (maximumFramesPerSlice)
), @"could not set mixer unit maximum frame per slice");
if (!result) return result;
// Attach the input render callback and context to each input bus
for (UInt16 busNumber = 0; busNumber < busCount; ++busNumber) {
// Setup the struture that contains the input render callback
AURenderCallbackStruct playbackCallback;
playbackCallback.inputProc = playbackCallbackFunc;
playbackCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
NSLog (@"Registering the render callback with mixer unit input bus %u", busNumber);
// Set a callback for the specified node's specified input
result = CheckOSStatus(AUGraphSetNodeInputCallback (
processingGraph,
mixerNode,
busNumber,
&playbackCallback
), @"couldn't set playback callback on mixer unit");
if (!result) return result;
}
// Config mixer unit input default format
result = CheckOSStatus(AudioUnitSetProperty (
mMixerUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
guitarBus,
&outputAudioDescription,
sizeof (outputAudioDescription)
), @"couldn't set the input 0 client format on mixer unit");
if (!result) return result;
result = CheckOSStatus(AudioUnitSetProperty (
mMixerUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
beatsBus,
&outputAudioDescription,
sizeof (outputAudioDescription)
), @"couldn't set the input 1 client format on mixer unit");
if (!result) return result;
Float64 graphSampleRate = 44100.0; // Hertz;
result = CheckOSStatus(AudioUnitSetProperty (
mMixerUnit,
kAudioUnitProperty_SampleRate,
kAudioUnitScope_Output,
0,
&graphSampleRate,
sizeof (graphSampleRate)
), @"couldn't set the output client format on mixer unit");
if (!result) return result;
////////////////////////////////////////////////////////////////////////////////////////////
// config void unit IO Enable status
UInt32 flag = 1;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Output,
kOutputBus,
&flag,
sizeof(flag)
), @"could not enable output on kAudioUnitSubType_RemoteIO");
if (!result) return result;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioOutputUnitProperty_EnableIO,
kAudioUnitScope_Input,
kInputBus,
&flag,
sizeof(flag)
), @"could not enable input on kAudioUnitSubType_RemoteIO");
if (!result) return result;
// config voip unit default format
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Output,
kInputBus,
&inputAudioDescription,
sizeof(inputAudioDescription)
), @"couldn't set the input client format on kAudioUnitSubType_RemoteIO");
if (!result) return result;
UInt32 maxFramesPerSlice = 4096;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioUnitProperty_MaximumFramesPerSlice,
kAudioUnitScope_Global,
0,
&maxFramesPerSlice,
sizeof(UInt32)
), @"couldn't set max frames per slice on kAudioUnitSubType_RemoteIO");
if (!result) return result;
// Set the record callback
AURenderCallbackStruct recordCallback;
recordCallback.inputProc = recordCallbackFunc;
recordCallback.inputProcRefCon = (__bridge void * _Nullable)(self);
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioOutputUnitProperty_SetInputCallback,
kAudioUnitScope_Global,
kInputBus,
&recordCallback,
sizeof(recordCallback)
), @"couldn't set record callback on kAudioUnitSubType_RemoteIO");
if (!result) return result;
// set buffer allocate
flag = 0;
result = CheckOSStatus(AudioUnitSetProperty(mVoipUnit,
kAudioUnitProperty_ShouldAllocateBuffer,
kAudioUnitScope_Output,
kInputBus,
&flag,
sizeof(flag)), @"couldn't set property for ShouldAllocateBuffer");
if (!result) return result;
/////////////////////////////////////////////////////////////////////////////////////////////
// Initialize the output IO instance
result = CheckOSStatus(AUGraphConnectNodeInput (
processingGraph,
mixerNode, // source node
0, // source node output bus number
iONode, // destination node
0 // desintation node input bus number
), @"couldn't connect ionode to mixernode");
if (!result) return result;
result = CheckOSStatus(AUGraphInitialize (processingGraph), @"AUGraphInitialize failed");
if (!result) return result;
return YES;
6.5 AudioUnit數據的輸入輸出方式
Unit處理音頻數據,都要經過一個輸入和輸出過程,設置輸入輸出的音頻格式(可以相同或者不同),兩個Unit對接即是將一個Unit的輸入接到另一個Unit的輸出,或者將一個Unit的輸出接到另一個Unit的輸入,需要注意的是在對接點要保證Audio Format的一致性。以Remote I/O Unit為例,結構如下圖所示:
一個I/O Unit包含兩個實體對象,兩個實體對象(Element 0、Element 1)相互獨立,根據需求可通過kAudioOutputUnitProperty_EnableIO屬性去開關它們。Element 1與硬體輸入連接,並且Element 1的輸入域(input scope)對你不可見,你只能讀取它的輸出域的數據及設置其輸出域的音頻格式;Element 0與硬體輸出連接,並且Element 0的輸出域(ouput scope)對你不可見,你只能寫入它的輸入域的數據及設置其輸入域的音頻格式。
如何將輸入設備採集的數據抓出來,又如何將處理後的數據送到輸出設備呢? 通過AURenderCallbackStruct結構,將定義的兩個回調靜態方法地址設置到需要的Element 0/1上,當Unit配置完畢並且運行後,Unit調度線程會按照當前設備狀態及音頻格式安排調度周期,循環往復的調用你提供的錄製與播放回調方法,樣例代碼如下:
// for record callback, read audio data from bufferlist
static OSStatus recordCallbackFunc(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData){
ASAudioEngineSingleU *engine = (__bridge ASAudioEngineSingleU* )inRefCon;
OSStatus err = noErr;
if (engine.audioChainIsBeingReconstructed == NO){
@autoreleasepool {
AudioBufferList bufList = [engine getBufferList:inNumberFrames];
err = AudioUnitRender([engine recorderUnit], ioActionFlags, inTimeStamp, inBusNumber, inNumberFrames, &bufList);
if (err) {
HMLogDebug(LogModuleAudio, @"AudioUnitRender error code = %d", err);
} else {
AudioBuffer buffer = bufList.mBuffers[0];
NSData *pcmBlock = [NSData dataWithBytes:buffer.mData length:buffer.mDataByteSize];
[engine didRecordData:pcmBlock];
}
}
}
return err;
}
// for play callback, fill audio data to bufferlist
static OSStatus playbackCallbackFunc(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData){
ASAudioEngineSingleU *engine = (__bridge ASAudioEngineSingleU* )inRefCon;
OSStatus err = noErr;
if (engine.audioChainIsBeingReconstructed == NO)
{
for (int i = 0; i < ioData -> mNumberBuffers; i++) {
@autoreleasepool {
AudioBuffer buffer = ioData -> mBuffers[i];
NSData *pcmBlock = [engine getPlayFrame:buffer.mDataByteSize];
if (pcmBlock && pcmBlock.length) {
UInt32 size = (UInt32)MIN(buffer.mDataByteSize, [pcmBlock length]);
memcpy(buffer.mData, [pcmBlock bytes], size);
buffer.mDataByteSize = size;
//HMLogDebug(LogModuleAudio, @"AudioUnitRender pcm data has filled");
} else {
buffer.mDataByteSize = 0;
*ioActionFlags |= kAudioUnitRenderAction_OutputIsSilence;
}
} // end pool
} // end for
} // end if
return err;
7、不同場景下AudioUnit構建樣例
7.1 I/O 無渲染
從輸入設備採集過來的數據,先經過MutilChannelMixer Unit,再送到輸出設備播放,該構建方式在於中間的Unit可對mic採集採集過來的數據進行聲相調節以及音量的調節
7.2 I/O 有渲染
該構建方式在輸入與輸出之間增加了rendercallback,可以在硬體採集過來的數據上做一些處理(例如,增益、調製、音效等)後再送到輸出播放
7.3 僅輸出並且帶渲染
適合音樂遊戲及合成器類的APP,僅使用IO Unit的output端,在rendercallback中負責播放源的提取整理並準備送播,比較簡單的構建方式
輸入端有兩路音頻流,都是通過rendercallback方式抓取數據,其中一路音頻流直接給入到Mixer Unit中,另一路先經過EQ Unit處理後給入到Mixer Unit中,
8、Tips
8.1 多線程及內存管理
儘可能的避免render callback方法內做加鎖及處理耗時較高的操作,這樣可以最大限度的提升實時性能,如果播放數據或者採集數據存在不同線程讀寫的情況,必需要加鎖保護,推薦pthread相關lock方法性能比其它鎖要高 音頻的輸入輸出一般都是一個持續的過程,在採集與播放的callback中,應儘量復用buffer及避免多次buffer拷貝,而不是每次回調都重新申請和釋放,在適當的位置加上@autoreleasepool避免長時間運行內存不斷上漲
8.2 格式
Core Audio Type中定義了AudioStreamBasicDescription結構,Audio Unit及其它很多音頻API對格式的配置都需要用到它,根據需要將該結構的信息填充正確,下面是44.1K,stereo,16bit的填充例子
audioDescription.mSampleRate = 44100;
audioDescription.mChannelsPerFrame = 2;
audioDescription.mBitsPerChannel = 16;
audioDescription.mFramesPerPacket = 1;
audioDescription.mFormatID = kAudioFormatLinearPCM;
audioDescription.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked;
audioDescription.mBytesPerFrame = (audioDescription.mBitsPerChannel/8) * audioDescription.mChannelsPerFrame;
audioDescription.mBytesPerPacket = audioDescription.mBytesPerFrame ;
蘋果官方建議在整個Audio Processing Graph或者Unit之間儘量以相同的音頻格式流通,儘管Audio Unit的輸入輸出可以不同。另外在Unit之間輸入輸出連接點要保持一致。
8.3 音質
在使用過程中,Audio Unit的format是可以動態改變的,但存在一種情況,Unit在銷毀前最好恢復到默認創建時的format,否則在銷毀後再重建Unit後,可能出現播放音質變差(音量變小,聲音粗糙)的情況。 在使用VoiceProcessing I/O Unit過程,遇到在有些iphone上開啟揚聲器後,Unit從Mic採集過來的數據為空或者噪音的情況,從APP STORE中下載了其它的VOIP類型的APP也同樣存在該問題,後來將AudioUnitSubType改成RemoteIO類型後,問題消失,懷疑蘋果在VoiceProcessing Unit上對回聲消除功能的處理上有bug
8.4 AudioSession
既然使用了音頻特性,就會用到AudioSession,隨著功能需求跟進,與它相關的問題也瞞多的,比如路由管理(聽筒揚聲器、線控耳機、藍牙耳機),打斷處理(interruption、iphone call)等,這裡以Audio Unit為主,就不對它進行詳細描述了,需要注意的是
- 音頻的路由變更(用戶挺拔耳機,或者代碼調用強制切換)涉及到iOS硬體上輸入和輸出設備的改變,I/O類型Unit的採集和播放線程在切換過程中會阻塞一定時間(200ms左右),如果是語音對講類對實時性要求較高的應用場景要考慮丟包策略。
- 在APP前台工作時,iPhone來電或者用戶主動切換到其它音頻類APP後,要及時處理音頻的打斷機制,在恰當的時機停止及恢復Unit的工作,由於iOS平台對資源的獨占方式,iPhone在通話等操作時,APP中的Unit是無法初始化或者繼續工作的。
原文連結:iOS Audio Unit (涓€) - 鎺橀噾