Create a seamless speech experience in your apps
Accessibility 进阶 22m

在 App 中创建无缝的语音体验

Create a seamless speech experience in your apps

2020年6月23日

在 Apple 官方观看视频

一句话判断

如果你的 App 有语音输入或文本转语音功能,这场 Session 讲了如何避免那些让用户抓狂的体验断裂——比如正在说话时键盘突然收起、TTS 还没播完页面就跳转了。

这场 Session 讲了什么

Session 聚焦于 App 中语音交互的连贯性体验,涵盖两个方面:语音识别(Speech Recognition)和文本转语音(Text to Speech)。

在语音识别部分,SFSpeechRecognizer 的使用已经不新鲜,但 Session 关注的是 UX 层面的问题:当用户正在语音输入时,如何处理系统弹出的权限对话框、如何在不中断语音的情况下更新 UI、以及如何给用户清晰的语音输入反馈。

在 TTS 部分介绍了 AVSpeechSynthesizer 的新特性。iOS 14 中 Personal Voice(个人声音)的引入让 TTS 更自然。Session 重点讲了如何正确管理 TTS 的生命周期——确保语音播报不会被页面切换打断、如何处理多段文本的队列播报、以及如何在语音播报和 UI 更新之间做好协调。

值得深挖的点

语音输入与 UI 更新的同步问题。 语音识别的回调频率很高(几乎每个音节都会触发),如果在回调中做大量 UI 更新会导致界面卡顿。Session 建议使用节流(Throttle)策略——比如每 0.3 秒更新一次文本显示,而不是每次回调都刷新。

AVSpeechSynthesizer 的队列管理。 当你有多段文本需要连续播报时,不要为每段创建新的 Utterance 并等待完成再播下一段。应该把所有 Utterance 放入队列,AVSpeechSynthesizer 会自动按顺序播放,且段之间的切换更流畅。

代码片段

import Speech
import AVFoundation

// 语音识别的节流更新策略
class SpeechInputManager: ObservableObject {
    @Published var recognizedText: String = ""
    private var lastUpdateTime: Date = .distantPast
    private let updateInterval: TimeInterval = 0.3  // 每 0.3 秒更新一次 UI
    
    func startRecognition() {
        let audioEngine = AVAudioEngine()
        let speechRecognizer = SFSpeechRecognizer(locale: Locale(identifier: "zh-CN"))!
        let request = SFSpeechAudioBufferRecognitionRequest()
        
        let inputNode = audioEngine.inputNode
        let format = inputNode.outputFormat(forBus: 0)
        inputNode.installTap(onBus: 0, bufferSize: 1024, format: format) { buffer, _ in
            request.append(buffer)
        }
        
        speechRecognizer.recognitionTask(with: request) { [weak self] result, error in
            guard let self = self, let result = result else { return }
            
            // 节流:只在间隔超过阈值时更新 UI
            let now = Date()
            guard now.timeIntervalSince(self.lastUpdateTime) >= self.updateInterval else { return }
            self.lastUpdateTime = now
            self.recognizedText = result.bestTranscription.formattedString
        }
    }
}
// TTS 队列播报——多段文本连续播放
let synthesizer = AVSpeechSynthesizer()

func announceMessages(_ messages: [String]) {
    // 将所有文本加入队列,synthesizer 自动按序播放
    for message in messages {
        let utterance = AVSpeechUtterance(string: message)
        utterance.rate = AVSpeechUtteranceDefaultSpeechRate
        utterance.voice = AVSpeechSynthesisVoice(language: "zh-CN")
        synthesizer.speak(utterance)  // 自动排队
    }
}

// 检查是否正在播放,避免打断
func navigateToNewPage() {
    if synthesizer.isSpeaking {
        // 先完成当前播报或停止后再跳转
        synthesizer.stopSpeaking(at: .immediate)
    }
    // 执行页面跳转
}

最佳实践

  • 语音识别回调中做节流。 不要在每次 partial result 回调中都更新 UI,使用 0.2-0.3 秒的节流间隔。
  • TTS 播报与导航冲突时要优雅处理。 页面跳转前检查 isSpeaking,要么等播完再跳转,要么停止播报后跳转——不要让声音播到一半突然消失。
  • 尊重用户的静音开关。 使用 AVAudioSession 的 ambient 类别,这样用户拨动静音开关时 TTS 也会静音(除非是必要的无障碍播报)。
  • 给语音输入提供实时视觉反馈。 显示正在录音的动画指示器,让用户知道系统在听。

还有什么值得关注

  • iOS 14 的 VoiceOver 支持更细粒度的播报控制
  • 新的 AVSpeechSynthesisVoiceQuality 枚举可以区分高质量和紧凑质量的语音
  • 语音识别的 on-device 模式在 2020 年有扩展,支持更多语言
  • Session 提到了辅助功能设置中的”朗读选择”功能与 AVSpeechSynthesizer 的关系
WWDC 2020