iOS Audio hand by hand: 變聲,混響,語音合成 TTS,Swift5,基於 AVAudioEngine 等
AVAudioEngine 比 AVAudioPlayer 更加強大,當然使用上比起 AVAudioPlayer 繁瑣。
AVAudioEngine 對於 Core Audio 作了一些使用上的封裝簡化,簡便的做了一些音訊訊號的處理。
使用 AVAudioPlayer ,是音訊檔案級別的處理。
使用 AVAudioEngine,是音訊資料流級別的處理。
AVAudioEngine 可以做到低時延的、實時音訊處理。還可以做到音訊的多輸入,新增特殊的效果,例如三維空間音效
AVAudioEngine 可以做出強大的音樂處理與混音 app,配合製作複雜的三維空間音效的遊戲,本文來一個簡單的變聲應用
通用架構圖,場景是 K 歌
AVAudioEngine 使用指南
首先,簡單理解下
來一個 AVAudioEngine 例項,然後新增節點 Node,有播放器的 Player Node,音效的 Effect Node.
將節點連在音訊引擎上,即 AVAudioEngine 例項。然後建立節點間的關聯,組成一條音訊的資料處理鏈。 處理後的音訊資料,流過最後的一個節點,就是音訊引擎的輸出了。
開始做一個變聲的功能,也就是音調變化
需要用到 AVAudioEngine 和 AVAudioPlayerNode
// 音訊引擎是樞紐
var audioAVEngine = AVAudioEngine()
// 播放節點
var enginePlayer = AVAudioPlayerNode()
// 變聲單元:調節音高
let pitchEffect = AVAudioUnitTimePitch()
// 混響單元
let reverbEffect = AVAudioUnitReverb()
// 調節音訊播放速度單元
let rateEffect = AVAudioUnitVarispeed()
// 調節音量單元
let volumeEffect = AVAudioUnitEQ()
// 音訊輸入檔案
var engineAudioFile: AVAudioFile!
複製程式碼
做一些設定
先取得輸入節點的 AVAudioFormat 引用,
這是音訊流資料的預設描述檔案,包含通道數、取樣率等資訊。
實際上,AVAudioFormat 就是對 Core Audio 的音訊緩衝資料格式檔案 AudioStreamBasicDescription, 做了一些封裝。
audioAVEngine 做子節點關聯的時候,可以用到
// 做一些配置,功能初始化
func setupAudioEngine() {
// 這個例子,是單音
let format = audioAVEngine.inputNode.inputFormat(forBus: 0)
// 新增功能
audioAVEngine.attach(enginePlayer)
audioAVEngine.attach(pitchEffect)
audioAVEngine.attach(reverbEffect)
audioAVEngine.attach(rateEffect)
audioAVEngine.attach(volumeEffect)
// 連線功能
audioAVEngine.connect(enginePlayer,to: pitchEffect,format: format)
audioAVEngine.connect(pitchEffect,to: reverbEffect,format: format)
audioAVEngine.connect(reverbEffect,to: rateEffect,format: format)
audioAVEngine.connect(rateEffect,to: volumeEffect,format: format)
audioAVEngine.connect(volumeEffect,to: audioAVEngine.mainMixerNode,format: format)
// 選擇混響效果為大房間
reverbEffect.loadFactoryPreset(AVAudioUnitReverbPreset.largeChamber)
do {
// 可以先開啟引擎
try audioAVEngine.start()
} catch {
print("Error starting AVAudioEngine.")
}
}
複製程式碼
播放
func play(){
let fileURL = getURLforMemo()
var playFlag = true
do {
// 先拿 URL 初始化 AVAudioFile
// AVAudioFile 載入音訊資料,形成資料緩衝區,方便 AVAudioEngine 使用
engineAudioFile = try AVAudioFile(forReading: fileURL)
// 變聲效果,先給一個音高的預設值
// 看效果,來點尖利的
pitchEffect.pitch = 2400
reverbEffect.wetDryMix = UserSetting.shared.reverb
rateEffect.rate = UserSetting.shared.rate
volumeEffect.globalGain = UserSetting.shared.volume
} catch {
engineAudioFile = nil
playFlag = false
print("Error loading AVAudioFile.")
}
// AVAudioPlayer 主要是音量大小的檢測,這裡做了一些取巧
// 就是為了製作上篇播客介紹的,企鵝張嘴的動畫效果
do {
audioPlayer = try AVAudioPlayer(contentsOf: fileURL)
audioPlayer.delegate = self
if audioPlayer.duration > 0.0 {
// 不靠他播放,要靜音
// audioPlayer 不是用於播放音訊的,所以他的音量設定為 0
audioPlayer.volume = 0.0
audioPlayer.isMeteringEnabled = true
audioPlayer.prepareToPlay()
} else {
playFlag = false
}
} catch {
audioPlayer = nil
engineAudioFile = nil
playFlag = false
print("Error loading audioPlayer.")
}
// 兩個播放器,要一起播放,前面做了一個 audioPlayer 可用的標記
if playFlag == true {
// enginePlayer,有聲音
// 真正用於播放的 enginePlayer
enginePlayer.scheduleFile(engineAudioFile,at: nil,completionHandler: nil)
enginePlayer.play()
// audioPlayer,沒聲音,用於檢測
audioPlayer.play()
setPlayButtonOn(flag: true)
startUpdateLoop()
audioStatus = .playing
}
}
複製程式碼
上面的小技巧: AVAudioPlayerNode + AVAudioPlayer
同時播放 AVAudioPlayerNode (有聲音), AVAudioPlayer (啞巴的,就為了取下資料與狀態), 通過 AVAudioPlayerNode 新增變聲等音效,通過做音量大小檢測。
看起來有些累贅,蘋果自然是不會推薦這樣做的。
如果是錄音,通過 NodeTapBlock 對音訊輸入流的資訊,做實時分析。
播放也類似,處理音訊訊號,取出平均音量,就可以重新整理 UI 了。
通過 AVAudioPlayer ,可以方便拿到當前播放時間,檔案播放時長等資訊,
通過 AVAudioPlayerDelegate,可以方便播放結束了,去重新整理 UI
當然,使用 AVAudioPlayerNode ,這些都是可以做到的
結束播放
func stopPlayback() {
setPlayButtonOn(flag: false)
audioStatus = .stopped
// 兩個播放器,一起開始,一起結束
audioPlayer.stop()
enginePlayer.stop()
stopUpdateLoop()
}
複製程式碼
音效: 音高,混響,播放速度,音量大小
調節音高,用來變聲, AVAudioUnitTimePitch
音效的 pitch 屬性,取值範圍從 -2400 音分到 2400 音分,包含 4 個八度音階。 預設值為 0
一個八度音程可以分為12個半音。
每一個半音的音程相當於相鄰鋼琴鍵間的音程,等於100音分
func setPitch(value: Float) {
pitchEffect.pitch = value
}
複製程式碼
調節混響, AVAudioUnitReverb
wetDryMix 的取值範圍是 0 ~ 100,
0 是全乾,幹聲即無音樂的純人聲
100 是全溼潤,空間感很強。
幹聲是原版,溼聲是經過後期處理的。
func toSetReverb(value: Float) {
reverbEffect.wetDryMix = value
}
複製程式碼
調節音訊播放速度, AVAudioUnitVarispeed
音訊播放速度 rate 的取值範圍是 0.25 ~ 4.0,
預設是 1.0,正常播放。
func toSetRate(value: Float) {
rateEffect.rate = value
}
複製程式碼
調節音量大小, AVAudioUnitEQ
globalGain 的取值範圍是 -96 ~ 24, 單位是分貝
func toSetVolumn(value: Float){
volumeEffect.globalGain = value
}
複製程式碼
語音合成 TTS,輸入文字,播放對應的語音
TTS,一般會用到 AVSpeechSynthesizer 和他的代理 AVSpeechSynthesizerDelegate AVSpeechSynthesizer 是 AVFoundation 框架下的一個類,它的功能就是輸入文字,讓你的應用,選擇 iOS 平臺支援的語言和方言,然後合成語音,播放出來。
iOS 平臺,支援三種中文,就是三種口音,有中文簡體 zh-CN,Ting-Ting 朗讀;有 zh-HK,Sin-Ji 朗讀;有 zh-TW,Mei-Jia 朗讀。
可參考 How to get a list of ALL voices on iOS
AVSpeechSynthesizer 合成器相關知識
AVSpeechSynthesizer 需要拿材料 AVSpeechUtterance 去朗讀。
語音文字單元 AVSpeechUtterance 封裝了文字,還有對應的朗讀效果引數。
朗讀效果中,可以設定口音,本文 Demo 採用 zh-CN。還可以設定變聲和語速 (發音速度)。
拿到 AVSpeechUtterance ,合成器 AVSpeechSynthesizer 就可以朗讀了。如果 AVSpeechSynthesizer 正在朗讀,AVSpeechUtterance 就會放在 AVSpeechSynthesizer 的朗讀佇列裡面,按照先進先出的順序等待朗讀。
蘋果框架的粒度都很細,語音合成器 AVSpeechSynthesizer,也有暫定、繼續播放與結束播放功能。
停止了語音合成器 AVSpeechSynthesizer,如果他的朗讀佇列裡面還有語音文字AVSpeechUtterance,剩下的都會直接移除。
AVSpeechSynthesizerDelegate 合成器代理相關
使用合成器代理,可以監聽朗讀時候的事件。例如:開始朗讀,朗讀結束
TTS: Text To Speech 三步走
先設定
// 來一個合成器
let synthesizer = AVSpeechSynthesizer()
// ...
// 設定合成器的代理,監聽事件
synthesizer.delegate = self
複製程式碼
朗讀、暫停、繼續朗讀與停止朗讀
// 朗讀
func play() {
let words = UserSetting.shared.message
// 拿文字,去例項化語音文字單元
let utterance = AVSpeechUtterance(string: words)
// 設定發音為簡體中文 ( 中國大陸 )
utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
// 設定朗讀的語速
utterance.rate = AVSpeechUtteranceMaximumSpeechRate * UserSetting.shared.rate
// 設定音高
utterance.pitchMultiplier = UserSetting.shared.pitch
synthesizer.speak(utterance)
}
// 暫停朗讀,沒有設定立即暫停,是按字暫停
func pausePlayback() {
synthesizer.pauseSpeaking(at: AVSpeechBoundary.word)
}
// 繼續朗讀
func continuePlayback() {
synthesizer.continueSpeaking()
}
// 停止播放
func stopPlayback() {
// 讓合成器馬上停止朗讀
synthesizer.stopSpeaking(at: AVSpeechBoundary.immediate)
// 停止計時器更新狀態,具體見文尾的 github repo
stopUpdateLoop()
setPlayButtonOn(false)
audioStatus = .stopped
}
複製程式碼
設定合成器代理,監聽狀態改變的時機
// 開始朗讀。朗讀每一個語音文字單元的時候,都會來一下
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,didStart utterance: AVSpeechUtterance) {
setPlayButtonOn(true)
startUpdateLoop()
audioStatus = .playing
}
// 結束朗讀。每一個語音文字單元結束朗讀的時候,都會來一下
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,didFinish utterance: AVSpeechUtterance) {
stopUpdateLoop()
setPlayButtonOn(false)
audioStatus = .stopped
}
// 語音文字單元裡面,每一個字要朗讀的時候,都會來一下
// 讀書應用,朗讀前,可以用這個高光正在讀的詞語
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,willSpeakRangeOfSpeechString characterRange: NSRange,utterance: AVSpeechUtterance) {
let speakingString = utterance.speechString as NSString
let word = speakingString.substring(with: characterRange)
print(word)
}
// 暫定朗讀
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,didPause utterance: AVSpeechUtterance) {
stopUpdateLoop()
setPlayButtonOn(false)
audioStatus = .paused
}
// 繼續朗讀
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer,didContinue utterance: AVSpeechUtterance) {
setPlayButtonOn(true)
startUpdateLoop()
audioStatus = .playing
}
複製程式碼
程式碼: