大多数浏览器和
Developer App 均支持流媒体播放。
-
使用个人和自定义声音扩展语音合成
将语音合成的最新进展引入你的 App。了解如何将自定义语音合成器和声音集成到 iOS 和 macOS 中。我们将向你展示如何使用 SSML 生成富有表现力的语音合成,并探索个人语音如何使你的增强和辅助通信 App 能够以真实的方式代表一个人说话。
章节
- 0:00 - Welcome
- 1:25 - Explore SSML
- 2:37 - Implement a synthesis provider
- 10:01 - Use Personal Voice
资源
- Audio Unit
- Creating an audio unit extension
- Speech synthesis
- Speech Synthesis Markup Language (SSML)
相关视频
WWDC20
-
下载
♪ ♪
Grant:大家好 我是 Grant 一名 Accessibility 团队的工程师 许多用户在 Apple 平台上 使用语音合成 并且有些用户依赖于合成器声音 这些声音是他们使用设备的窗口 因此 声音选择是一个 非常个性化的选择 使用 iOS 中语音合成的用户 现在有多种声音选择 接下来 我们来聊一聊 如何为用户提供更多的选择 首先 我们会介绍 语音合成标记语言是什么 该语言如何为你的自定义声音 带来沉浸式语音输出 以及为什么你的语音提供器应该 使用该语言 接着 我们会演示 如何实现语音合成提供器 来为设备提供合成器和声音体验 最后 我们会深入了解个人声音 这是一项全新的功能 用户可以录制自己的声音 然后根据这些录音生成合成语音 所以 你可以使用用户自己的 个人声音来合成语音 首先 我们来介绍一下 SSML SSML 是用于表示口头文本的 W3C 标准 SSML 语音通过使用 带有各种标签和属性的 XML 格式以声明的方式进行表示 你可以使用这些标签来控制 速率和音高等语音属性 SSML 可用于第一方合成器 其中包括 WebKit 中的 WebSpeech 等 语音合成器的标准输入 接下来 我们来了解一下 如何使用 SSML 以这句带有停顿的短语为例 我们可以在 SSML 中 表示这种停顿 首先 我们从“Hello”字符串开始 使用 SSML 的 break 标记 添加 1 秒的停顿 然后使用加速语句 “nice to meet you!”作为结束 为了实现这个目的 我们添加 SSML 的 prosody 标记 并将速率属性设置为 200% 现在 我们就可以使用该 SSML 创建 一个可与之交流的 AVSpeechUtterance 接着 我们来了解一下 如何实现自己的语音合成器声音
那么 什么是语音合成器呢? 语音合成器能够以 SSML 形式 接收文件 以及与所需语音属性相关的信息 并且能够提供该文本的音频表示 假设你有一个使用新声音的合成器 并且你希望将其引入 iOS、 macOS 和 iPadOS 那么语音合成提供器就可以帮助你 在我们的平台上实现 自己的语音合成器 来为用户提供系统声音之外的 更多个性化选择
接下来 我们来了解一下 语音合成提供器的工作原理 语音合成提供器的 Audio Unit 扩展会 嵌入到主机的 App 中 并以 SSML 的形式接收语音请求 该扩展负责 为 SSML 输入渲染音频 并选择性地返回用于指示单词 在音频缓冲区所处位置的标记 然后 系统就会管理 该语音请求的所有播放 你无需处理任何音频会话管理 语音合成提供器框架 会在内部对其进行管理 在了解合成器是什么后 现在我们就可以开始 编译语音合成器扩展 首先 我们在 Xcode 中创建 一个新的 Audio Unit 扩展 App 项目 接着选择“语音合成器” Audio Unit 类型 并为合成器提供 四个字符的子类型标识符 同时为你 也就是制作者 提供四个字符的子类型标识符 Audio Unit 扩展是用于 编译语音合成器扩展的核心架构 该扩展可以让合成器 在扩展进程中运行 而不是在主机 App 进程中运行
接下来 我们的 App 会提供 一个简单的界面 用于购买和选择 该扩展可为其合成语音的声音 首先 我们会创建一个列表视图 用于显示可供购买的声音 并且 每个语音单元格都会显示 声音的名称以及购买按钮
接着我会用一些声音来填充该列表 这里 简单的 WWDCVoice 结构 包含了声音名称以及标识符
同时 我们还需要一个状态变量 来跟踪购买的声音 以及一个新的部分来展示这些信息 接着我们为购买声音创建一个函数 在这里 我们将新购买的声音 添加到列表 并相应地更新 UI 你需要注意 AVSpeechSynthesisProviderVoice 中的 updateSpeechVoices 方法 该方法可以让 App 发出信号以表明合成器使用的 声音集合已经发生更改 并且系统声音列表需要进行重建 在本示例中 我们会在 完成声音的 App 内购买项目后 调用该方法 同时 我们需要一种方法来跟踪 语音合成器扩展中可用的声音 为了实现这一点 我们可以创建一个 UserDefaults 实例 并将其通过 App 小组进行共享 App 小组可以让我们在 主机 App 和扩展之间 共享该声音列表 并且 我们在创建 App 小组时 需要清晰指明我们提供的套件名称 以确保主机 App 和扩展 会从同一个域中进行读取 我们来回顾一下 purchase 函数 我已经实现了一个方法 用于在新声音被购买时 更新用户默认设置 AVSpeechSynthesizer 也推出了一个新的 API 用于监听可用系统声音发生的更改 在用户删除或下载声音时 系统声音集合会发生更改 你可以订阅 availableVoicesDidChangeNotification 来根据这些更改更新声音列表 现在 我们的主机 App 就完成了 接下来 我们来填写 Audio Unit 该单元由 4 个关键组件构成
首先 我们需要添加一种方法来告知 系统合成器将会提供什么声音 为了实现这一点 我们可以重写 speechVoices 获取器 来提供声音列表 并从此前指定的 App 小组 UserDefaults 域中进行读取 对于声音列表中的每个项目 我们都会构建一个美式英语的 AVSpeechSynthesisProviderVoice 接着我们需要一种方法来告知合成器 需要合成什么文本 在系统向扩展发出信号 来告知其开始合成文本时 我们就会调用 SynthesizeSpeechRequest 方法 该方法的参数是一个 AVSpeechSynthesisProviderRequest 实例 包含了 SSML 以及 说话使用的声音 然后我会调用此前在语音引擎实现中 创建的辅助方法 在本示例中 getAudioBuffer 方法 会根据请求和 SSML 输入中 指定的声音生成音频数据 并且 我们还会将 framePosition 实例变量 设置为 0 来跟踪渲染的帧数 因为我们在调用渲染块时 会从缓冲区中复制帧 该系统还需要一种方法 来向合成器发出信号 以停止合成音频 并丢弃当前的语音请求 cancelSpeechRequest 就可以实现这一点 并且 我们可以在其中 只丢弃当前缓冲区 最后 我们需要实现渲染块 系统会使用所需的 frameCount 来调用渲染块 接着 Audio Unit 就会负责 在 outputAudioBuffer 中 填充请求的帧数 接下来我们会引用目标缓冲区 以及此前在调用 synthesizeSpeechRequest 时 生成并存储的缓冲区 来对自己进行设置 然后我们将帧复制到目标缓冲区中 最后在 Audio Unit 使用完 当前语音请求的所有缓冲区后 我们会将 actionFlags 的参数设置为 offlineUnitRenderAction_Complete 来向系统发出信号 告知其渲染已经完成 没有其他音频缓冲区需要渲染了 我们来看看实际效果如何! 这里展示的是语音合成器 App 接下来 我会购买语音并导航到使用 新声音和语音引擎合成语音的视图 首先 我向合成器输入“Hello”
合成声音:Hello Grant:接着 我输入“Goodbye”
合成声音:Goodbye Grant:至此 我们已经实现了 一个合成提供器 并创建了一个主机 App 来提供用于整个系统的声音 包括从旁白到你的 App! 我们十分期待看到你使用 这些 API 创造出新的声音 以及设计出从文本到语音的体验 接下来 我们来了解一下 个人声音这个新功能 当前 用户可以使用设备的功能 在 iOS 和 macOS 上 录制和重新创建自己的声音 个人语音是在设备上 而不是在服务器上生成的 该声音会出现在其他系统声音中 并可与新功能 Live Speech 结合使用 Live Speech 是一种可用于 iOS、iPadOS、 macOS 和 watchOS 的打字朗读功能 能让用户即时 用自己的声音来合成语音 你可以使用新的请求授权 API 来向个人声音 请求授权使用这些声音来合成语音 你需要记住 使用个人声音是非常敏感的 所以 你应该将其主要用于 增强的或可替代的通信 App 接下来 我们来看看我开发的一款 使用个人声音的 AAC App 该 App 有两个按钮 可用于说出我在 WWDC 常说的短语 以及请求访问使用个人声音 我在 AVSpeechSynthesizer 上调用了新的 API requestPersonalVoiceAuthorization 来用于请求授权 得到授权后 个人声音就会和 系统声音一起出现在 AVSpeechSynthesisVoice API 的 speechVoices 中 并使用名为 isPersonalVoice 的 新 voiceTrait 来表示
现在 我就可以访问个人语音 并使用该语音说话了
接下来 我们来看看 个人声音实际运行的演示 首先 我轻点“使用个人声音”按钮 来请求授权 在得到授权后 我轻点符号来录制我的声音 个人声音:大家好 我是 Grant 欢迎来到 WWDC23 Grant:很神奇不是吗? 现在 你也可以在自己的 App 中 使用这些声音
至此 我们已经讨论了 SSML 你可以使用该语言标准化语音输入 并在 App 中创造丰富的语音体验 我们还介绍了如何在 Apple 平台上 实现语音合成 以便你可以提供优质的新语音声音 以供用户在系统中使用 最后 你还可以使用个人声音 来为 App 带来更加个性化的合成 并且 这一点尤其适合于 有可能会失去声音的用户 我们十分期待看到你 使用这些 API 创造的体验 感谢你的观看
-
-
2:10 - SSML phrase
<speak> Hello <break time="1s"/> <prosody rate="200%">nice to meet you!</prosody> </speak>
-
2:29 - SSML utterance
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)
-
4:33 - Create a host 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") ] } }
-
5:04 - Keep track of purchased voices
struct ContentView: View { @State var purchasedVoices: [WWDCVoice] = [] var body: some View { NavigationStack { List { MyAwesomeVoicesSection Section("Purchased Voices") { ForEach(purchasedVoices) { voice in NavigationLink { // Destination View } label: { Text(voice.name) } } } } } } }
-
5:13 - Inform the system when available voices change
struct ContentView: View { @State var purchasedVoices: [WWDCVoice] = [] var body: some View { List { MyAwesomeVoicesSection PurchasedVoicesSection } } func purchase(voice: WWDCVoice) { // Append voice to list of purchased voices purchasedVoices.append(voice) // Inform system of change in voices AVSpeechSynthesisProviderVoice.updateSpeechVoices() } }
-
5:39 - Update UI with purchased voices
struct ContentView: View { @State var purchasedVoices: [WWDCVoice] = [] var body: some View { List { Section("My Awesome Voices") { ForEach(availableVoices.filter { !purchasedVoices.contains($0) }) { voice in HStack { Text(voice.name) Spacer() Button("Buy") { purchase(voice: voice) } } } } PurchasedVoicesSection } } }
-
5:46 - Save available voices into UserDefaults
struct ContentView: View { let groupDefaults = UserDefaults(suiteName: "group.com.example.SpeechSynthesizerApp")! @State var purchasedVoices: [WWDCVoice] = [] var body: some View { List { MyAwesomeVoicesSection PurchasedVoicesSection } } func purchase(voice: WWDCVoice) { // Append voice to list of purchased voices purchasedVoices.append(voice) // Write purchasedVoices to defaults updatePurchasedVoices() // Inform system of change in voices AVSpeechSynthesisProviderVoice.updateSpeechVoices() } }
-
6:25 - Monitor for system voice changes
struct ContentView: View { @State var systemVoices: [AVSpeechSynthesisVoice] = AVSpeechSynthesisVoice.speechVoices() var body: some View { List { MyAwesomeVoicesSection PurchasedVoicesSection Section("System Voices") { ForEach(systemVoices.filter { $0.language == "en-US" }) { voice in Text(voice.name) } } } .onReceive(NotificationCenter.default .publisher(for: AVSpeechSynthesizer.availableVoicesDidChangeNotification)) { _ in systemVoices = AVSpeechSynthesisVoice.speechVoices() } } }
-
6:53 - Override speechVoices getter
// Implement a synthesis provider public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit { public override var speechVoices: [AVSpeechSynthesisProviderVoice] { get { } } }
-
7:02 - Use UserDefaults to provide set of available voices
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"] ) } } } }
-
7:22 - Use your synthesis engine on each synthesis request
public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit { public override func synthesizeSpeechRequest(speechRequest: AVSpeechSynthesisProviderRequest) { currentBuffer = getAudioBuffer(for: speechRequest.voice, with: speechRequest.ssmlRepresentation) framePosition = 0 } }
-
8:14 - Handle request cancellation
public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit { public override func synthesizeSpeechRequest(speechRequest: AVSpeechSynthesisProviderRequest) { currentBuffer = getAudioBuffer(for: speechRequest.voice, with: speechRequest.ssmlRepresentation) framePosition = 0 } public override func cancelSpeechRequest() { currentBuffer = nil } }
-
8:28 - Override internalRenderBlock
public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit { public override var internalRenderBlock: AUInternalRenderBlock { return { [weak self] actionFlags, timestamp, frameCount, outputBusNumber, outputAudioBufferList, _, _ in guard let self else { return kAudio_ParamError } return noErr } } }
-
8:42 - Implement the render block
public class WWDCSynthAudioUnit: AVSpeechSynthesisProviderAudioUnit { public override var internalRenderBlock: AUInternalRenderBlock { return { [weak self] actionFlags, timestamp, frameCount, outputBusNumber, outputAudioBufferList, _, _ in guard let self else { return kAudio_ParamError } // This is the audio buffer we are going to fill up 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 } } }
-
11:10 - Request authorization for Personal Voice
struct ContentView: View { @State private var personalVoices: [AVSpeechSynthesisVoice] = [] func fetchPersonalVoices() async { AVSpeechSynthesizer.requestPersonalVoiceAuthorization() { status in if status == .authorized { personalVoices = AVSpeechSynthesisVoice.speechVoices().filter { $0.voiceTraits.contains(.isPersonalVoice) } } } } }
-
11:34 - Use Personal Voice
func speakUtterance(string: String) { let utterance = AVSpeechUtterance(string: string) if let voice = personalVoices.first { utterance.voice = voice syntheizer.speak(utterance) } }
-
-
正在查找特定内容?在上方输入一个主题,就能直接跳转到相应的精彩内容。
提交你查询的内容时出现错误。请检查互联网连接,然后再试一次。