WWDC Quick Look 💓 By SwiftGGTeam
Extend Speech Synthesis with personal and custom voices

Extend Speech Synthesis with personal and custom voices

观看原视频

Highlight

iOS 17 和 macOS Sonoma 的 Speech Synthesis 框架新增三项能力:SSML 支持让语音合成更富表现力、Speech Synthesis Provider 允许开发者将自定义语音引擎以 Audio Unit Extension 形式接入系统、Personal Voice 让用户用自己的声音生成合成语音用于辅助交流。

核心内容

很多人依赖语音合成与设备交互。对这部分用户来说,合成语音的选择是很私人的事情。Apple 系统已经提供了丰富的语音选项,但第三方开发者和有特殊需求的用户需要更多。

SSML(Speech Synthesis Markup Language)是 W3C 标准,用 XML 描述语音属性。以前 AVSpeechSynthesizer 只接受纯文本,无法控制停顿、语速、音高等细节。现在可以直接传入 SSML,让合成语音更自然、更富表现力。

Speech Synthesis Provider 是今年最大的新能力。它基于 Audio Unit Extension 架构,允许开发者把自己的语音合成引擎打包成 Extension,接入系统语音列表。用户可以在系统设置中选择你的语音,VoiceOver、Siri、甚至第三方 App 都能使用。

Personal Voice 是一项辅助功能。用户录制自己的声音后,设备在本地生成个性化的合成语音。这对渐冻症等可能影响发声能力的用户意义重大——他们可以用自己的声音与外界交流。

详细内容

SSML 语音标记

02:10)SSML 用 XML 标签控制语音属性。<break> 插入停顿,<prosody> 控制语速和音高:

<speak>
    Hello
    <break time="1s"/>
    <prosody rate="200%">nice to meet you!</prosody>
</speak>

02:29)将 SSML 传入 AVSpeechUtterance

let ssml = """
    <speak>
        Hello
        <break time="1s" />
        <prosody rate="200%">nice to meet you!</prosody>
    </speak>
"""

guard let ssmlUtterance = AVSpeechUtterance(ssmlRepresentation: ssml) else {
    return
}

self.synthesizer.speak(ssmlUtterance)

关键点:

  • AVSpeechUtterance(ssmlRepresentation:) 是新的初始化方法,接受 SSML 字符串
  • 返回值为 Optional,SSML 格式错误时返回 nil
  • SSML 是 WebSpeech 和苹果第一方合成器的标准输入格式
  • 支持的标签包括 breakprosody 等,具体范围参考 W3C 规范

Speech Synthesis Provider 架构

03:21)Provider 基于 Audio Unit Extension,运行在独立的 Extension 进程中。系统负责音频会话管理和播放,Extension 只需将 SSML 渲染为音频。

创建项目时选择 Audio Unit Extension,类型选”Speech Synthesizer”,提供四字符的 subtype 和 manufacturer 标识符。

宿主 App 负责展示可用语音列表和购买流程:

struct ContentView: View {
    var body: some View {
        List {
            Section("My Awesome Voices") {
                ForEach(availableVoices) { voice in
                    HStack {
                        Text(voice.name)
                        Spacer()
                        Button("Buy") {
                            // Buy this voice...
                        }
                    }
                }
            }
        }
    }

    var availableVoices: [WWDCVoice] {
        return [
            WWDCVoice(name: "Screen Reader Voice", id: "com.example.screen-reader-voice"),
            WWDCVoice(name: "Reading Voice", id: "com.example.reading-voice")
        ]
    }
}

关键点:

  • WWDCVoice 是示例中的自定义结构体,存储语音名称和标识符
  • 宿主 App 和 Extension 通过 App Group 共享 UserDefaults 来同步已购买的语音列表
  • 购买完成后必须调用 AVSpeechSynthesisProviderVoice.updateSpeechVoices() 通知系统刷新语音列表

05:13)购买语音后更新系统语音列表:

func purchase(voice: WWDCVoice) {
    purchasedVoices.append(voice)
    
    // 写入共享 UserDefaults
    updatePurchasedVoices()
    
    // 通知系统语音列表已变更
    AVSpeechSynthesisProviderVoice.updateSpeechVoices()
}

06:25)监听系统语音列表变化:

.onReceive(NotificationCenter.default
    .publisher(for: AVSpeechSynthesizer.availableVoicesDidChangeNotification)) { _ in
        systemVoices = AVSpeechSynthesisVoice.speechVoices()
}

实现 Audio Unit Extension

06:53)Extension 的核心是继承 AVSpeechSynthesisProviderAudioUnit,实现四个关键部分。

声明可用语音:

public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit {
    public override var speechVoices: [AVSpeechSynthesisProviderVoice] {
        get {
            let voices: [String : String] = groupDefaults.value(forKey: "voices") as? [String : String] ?? [:]
            return voices.map { key, value in
                return AVSpeechSynthesisProviderVoice(
                    name: value,
                    identifier: key,
                    primaryLanguages: ["en-US"],
                    supportedLanguages: ["en-US"]
                )
            }
        }
    }
}

