返回

AVSpeechSynthesizer 语音合成暂停控制方案详解

IOS

AVSpeechSynthesizer 语音合成中的暂停控制

AVSpeechSynthesizer 为 iOS, macOS, watchOS 和 tvOS 等 Apple 平台提供了文本转语音的功能。开发者经常需要对合成过程进行细粒度控制,比如在特定位置暂停朗读,然后在稍后恢复。本篇文章探讨如何实现这一需求。

难题分析:缺乏直接的暂停机制

AVSpeechSynthesizer 的设计没有直接提供按字符串索引或时间戳暂停的方法。pauseSpeaking(at: .immediate) 方法只会立即暂停,并没有与字符串位置相关的接口,continueSpeaking() 方法只能恢复播放,同样缺少精细控制。这带来了以下两个难题:

  • 定位暂停点困难 : 如何精准确定字符串中的暂停位置?
  • 同步延迟与恢复 : 如何在指定延迟后,无缝恢复语音合成?

解决这些难题需要另辟蹊径。

解决方案一:分割文本,逐段合成

此方案的核心是将文本分解成更小的片段,利用 AVSpeechSynthesizer 完成分段合成。每个片段的结尾即为暂停点。这样可以通过控制每段的开始和延迟来实现暂停与恢复的效果。

原理:

将需要朗读的文本按照预期暂停点进行切割,例如按句子、段落或特定的索引位置。随后,逐一合成这些片段,在每个片段完成之后,设置定时器,等待延迟后再开始下一个片段。通过这种方法实现类似暂停的效果。

步骤:

  1. 文本分割: 依据需求,对文本进行分割。
  2. 合成处理: 创建 AVSpeechSynthesizer 对象。
  3. 分段播放: 遍历分割的文本片段。每合成完一段,设置定时器或使用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来监听每一个utterancedidFinish 事件。didFinish后在计时,确保每段话都有缓冲时间播放完成。
  • 要处理取消合成时的逻辑,例如快速切换文本时需要停止旧合成任务,确保不会造成混乱。
  • 此解决方案对文本分割逻辑有一定的要求,务必测试不同的暂停点位置,避免语音被打断,或出现逻辑上的异常。
  • 为了避免过快或过慢的音频播放速度,需要在创建 AVSpeechUtterance 时正确配置播放速率参数。

解决方案二: 利用Delegate和标志位

此方案利用 AVSpeechSynthesizerDelegate 协议中的代理方法 speechSynthesizer(_:willSpeakRangeOf:utterance:) 来进行精准控制,可以根据当前的朗读范围,在特定位置插入暂停。

原理:

该方法提供正在朗读的字符串范围信息。我们可以结合事先确定的暂停位置,在 willSpeakRangeOf 方法中判断,如果到达暂停点,调用 pauseSpeaking 暂停语音合成,并在一定延迟之后恢复。

步骤:

  1. 记录暂停点 : 在开始朗读前,记录预期的暂停点索引。
  2. 代理回调:speechSynthesizer(_:willSpeakRangeOf:utterance:) 方法中检查当前的 range.location
  3. 条件暂停 : 如果到达暂停点,暂停合成,并设置延迟定时器。
  4. 恢复 : 定时器结束时,调用 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 进行暂停控制需要根据具体的应用场景,采取不同的策略。对于需要精准控制暂停位置的场景,可以考虑对文本进行分割,然后逐段合成,或借助代理方法精确定位暂停。选择何种方案取决于所需的控制精度和实现的复杂度,切记安全性和异常情况处理不可忽视。