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
}
代码: