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 和苹果第一方合成器的标准输入格式
- 支持的标签包括
break、prosody等,具体范围参考 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被系统周期性调用,要求填充指定数量的音频帧- 将之前生成的音频缓冲区数据复制到输出缓冲区
- 所有帧渲染完成后,设置
actionFlags为offlineUnitRenderAction_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 中使用,属于敏感权限
核心启发
-
做一个支持 SSML 的朗读 App
- 在文本朗读功能中支持 SSML 标记,让用户可以控制停顿、语速、音高
- 为什么值得做:SSML 让机器朗读更接近真人表达,长文阅读体验大幅提升
- 怎么开始:在文本编辑器中加入 SSML 标签插入按钮,用
AVSpeechUtterance(ssmlRepresentation:)合成
-
将自有 TTS 引擎接入 iOS 系统
- 如果你有自研的语音合成技术,通过 Speech Synthesis Provider 做成 Audio Unit Extension,让系统所有语音功能都能使用
- 为什么值得做:突破系统语音的限制,让用户在 VoiceOver、Siri 等场景使用你的语音
- 怎么开始:创建 Audio Unit Extension 项目,类型选 Speech Synthesizer,实现
AVSpeechSynthesisProviderAudioUnit的四个核心方法
-
为 AAC 辅助交流 App 集成 Personal Voice
- 在辅助交流 App 中让用户选择自己的 Personal Voice 作为合成语音
- 为什么值得做:对可能失去发声能力的用户,用自己的声音交流有巨大的心理价值
- 怎么开始:调用
requestPersonalVoiceAuthorization获取授权,筛选isPersonalVoice语音,传入AVSpeechUtterance
关联 Session
- Build accessible apps with SwiftUI and UIKit — 无障碍 App 开发的最佳实践,与语音合成辅助功能直接相关
- Meet Assistive Access — 认知障碍辅助功能,与语音合成共同构成完整的无障碍生态
- Design considerations for vision and motion — 视觉和运动障碍的设计考量,与辅助交流 App 的设计原则互补
评论
GitHub Issues · utterances