iOS Audio hand by hand: 变声,混响,语音合成 TTS,Swift5,基于 AVAudioEngine 等

5,046 阅读6分钟

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