Audio Unit詳解
本篇博客有何不同
Audio Unit(以下稱AU)是iOS底層的音頻框架,對于進階開發(fā)者AU是必需掌握的框架之一,因為面向當下,掌握底層的音頻框架可以讓你與其他初級開發(fā)者區(qū)別開,如果面向未來损趋,隨著網(wǎng)絡帶寬的增加,音視頻技術的應用范圍一定會更廣椅寺,應用頻率也會更高舶沿。
我看了不少關于AU的技術博客,可能出于項目機密的原因配并,大多數(shù)只講原理括荡,而且只講某一個應用方向的原理,比如錄音溉旋、播放畸冲、錄音同時播放,對于api的講解也不夠全面,比如同樣實現(xiàn)錄音邑闲,大多數(shù)博客講的兩種不同方式算行,卻沒有說清楚原因。對于實現(xiàn)功能的代碼也不是很完整苫耸,大多都是從各自項目里面摘抄的部分代碼州邢,導致我們在實際使用的時候找不到完整的例子,今天這篇博客就是站在巨人的肩上褪子,從原理到demo統(tǒng)統(tǒng)給你講清楚量淌,希望對你有所幫助。
框架層級
從上圖可見嫌褪,AU處于距離硬件最近的底層呀枢,幾乎就是直接和硬件打交道了,所以如果使用這一層的api笼痛,你能得到最多的自由度和最低的延遲裙秋,但副作用就是最高的復雜度,這一層的很多api不是很直觀缨伊,也有的概念會出現(xiàn)重疊和歧義摘刑,再加上直接使用這層api的應用不多見,所以相關的資料比較少刻坊。
如果只是錄音或者播放音頻枷恕,完全沒有必要使用AU,直接使用AVKit或者Audio Queue簡單得多紧唱。
那么AU能實現(xiàn)哪些功能呢活尊?或者說什么樣的需求才犯得上我們直接啃AU的硬骨頭呢隶校?有這些
- 低延時同步音頻輸入輸出漏益,例如 VoIP 應用
- 響應回放合成聲音,例如音樂游戲或合成樂器
- 使用特定的 audio unit 特征深胳,例如回聲消除绰疤,混音,色調(diào)均衡
- 處理鏈結構讓你可以將音頻處理模塊組裝到靈活的網(wǎng)絡中舞终。這是 iOS 中唯一提供此功能的音頻 API轻庆。(這句話是從其他博客抄的,用人話說敛劝,就是你需要鏈式處理音頻單元時就會用到余爆,比如依次進行錄音-回聲消除-美音-混音-輸出到設備)
工作原理
這里用三個圖來舉例比較形象生動。
1夸盟、采集音頻-播放音頻
在AU中有三個基本的Element蛾方,分別是Element0、Element1和Global(下面會說),有的地方把Element叫做bus桩砰,就是總線拓春,很多關于Audio Unit的教程里面說的bus通常就是這玩意。
bus就是硬件管道在軟件上的抽象概念亚隅,在上圖中AU里面音頻數(shù)據(jù)的流動被抽象為從Element1流向Element0硼莽,即從硬件話筒到APP處理,再到硬件麥克風煮纵。scope表示在一個Element上輸入或輸出懂鸵。而APP能影響的范圍,就是從Element1的output scope到Element0的input scope醉途。
2矾瑰、多個音頻單元鏈式處理
在AU中一個unit被稱為一個音頻處理單元,通常一種單元只能做一種固定的事情隘擎,比如連接硬件(remote i/o, VP i/o)殴穴、效果器(effect)、混音(mix)货葬、轉(zhuǎn)換器(Format)采幌,每種unit可能有一個或多個輸入,比如remote i/o只有一個輸入震桶, mix可有多個輸入休傍,但每種unit通常只有一個輸出。
多個unit可以并行或串行進行處理蹲姐,上圖中就是兩個效果器unit(EQ unit)的輸出連接到一個混音unit(Mixer unit)的輸入上磨取,最后輸出到硬件(I/O unit)。
3柴墩、音頻數(shù)據(jù)控制流
在實際處理音頻數(shù)據(jù)時忙厌,音頻數(shù)據(jù)雖然是按照順序在處理鏈中流動,但數(shù)據(jù)控制流卻是相反的江咳,有點像Cocoa Touch中事件傳遞鏈和響應鏈的關系逢净,這么說大概懂了吧。
舉個栗子歼指,考試的時候?qū)W霸坐在第一排爹土,后邊的都是學渣,最后一排的學渣想要小抄就去問前面一排的學渣踩身,前面一排的學渣說我也沒有胀茵,又去問再前一排的學渣,直到問到第一排的學霸挟阻,學霸才把答案寫好往后傳琼娘,每一排的學渣抄一遍答案后就把小抄往下傳呵哨,直到最后一名學渣得到答案。
真實的流程就是這樣轨奄,當啟動每個unit后孟害,每個unit都在等待獲取數(shù)據(jù),于是可以在回調(diào)函數(shù)中調(diào)用AudioUnitRender挪拟,來向上一級unit申請數(shù)據(jù)(上一級unit的回調(diào)函數(shù)就會響應)挨务,就算你不使用AudioUnitRender,系統(tǒng)也會根據(jù)Unit的連接順序pull上一個unit的輸出玉组。如果上一級也沒有谎柄,再向上一級申請。當?shù)谝患塽nit處理好數(shù)據(jù)后惯雳,就把數(shù)據(jù)從當前Unit的output輸出朝巫,這樣下一級unit就可以繼續(xù)處理了。
數(shù)字信號基礎知識
1石景、信號的編碼與解碼
編碼過程-信號的數(shù)字化
<center>圖1.5</center>
信號的數(shù)字化就是將連續(xù)的模擬信號轉(zhuǎn)換成離散的數(shù)字信號劈猿, 一般需要完成采樣、量化和編碼三個步驟潮孽,如圖 1.5 所示揪荣。采樣是指用每隔一定時間間隔的信號樣本值序列來代替原來在時間上連續(xù)的信號。量化是用有限個幅度近似表示原來在時間上連續(xù)變化的幅度值往史,把模擬信號的連續(xù)幅度變?yōu)橛邢迶?shù)量仗颈、有一定時間間隔的離散值。編碼則是按照一定的規(guī)律椎例,把量化后的離散值用二進制數(shù)碼表示挨决。上述數(shù)字化的過程又稱為脈沖編碼調(diào)制(Pulse Code Modulation) ,通常由 A/D 轉(zhuǎn)換器來實現(xiàn)订歪。
解碼過程
音頻解碼及編碼的逆過程脖祈,通過使用與編碼方式對應的解碼器,對數(shù)字信號進行模擬化陌粹。
數(shù)字信號編解碼的特有難點在于壓縮算法撒犀,壓縮算法決定了帶寬的利用率福压、聲音的還原程度和延遲等掏秩,這里會根據(jù)具體的應用場景去靜態(tài)或動態(tài)地選擇不同的壓縮算法,即選擇不同的音頻格式荆姆。由于篇幅有限蒙幻,這里涵蓋的內(nèi)容又很多,等我研究清楚了再展開討論胆筒。
Audio Unit的基本使用方法
與AU相關的api通常有兩套邮破,一套是直接使用Audio Unit诈豌,另一套是使用AUGraph,但是AUGraph已經(jīng)被聲明為Deprecated抒和,目前主推的是AudioEngine矫渔,AudioEngine位于AVFoundation中,用起來像是被封裝過的AUGraph摧莽,本文僅討論Audio Unit的使用方法庙洼。
AU的使用步驟大概是這樣:
- 創(chuàng)建需要的Unit
- 給每個Unit設置對應的屬性,聲明每個Unit的output格式
- 初始化Unit
- 開啟Unit
- 關閉Unit
AU包含的Unit有7種:
- Effect - iPod Equalizer 效果器镊辕,比如均衡油够、延遲、回響等
- Mixing - 3D Mixer 和OpenAl相關的混音
- Mixing - Multichannel Mixer 多路混音征懈,我們一般用這個
- I/O - Remote I/O 連接硬件的io
- I/O - Voice-Processing I/O 在硬件io的基礎上增加了回聲消除石咬、自動增益校正、語音質(zhì)量調(diào)整卖哎、靜音等功能
- I/O - Generic Output 脫離音頻硬件的io通道鬼悠,可以從文件中獲取音頻源
- Format conversion - Format Converter 格式轉(zhuǎn)換。注意了亏娜,變調(diào)timePitch是這個類型里面的子類
每種Unit可用的屬性都不同厦章,但是這些屬性的名字又都在一個Enum里面,用的時候要小心照藻,建議先了解清楚你需要的每種Unit的用法袜啃。
擼起袖子開始寫demo
接下來會先分析Audio Unit中的各種api,完整的Demo在最后面幸缕。
使用Audio Unit實現(xiàn)錄音耳返
使用AU進行錄音有兩種方式群发,一、直接使用Audio Unit发乔,二熟妓、使用AUGraphic連接音頻輸入輸出單元。這里我們分開講栏尚,但是他們有些共同的地方起愈。
一、直接使用Audio Unit
1.設置AudioSession
func setupAudioSession() {
let session: AVAudioSession = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
try session.overrideOutputAudioPort(.none)
try session.setPreferredSampleRate(Double(AudioConst.SampleRate))
//每次處理的buffer大小
try session.setPreferredIOBufferDuration(Double(AudioConst.BufferDuration) / 1000.0)
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print(error.localizedDescription)
}
}
2.創(chuàng)建ioUnit
var ioDes: AudioComponentDescription = AudioComponentDescription.init(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_RemoteIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0)
guard let inputComp: AudioComponent = AudioComponentFindNext(nil, &ioDes) else {
print("outputComp init error")
return false
}
if AudioComponentInstanceNew(inputComp, &ioUnit) != noErr {
print("io AudioComponentInstanceNew error")
return false
}
3.設置ioUnit參數(shù)
//是否打開輸入译仗、輸出
var value: UInt32 = 1
if AudioUnitSetProperty(self.ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, AudioConst.InputBus, &value, UInt32(MemoryLayout.size(ofValue: value))) != noErr {
print("can't enable input io")
return false
}
value = 1 //如果不需要從硬件輸出 就把value設置為0
if AudioUnitSetProperty(self.ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, AudioConst.OutputBus, &value, UInt32(MemoryLayout.size(ofValue: value))) != noErr {
print("can't enable output io")
return false
}
//設置最大切片抬虽,就是連接兩個unit的管道有多粗,這個參數(shù)和第一步setPreferredIOBufferDuration的大小有關纵菌,太小的話會報錯阐污,最好設置大一些,
var maxSlice: Int32 = 4096
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AudioConst.OutputBus, &maxSlice, UInt32(MemoryLayout.size(ofValue: maxSlice))) != noErr {
print("set MaximumFramesPerSlice error")
return false
}
//設置Unit輸出格式
var ioFormat: AudioStreamBasicDescription = AudioStreamBasicDescription.init(
mSampleRate: Float64(AudioConst.SampleRate),
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked,
mBytesPerPacket: UInt32(2 * AudioConst.Channels),
mFramesPerPacket: 1,
mBytesPerFrame: UInt32(2 * AudioConst.Channels),
mChannelsPerFrame: UInt32(AudioConst.Channels),
mBitsPerChannel: 16,
mReserved: 0)
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, AudioConst.InputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
print("set StreamFormat error")
return false
}
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, AudioConst.OutputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
print("set StreamFormat error")
return false
}
//設置回調(diào)咱圆,下一級unit取數(shù)據(jù)的時候回到這里來取笛辟,具體回調(diào)定義在demo里面
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, AudioConst.OutputBus, &recordCallback, UInt32(MemoryLayout.size(ofValue: recordCallback))) != noErr {
print("SetRenderCallback error")
return false
}
3.啟動功氨、關閉Unit
//啟動
public func startRecord() {
var error = AudioUnitInitialize(self.ioUnit!)
if error != noErr {
print("AudioUnitInitialize error: \(error)")
}
error = AudioOutputUnitStart(self.ioUnit!)
if error != noErr {
print("AudioOutputUnitStart error")
}
}
//關閉
public func stopRecord() {
AudioUnitUninitialize(self.ioUnit!)
AudioOutputUnitStop(self.ioUnit!)
}
tips:AU的每個api都會返回錯誤碼,如果遇到不是noErr手幢,即0的結果捷凄,可以到這里去查一下錯誤碼的定義,可以有效地幫助你排查問題原因
二围来、使用AUGraphic實現(xiàn)錄音
1.設置AudioSession
func setupSession() {
let session: AVAudioSession = AVAudioSession.sharedInstance()
do {
try session.setCategory(.playAndRecord, options: [.allowBluetooth, .allowBluetoothA2DP])
try session.overrideOutputAudioPort(.none)
try session.setPreferredSampleRate(Double(AudioConst.SampleRate))
try session.setPreferredIOBufferDuration(Double(AudioConst.BufferDuration) / 1000.0)
try session.setActive(true, options: .notifyOthersOnDeactivation)
} catch {
print(error.localizedDescription)
}
}
2.獲取ioUnit
var ioDes: AudioComponentDescription = AudioComponentDescription.init(
componentType: kAudioUnitType_Output,
componentSubType: kAudioUnitSubType_RemoteIO,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0)
var status: OSStatus = noErr
status = NewAUGraph(&process)
if status != noErr {
print("NewAUGraph error")
return
}
status = AUGraphOpen(self.process!)
if status != noErr {
print("AUGraphOpen error")
return
}
//獲取node
var ioNode: AUNode = 0
status = AUGraphAddNode(self.process!, &ioDes, &ioNode)
if status != noErr {
print("AUGraphAddNode error")
return
}
//從node獲取unit引用
AUGraphNodeInfo(self.process!, ioNode, &ioDes, &ioUnit)
2.設置unit參數(shù)纵势,同單獨使用Unit一樣
//是否打開輸入、輸出
var value: UInt32 = 1
if AudioUnitSetProperty(self.ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Input, AudioConst.InputBus, &value, UInt32(MemoryLayout.size(ofValue: value))) != noErr {
print("can't enable input io")
return false
}
value = 1 //如果不需要從硬件輸出 就把value設置為0
if AudioUnitSetProperty(self.ioUnit!, kAudioOutputUnitProperty_EnableIO, kAudioUnitScope_Output, AudioConst.OutputBus, &value, UInt32(MemoryLayout.size(ofValue: value))) != noErr {
print("can't enable output io")
return false
}
//設置最大切片管钳,就是連接兩個unit的管道有多粗钦铁,這個參數(shù)和第一步setPreferredIOBufferDuration的大小有關,太小的話會報錯才漆,最好設置大一些牛曹,
var maxSlice: Int32 = 4096
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AudioConst.OutputBus, &maxSlice, UInt32(MemoryLayout.size(ofValue: maxSlice))) != noErr {
print("set MaximumFramesPerSlice error")
return false
}
//設置Unit輸出格式
var ioFormat: AudioStreamBasicDescription = AudioStreamBasicDescription.init(
mSampleRate: Float64(AudioConst.SampleRate),
mFormatID: kAudioFormatLinearPCM,
mFormatFlags: kLinearPCMFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked,
mBytesPerPacket: UInt32(2 * AudioConst.Channels),
mFramesPerPacket: 1,
mBytesPerFrame: UInt32(2 * AudioConst.Channels),
mChannelsPerFrame: UInt32(AudioConst.Channels),
mBitsPerChannel: 16,
mReserved: 0)
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, AudioConst.InputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
print("set StreamFormat error")
return false
}
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Input, AudioConst.OutputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
print("set StreamFormat error")
return false
}
//設置回調(diào),下一級unit取數(shù)據(jù)的時候回到這里來取醇滥,具體回調(diào)定義在demo里面
if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_SetRenderCallback, kAudioUnitScope_Input, AudioConst.OutputBus, &recordCallback, UInt32(MemoryLayout.size(ofValue: recordCallback))) != noErr {
print("SetRenderCallback error")
return false
}
3.連接node
//連接node, 把ioNode的bus1 連接在ioNode的bus0黎比,即硬件的輸入連到硬件輸出上
status = AUGraphConnectNodeInput(self.process!, ioNode, 1, ioNode, 0)
if status != noErr {
print("AUGraphConnectNodeInput error")
return
}
//初始化AUGraphic流程,如果前面哪些步驟有問題鸳玩,這里也會報錯
status = AUGraphInitialize(self.process!)
if status != noErr {
print("AUGraphInitialize error: \(status)")
}
4.開啟和停止
public func start() {
AUGraphStart(self.process!)
}
public func stop() {
AUGraphStop(self.process!)
}
使用Audio Unit實現(xiàn)實時變調(diào)
實現(xiàn)音頻變調(diào)有多種方式阅虫,這里只講通過Audio Unit中的timePitch來實現(xiàn)變調(diào)。
先講一個結論不跟,經(jīng)過我多種嘗試和國內(nèi)外論壇中摸爬滾打颓帝,始終沒能通過Audio Unit直接實現(xiàn)變調(diào),嘗試過程中要么是報錯要么沒有聲音窝革,最終是通過AUGraphic來實現(xiàn)實時變調(diào)功能购城。
以下是實現(xiàn)過程,這里只講重點哈虐译,因為大部分內(nèi)容和上述耳返過程相同瘪板。
1.獲取pitchUnit,并設置參數(shù)
//經(jīng)過多方嘗試漆诽,發(fā)現(xiàn)一旦使用了pitch這個unit侮攀,就不能設置ioUnit的輸入格式,所以需要把設置ioUnit輸入格式的地方注釋掉
// if AudioUnitSetProperty(self.ioUnit!, kAudioUnitProperty_StreamFormat, kAudioUnitScope_Output, AudioConst.InputBus, &ioFormat, UInt32(MemoryLayout.size(ofValue: ioFormat))) != noErr {
// print("set StreamFormat error")
// return
// }
//如果需要獲取變調(diào)后的音頻數(shù)據(jù)厢拭,即設置了renderCallback兰英,
//使用pitchUnit的時候setPreferredIOBufferDuration的值要大一些,具體多大蚪腐,我設置了1s箭昵。發(fā)現(xiàn)超過200ms以后效果就有明顯改善
//如果沒有設置renderCallback税朴,setPreferredIOBufferDuration設置10ms就夠了回季,耳返效果完美家制。
//這里也是本項目未能完美解決的地方,如有更好的方案泡一,請與我分享颤殴。
var pitchDes: AudioComponentDescription = AudioComponentDescription.init(
componentType: kAudioUnitType_FormatConverter,
componentSubType: kAudioUnitSubType_NewTimePitch,
componentManufacturer: kAudioUnitManufacturer_Apple,
componentFlags: 0,
componentFlagsMask: 0)
var pitchNode: AUNode = 0
status = AUGraphAddNode(self.process!, &pitchDes, &pitchNode)
if status != noErr {
print("AUGraphAddNode error")
return
}
status = AUGraphNodeInfo(self.process!, pitchNode, &pitchDes, &pitchUnit)
if status != noErr {
print("AUGraphNodeInfo error")
return
}
if AudioUnitSetProperty(self.pitchUnit!, kAudioUnitProperty_MaximumFramesPerSlice, kAudioUnitScope_Global, AudioConst.OutputBus, &maxSlice, UInt32(MemoryLayout.size(ofValue: maxSlice))) != noErr {
print("set MaximumFramesPerSlice error")
return
}
2.連接Unit
//連接node
status = AUGraphConnectNodeInput(self.process!, ioNode, 1, pitchNode, 0)
if status != noErr {
print("AUGraphConnectNodeInput error")
return
}
status = AUGraphConnectNodeInput(self.process!, pitchNode, 0, ioNode, 0)
if status != noErr {
print("AUGraphConnectNodeInput error")
return
}
status = AUGraphInitialize(self.process!)
if status != noErr {
print("AUGraphInitialize error: \(status)")
}
3.啟動和停止
public func start() {
AUGraphStart(self.process!)
}
public func stop() {
AUGraphStop(self.process!)
}
4.設置變調(diào)參數(shù)
public func setPitch(pValue: Float) {
//取值范圍 -2000 ~ 2000
var value: Float32 = Float32((pValue - 0.5) * 2 * 2000)
if AudioUnitSetParameter(self.pitchUnit!, kNewTimePitchParam_Pitch, kAudioUnitScope_Global, AudioConst.OutputBus, AudioUnitParameterValue(value), 0) != noErr {
print("set kNewTimePitchParam_Pitch error")
}
}
引用文摘
https://blog.csdn.net/alwaysrun/article/details/108476785
https://www.cnblogs.com/wangyaoguo/p/8392660.html
https://blog.csdn.net/chenhande1990chenhan/article/details/78770452