控制循环执行:AVSpeechSynthesizer多语句顺序播报时序问题
问题根因
- 原有
playSound函数中的for循环为同步执行逻辑,会瞬间完成所有迭代,一次性创建全部4个Timer实例,完全不会等待前一句语音播报完成。所有Timer在同一时间被创建后,只是各自按照延迟时间排队触发语音播放,和语音实际的播放进度完全脱节,自然无法实现播完一句停一下、再播下一句的效果。 - 没有利用
AVSpeechSynthesizer的完成代理回调做流程衔接,靠外部硬编码Timer凑间隔的逻辑和真实的语音播放状态完全不同步。
回调驱动的实现方案
你提到的“等待didFinish代理回调触发后再推进下一条播报”的思路是完全正确的,也是苹果官方推荐的状态驱动实现方式。核心逻辑是把主动循环遍历的模式,改成队列+回调推进的模式:
- 维护一个待播报的文本队列,启动时把所有需要播报的内容存入队列
- 每次只从队列头部取出一条文本,构造语音任务开始播报
- 监听
didFinish代理回调,确认单句播报完全结束后,先预留设定的停顿间隔,再触发队列中下一条内容的播报 - 直到队列清空时,整个播报流程自动终止
改造后的Speaker核心实现
import AVFoundation class Speaker: NSObject { let synth = AVSpeechSynthesizer() // 存储待播报的文本队列 private var pendingLines: [String] = [] // 配置单句播报完成后的停顿间隔(单位:秒) var pauseBetweenLines: TimeInterval = 1.5 override init() { super.init() synth.delegate = self } // 对外暴露的批量播报入口 func startSpeaking(lines: [String]) { pendingLines = lines // 启动第一句播报 speakNext() } // 内部逻辑:取出队列下一条内容开始播报 private func speakNext() { // 队列为空时直接终止流程 guard !pendingLines.isEmpty else { print("所有播报内容已执行完成") return } let nextContent = pendingLines.removeFirst() let utterance = AVSpeechUtterance(string: nextContent) utterance.voice = AVSpeechSynthesisVoice(language: "en-GB") utterance.rate = 0.5 synth.speak(utterance) print("开始播报:\(nextContent)") } } extension Speaker: AVSpeechSynthesizerDelegate { func speechSynthesizer(_ synthesizer: AVSpeechSynthesizer, didFinish utterance: AVSpeechUtterance) { print("单句播报完成") // 单句播完后,等待设定的停顿时长,再触发下一句播报 DispatchQueue.main.asyncAfter(deadline: .now() + pauseBetweenLines) { [weak self] in self?.speakNext() } } }
上层调用逻辑简化
不需要再手动写循环遍历,直接把待播报的内容数组传给Speaker即可:
let speaker = Speaker() let capitals = ["Canberra is the capital of Australia", "Seoul is the capital of South Korea", "Tokyo is the capital of Japan", "Berlin is the capital of Germany"] var body: some View { Button("Play Sound") { speaker.startSpeaking(lines: capitals) } }
方案说明:
- 全程不存在循环提前跑完的问题,整个流程完全靠状态回调推进,不会一次性把所有语音任务提交给合成器
- 停顿间隔从单句播报完全结束后才开始计时,不会出现语音还在播放、停顿已经过完的错位问题
- 如果需要中途停止播报,只需在Speaker中增加方法,将
pendingLines队列清空、同时调用synth.stopSpeaking(at: .immediate)终止当前播放即可- 原代码中设置的20秒Timer延迟过长,常规场景下句间停1-2秒的听感会更自然
内容的提问来源于stack exchange,提问作者MikeMaus
相关产品推荐
相关产品推荐

