1. 程式人生 > IOS開發 >iOS Audio hand by hand: 變聲,混響,語音合成 TTS,Swift5,基於 AVAudioEngine 等

iOS Audio hand by hand: 變聲,混響,語音合成 TTS,Swift5,基於 AVAudioEngine 等

AVAudioEngine 比 AVAudioPlayer 更加強大,當然使用上比起 AVAudioPlayer 繁瑣。

AVAudioEngine 對於 Core Audio 作了一些使用上的封裝簡化,簡便的做了一些音訊訊號的處理。

使用 AVAudioPlayer ,是音訊檔案級別的處理。

使用 AVAudioEngine,是音訊資料流級別的處理。

AVAudioEngine 可以做到低時延的、實時音訊處理。還可以做到音訊的多輸入,新增特殊的效果,例如三維空間音效

AVAudioEngine 可以做出強大的音樂處理與混音 app,配合製作複雜的三維空間音效的遊戲,本文來一個簡單的變聲應用

通用架構圖,場景是 K 歌

aaa

AVAudioEngine 使用指南

首先,簡單理解下

111

來一個 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 新增變聲等音效,通過做音量大小檢測。

看起來有些累贅,蘋果自然是不會推薦這樣做的。

111

如果是錄音,通過 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 平臺支援的語言和方言,然後合成語音,播放出來。

111

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
    }
複製程式碼

程式碼:

github repo