AVSpeechSynthesizer 语音合成暂停控制方案详解
2025-01-27 04:59:47
AVSpeechSynthesizer 语音合成中的暂停控制
AVSpeechSynthesizer 为 iOS, macOS, watchOS 和 tvOS 等 Apple 平台提供了文本转语音的功能。开发者经常需要对合成过程进行细粒度控制,比如在特定位置暂停朗读,然后在稍后恢复。本篇文章探讨如何实现这一需求。
难题分析:缺乏直接的暂停机制
AVSpeechSynthesizer 的设计没有直接提供按字符串索引或时间戳暂停的方法。pauseSpeaking(at: .immediate)
方法只会立即暂停,并没有与字符串位置相关的接口,continueSpeaking()
方法只能恢复播放,同样缺少精细控制。这带来了以下两个难题:
- 定位暂停点困难 : 如何精准确定字符串中的暂停位置?
- 同步延迟与恢复 : 如何在指定延迟后,无缝恢复语音合成?
解决这些难题需要另辟蹊径。
解决方案一:分割文本,逐段合成
此方案的核心是将文本分解成更小的片段,利用 AVSpeechSynthesizer
完成分段合成。每个片段的结尾即为暂停点。这样可以通过控制每段的开始和延迟来实现暂停与恢复的效果。
原理:
将需要朗读的文本按照预期暂停点进行切割,例如按句子、段落或特定的索引位置。随后,逐一合成这些片段,在每个片段完成之后,设置定时器,等待延迟后再开始下一个片段。通过这种方法实现类似暂停的效果。
步骤:
- 文本分割: 依据需求,对文本进行分割。
- 合成处理: 创建
AVSpeechSynthesizer
对象。 - 分段播放: 遍历分割的文本片段。每合成完一段,设置定时器或使用DispatchWorkItem延迟执行下一个片段。
代码示例:
import AVFoundation
import Foundation
class TextSpeechManager {
private let synthesizer = AVSpeechSynthesizer()
private var currentUtteranceIndex = 0
private var utterances: [AVSpeechUtterance] = []
private var delay: TimeInterval = 0
func speak(text: String, pauses: [Int], delay: TimeInterval) {
self.delay = delay
utterances = createUtterances(text: text, pauses: pauses)
if utterances.count > 0{
currentUtteranceIndex = 0
speakCurrentUtterance()
}
}
func stopSpeaking(){
synthesizer.stopSpeaking(at: .immediate)
}
private func createUtterances(text: String, pauses: [Int]) -> [AVSpeechUtterance]{
var splitSentences : [String] = []
var startIndex = 0
for pauseIndex in pauses {
let pausePoint = text.index(text.startIndex, offsetBy: pauseIndex)
let splitSegment = String(text[text.index(text.startIndex, offsetBy: startIndex)..<pausePoint])
splitSentences.append(splitSegment)
startIndex = pauseIndex
}
if(startIndex < text.count){
let lastSegment = String(text[text.index(text.startIndex,offsetBy: startIndex)..<text.endIndex])
splitSentences.append(lastSegment)
}
return splitSentences.map{
let utterance = AVSpeechUtterance(string: $0)
// 自定义 utterance 配置 (语音, 速率等等)
return utterance
}
}
private func speakCurrentUtterance() {
guard currentUtteranceIndex < utterances.count else{return}
let utterance = utterances[currentUtteranceIndex]
synthesizer.speak(utterance)
}
func audioPlayEndCallBack() {
currentUtteranceIndex += 1
if currentUtteranceIndex < utterances.count {
DispatchQueue.main.asyncAfter(deadline: .now() + delay){ [weak self] in
self?.speakCurrentUtterance()
}
}
}
}
extension TextSpeechManager : AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) {
self.audioPlayEndCallBack()
}
}
安全建议:
- 可以使用
AVSpeechSynthesizerDelegate
来监听每一个utterance
的didFinish
事件。didFinish
后在计时,确保每段话都有缓冲时间播放完成。 - 要处理取消合成时的逻辑,例如快速切换文本时需要停止旧合成任务,确保不会造成混乱。
- 此解决方案对文本分割逻辑有一定的要求,务必测试不同的暂停点位置,避免语音被打断,或出现逻辑上的异常。
- 为了避免过快或过慢的音频播放速度,需要在创建
AVSpeechUtterance
时正确配置播放速率参数。
解决方案二: 利用Delegate和标志位
此方案利用 AVSpeechSynthesizerDelegate
协议中的代理方法 speechSynthesizer(_:willSpeakRangeOf:utterance:)
来进行精准控制,可以根据当前的朗读范围,在特定位置插入暂停。
原理:
该方法提供正在朗读的字符串范围信息。我们可以结合事先确定的暂停位置,在 willSpeakRangeOf
方法中判断,如果到达暂停点,调用 pauseSpeaking
暂停语音合成,并在一定延迟之后恢复。
步骤:
- 记录暂停点 : 在开始朗读前,记录预期的暂停点索引。
- 代理回调: 在
speechSynthesizer(_:willSpeakRangeOf:utterance:)
方法中检查当前的range.location
。 - 条件暂停 : 如果到达暂停点,暂停合成,并设置延迟定时器。
- 恢复 : 定时器结束时,调用
continueSpeaking()
.
代码示例:
import AVFoundation
import Foundation
class CustomAVSpeechSynthesizer {
private var synthesizer = AVSpeechSynthesizer()
private var shouldPause = true
private var pausedPoint = -1
private var currentPauseDelay: TimeInterval = 0
func speechSynthesizer(text: String, pausedPoint:Int, delay: TimeInterval ){
self.pausedPoint = pausedPoint
self.currentPauseDelay = delay
let utterance = AVSpeechUtterance(string: text)
synthesizer.delegate = self
synthesizer.speak(utterance)
}
func stopSpeak(){
synthesizer.stopSpeaking(at: .immediate)
}
}
extension CustomAVSpeechSynthesizer: AVSpeechSynthesizerDelegate {
func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, willSpeakRangeOf range: NSRange, utterance: AVSpeechUtterance) {
if range.location == pausedPoint && shouldPause {
synthesizer.pauseSpeaking(at: .immediate)
shouldPause = false
DispatchQueue.main.asyncAfter(deadline: .now() + currentPauseDelay) { [weak self] in
self?.synthesizer.continueSpeaking()
self?.shouldPause = true
}
}
}
}
安全建议:
- 标志位的设置要清晰,确保只有一个暂停事件正在等待执行,可以规避异步执行导致的意外问题。
- 该方案依赖
willSpeakRangeOf
代理方法。如果语音合成的引擎跳过某些片段(特别是在处理不常见的语言或语音错误时),那么实际的暂停位置可能和预期不同。充分测试各种场景下的暂停表现。 - 要在退出页面或取消语音合成时重置
shouldPause
等状态标志,避免出现不可控的行为。
总结
对 AVSpeechSynthesizer
进行暂停控制需要根据具体的应用场景,采取不同的策略。对于需要精准控制暂停位置的场景,可以考虑对文本进行分割,然后逐段合成,或借助代理方法精确定位暂停。选择何种方案取决于所需的控制精度和实现的复杂度,切记安全性和异常情况处理不可忽视。