关键点:

  • speechVoices 返回 AVSpeechSynthesisProviderVoice 数组
  • 从 App Group 的 UserDefaults 读取已购买语音列表
  • 每个语音需要指定名称、标识符、主语言和支持语言

接收合成请求:

public override func synthesizeSpeechRequest(speechRequest: AVSpeechSynthesisProviderRequest) {
    currentBuffer = getAudioBuffer(for: speechRequest.voice, with: speechRequest.ssmlRepresentation)
    framePosition = 0
}

关键点:

  • synthesizeSpeechRequest 收到系统发来的合成请求
  • speechRequest.ssmlRepresentation 包含 SSML 文本
  • speechRequest.voice 指定使用哪个语音
  • 需要生成音频缓冲区并记录当前帧位置

取消请求:

public override func cancelSpeechRequest() {
    currentBuffer = nil
}

渲染音频帧:

public override var internalRenderBlock: AUInternalRenderBlock {
    return { [weak self]
        actionFlags, timestamp, frameCount, outputBusNumber, outputAudioBufferList, _, _ in
        guard let self else { return kAudio_ParamError }

        var unsafeBuffer = UnsafeMutableAudioBufferListPointer(outputAudioBufferList)[0]
        let frames = unsafeBuffer.mData!.assumingMemoryBound(to: Float32.self)
             
        var sourceBuffer = UnsafeMutableAudioBufferListPointer(self.currentBuffer!.mutableAudioBufferList)[0]
        let sourceFrames = sourceBuffer.mData!.assumingMemoryBound(to: Float32.self)

        for frame in 0..<frameCount {
            if frames.count > frame && sourceFrames.count > self.framePosition {
                frames[Int(frame)] = sourceFrames[Int(self.framePosition)]
                self.framePosition += 1
                if self.framePosition >= self.currentBuffer!.frameLength {
                    break
                }
            }
        }
             
        return noErr
    }
}

关键点:

  • internalRenderBlock 被系统周期性调用,要求填充指定数量的音频帧
  • 将之前生成的音频缓冲区数据复制到输出缓冲区
  • 所有帧渲染完成后,设置 actionFlagsofflineUnitRenderAction_Complete 通知系统

Personal Voice

11:10)Personal Voice 让用户录制自己的声音,设备在本地生成合成语音。使用前需要请求授权:

func fetchPersonalVoices() async {
    AVSpeechSynthesizer.requestPersonalVoiceAuthorization() { status in
        if status == .authorized {
            personalVoices = AVSpeechSynthesisVoice.speechVoices().filter {
                $0.voiceTraits.contains(.isPersonalVoice)
            }
        }
    }
}

11:34)使用 Personal Voice 合成语音:

func speakUtterance(string: String) {
    let utterance = AVSpeechUtterance(string: string)
    if let voice = personalVoices.first {
        utterance.voice = voice
        synthesizer.speak(utterance)
    }
}

关键点:

  • requestPersonalVoiceAuthorization 请求用户授权访问 Personal Voice
  • 授权后,Personal Voice 会出现在 AVSpeechSynthesisVoice.speechVoices()
  • 通过 voiceTraits.contains(.isPersonalVoice) 筛选出 Personal Voice
  • Personal Voice 主要在辅助交流(AAC)App 中使用,属于敏感权限

核心启发

  1. 做一个支持 SSML 的朗读 App

    • 在文本朗读功能中支持 SSML 标记,让用户可以控制停顿、语速、音高
    • 为什么值得做:SSML 让机器朗读更接近真人表达,长文阅读体验大幅提升
    • 怎么开始:在文本编辑器中加入 SSML 标签插入按钮,用 AVSpeechUtterance(ssmlRepresentation:) 合成
  2. 将自有 TTS 引擎接入 iOS 系统

    • 如果你有自研的语音合成技术,通过 Speech Synthesis Provider 做成 Audio Unit Extension,让系统所有语音功能都能使用
    • 为什么值得做:突破系统语音的限制,让用户在 VoiceOver、Siri 等场景使用你的语音
    • 怎么开始:创建 Audio Unit Extension 项目,类型选 Speech Synthesizer,实现 AVSpeechSynthesisProviderAudioUnit 的四个核心方法
  3. 为 AAC 辅助交流 App 集成 Personal Voice

    • 在辅助交流 App 中让用户选择自己的 Personal Voice 作为合成语音
    • 为什么值得做:对可能失去发声能力的用户,用自己的声音交流有巨大的心理价值
    • 怎么开始:调用 requestPersonalVoiceAuthorization 获取授权,筛选 isPersonalVoice 语音,传入 AVSpeechUtterance

关联 Session

评论

GitHub Issues · utterances