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

AVAudioEngine 比 AVAudioPlayer 更增強大,固然使用上比起 AVAudioPlayer 繁瑣。ios

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

使用 AVAudioPlayer ,是音頻文件級別的處理。github

使用 AVAudioEngine,是音頻數據流級別的處理。面試

AVAudioEngine 能夠作到低時延的、實時音頻處理。還能夠作到音頻的多輸入,添加特殊的效果,例如三維空間音效算法

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

通用架構圖,場景是 K 歌bash

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

給你們推薦一個iOS技術交流羣,羣內提供數據結構與算法、底層進階、swift、逆向、底層面試題整合文檔等免費資料!!!能夠加本人微信拉你進羣!

做者:鄧輕舟 連接:https://juejin.im/post/5d964922e51d4577ee4f4808
相關文章
相關標籤/搜索