You need to enable JavaScript to run this app. 控制循环执行:AVSpeechSynthesizer多语句顺序播报时序问题
优惠活动
大模型
产品
解决方案
定价
更多

控制循环执行: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.11.05 03:54:08