你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

如何基于文本合成语音

参考文档 | 包 (NuGet) | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

选择合成语言和语音

语音服务中的文本转语音功能支持 400 多种语音和 140 多种语言和变体。 你可以获取完整列表,或在语音库中试用它们。

指定 SpeechConfig 的语言或语音,使其与输入文本匹配,并使用指定的语音。 以下代码片段显示了此技术的工作原理:

static async Task SynthesizeAudioAsync()
{
    var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
    // Set either the `SpeechSynthesisVoiceName` or `SpeechSynthesisLanguage`.
    speechConfig.SpeechSynthesisLanguage = "en-US"; 
    speechConfig.SpeechSynthesisVoiceName = "en-US-AvaMultilingualNeural";
}

所有神经网络声音都是多语言的,并且能够流利地使用自己的语言和英语。 例如,如果英语的输入文本为“I'm excited to try text to speech”并且你选择了 es-ES-ElviraNeural,则该文本将用带西班牙口音的英语讲出。

如果语音没有使用输入文本的语言讲出,则语音服务不会创建合成音频。 有关支持的神经语音的完整列表,请参阅语音服务的语言和语音支持

注意

默认语音是从语音列表 API 根据区域设置返回的第一个语音。

所讲的语音按以下优先顺序确定:

  • 如果你没有设置 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会讲 en-US 的默认语音。
  • 如果你仅设置了 SpeechSynthesisLanguage,则会讲指定区域设置的默认语音。
  • 如果同时设置了 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会忽略 SpeechSynthesisLanguage 设置。 系统会讲你使用 SpeechSynthesisVoiceName 指定的语音。
  • 如果使用语音合成标记语言 (SSML) 设置了 voice 元素,则会忽略 SpeechSynthesisVoiceNameSpeechSynthesisLanguage 设置。

将语音合成到文件中

创建一个 SpeechSynthesizer 对象。 以下片段中显示的此对象可运行文本到语音的转换,并将转换结果输出到扬声器、文件或其他输出流。 SpeechSynthesizer 接受以下对象作为参数:

  1. 创建一个 AudioConfig 实例,以使用 FromWavFileOutput() 函数自动将输出写入到 .wav 文件。 使用 using 语句将其实例化。

    static async Task SynthesizeAudioAsync()
    {
        var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
        using var audioConfig = AudioConfig.FromWavFileOutput("path/to/write/file.wav");
    }
    

    此上下文中的 using 语句会自动释放非托管资源,导致对象在释放后超出范围。

  2. 使用另一个 using 语句实例化 SpeechSynthesizer 实例。 将 speechConfig 对象和 audioConfig 对象作为参数传递。 若要合成语音并写入文件,请使用文本字符串运行 SpeakTextAsync()

static async Task SynthesizeAudioAsync()
{
    var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
    using var audioConfig = AudioConfig.FromWavFileOutput("path/to/write/file.wav");
    using var speechSynthesizer = new SpeechSynthesizer(speechConfig, audioConfig);
    await speechSynthesizer.SpeakTextAsync("I'm excited to try text to speech");
}

运行程序时,它会创建一个合成的 .wav 文件,该文件将写入到你指定的位置。 此结果是最基本用法的一个很好示例。 接下来,可以自定义输出,并将输出响应作为适用于自定义方案的内存中流进行处理。

合成到扬声器输出

若要将合成语音输出到当前活动输出设备(例如扬声器),请在创建 SpeechSynthesizer 实例时省略 AudioConfig 参数。 下面是一个示例:

static async Task SynthesizeAudioAsync()
{
    var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
    using var speechSynthesizer = new SpeechSynthesizer(speechConfig);
    await speechSynthesizer.SpeakTextAsync("I'm excited to try text to speech");
}

获取内存中流形式的结果

可将生成的音频数据用作内存中流,而不是直接写入到文件。 使用内存中流可以构建自定义行为:

  • 抽取生成的字节数组,作为自定义下游服务的可搜寻流。
  • 将结果与其他 API 或服务集成。
  • 修改音频数据,编写自定义 .wav 标头,并执行相关任务。

可以对上一个示例进行此更改。 首先删除 AudioConfig 块,因为从现在起,你将手动管理输出行为,以提高控制度。 在 SpeechSynthesizer 构造函数中为 AudioConfig 传递 null

注意

如果为 AudioConfig 传递 null,而不是像在前面的扬声器输出示例中那样省略它,则默认不会在当前处于活动状态的输出设备上播放音频。

将结果保存到 SpeechSynthesisResult 变量。 AudioData 属性包含输出数据的 byte [] 实例。 可以手动使用此 byte [] 实例,也可以使用 AudioDataStream 类来管理内存中流。

此示例使用 AudioDataStream.FromResult() 静态函数从结果中获取流:

static async Task SynthesizeAudioAsync()
{
    var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
    using var speechSynthesizer = new SpeechSynthesizer(speechConfig, null);

    var result = await speechSynthesizer.SpeakTextAsync("I'm excited to try text to speech");
    using var stream = AudioDataStream.FromResult(result);
}

此时,可以使用生成的 stream 对象来实现任何自定义行为。

自定义音频格式

你可以自定义音频输出属性,包括:

  • 音频文件类型
  • 采样速率
  • 位深度

若要更改音频格式,请对 SpeechConfig 对象使用 SetSpeechSynthesisOutputFormat() 函数。 此函数需要一个 SpeechSynthesisOutputFormat 类型的 enum 实例。 使用 enum 选择输出格式。 有关可用格式,请参阅音频格式列表

可根据要求对不同的文件类型使用不同的选项。 根据定义,Raw24Khz16BitMonoPcm 等原始格式不包括音频标头。 仅在以下任一情况下使用原始格式:

  • 你知道下游实现可以解码原始位流。
  • 你打算基于位深度、采样率、通道数等因素手动生成标头。

此示例通过对 SpeechConfig 对象设置 SpeechSynthesisOutputFormat 来指定高保真 RIFF 格式 Riff24Khz16BitMonoPcm。 类似于上一部分中的示例,可以使用 AudioDataStream 获取结果的内存中流,然后将其写入文件。

static async Task SynthesizeAudioAsync()
{
    var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
    speechConfig.SetSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm);

    using var speechSynthesizer = new SpeechSynthesizer(speechConfig, null);
    var result = await speechSynthesizer.SpeakTextAsync("I'm excited to try text to speech");

    using var stream = AudioDataStream.FromResult(result);
    await stream.SaveToWaveFileAsync("path/to/write/file.wav");
}

运行程序时,它会将 .wav 文件写入指定路径。

使用 SSML 自定义语音特征

借助 SSML,可以通过从 XML 架构中提交请求,来微调文本转语音输出的音节、发音、语速、音量和其他方面。 本部分展示了更改语音的示例。 有关详细信息,请参阅语音合成标记语言概述

若要开始使用 SSML 进行自定义,需要进行一个小的更改来切换语音。

  1. 在根项目目录中为 SSML 配置创建一个新的 XML 文件。

    <speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="en-US">
      <voice name="en-US-AvaMultilingualNeural">
        When you're on the freeway, it's a good idea to use a GPS.
      </voice>
    </speak>
    

    在此示例中,文件为 ssml.xml。 根元素始终是 <speak>。 通过将文本包装在 <voice> 元素中,可以使用 name 参数来更改语音。 有关支持的神经语音的完整列表,请参阅支持的语言

  2. 更改语音合成请求以引用 XML 文件。 该请求基本上保持不变,只不过需要使用 SpeakSsmlAsync() 而不是 SpeakTextAsync() 函数。 此函数需要 XML 字符串。 首先,使用 File.ReadAllText() 将 SSML 配置加载为字符串。 此时,结果对象与前面的示例完全相同。

    注意

    如果使用的是 Visual Studio,则默认情况下,生成配置可能不会查找 XML 文件。 右键单击 XML 文件并选择“属性”。 将“生成操作”更改为“内容”。 将“复制到输出目录”更改为“始终复制”。

    public static async Task SynthesizeAudioAsync()
    {
        var speechConfig = SpeechConfig.FromSubscription("YourSpeechKey", "YourSpeechRegion");
        using var speechSynthesizer = new SpeechSynthesizer(speechConfig, null);
    
        var ssml = File.ReadAllText("./ssml.xml");
        var result = await speechSynthesizer.SpeakSsmlAsync(ssml);
    
        using var stream = AudioDataStream.FromResult(result);
        await stream.SaveToWaveFileAsync("path/to/write/file.wav");
    }
    

注意

若要在不使用 SSML 的情况下更改语音,可使用 SpeechConfig.SpeechSynthesisVoiceName = "en-US-AvaMultilingualNeural";SpeechConfig 上设置属性。

订阅合成器事件

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

在对文本转语音使用 SpeechSynthesizer 时,可以订阅此表中的事件:

事件 说明 用例
BookmarkReached 指示已进入书签。 若要触发书签进入事件,需要在 SSML 中指定一个 bookmark 元素。 此事件会报告输出音频在合成开始处到 bookmark 元素之间经历的时间。 该事件的 Text 属性是在书签的 mark 特性中设置的字符串值。 不会说出 bookmark 元素。 可以使用 bookmark 元素在 SSML 中插入自定义标记,以获得音频流中每个标记的偏移量。 bookmark 元素可用于引用文本或标记序列中的特定位置。
SynthesisCanceled 指示已取消语音合成。 可在合成取消后进行确认。
SynthesisCompleted 指示语音合成已完成。 可在合成完成后进行确认。
SynthesisStarted 表示已启动语音合成。 可在合成启动后进行确认。
Synthesizing 指示语音合成正在进行。 每次 SDK 从语音服务收到音频区块,都会触发此事件。 你可以确认合成何时正在进行。
VisemeReceived 指示已收到嘴形视位事件。 视素通常用于表示观察到的语音中的关键姿态。 关键姿态包括在产生特定音素时嘴唇、下巴和舌头的位置。 可以使用嘴形视位来来动画显示播放语音音频时人物的面部。
WordBoundary 指示已收到字边界。 在每个新的讲述字词、标点和句子的开头引发此事件。 该事件报告当前字词从输出音频开始的时间偏移(以时钟周期为单位)。 此事件还会报告输入文本或 SSML 中紧靠在要说出的字词之前的字符位置。 此事件通常用于获取文本和相应音频的相对位置。 你可能想要知道某个新字词,然后根据时间采取操作。 例如,可以获取所需的信息来帮助确定在说出字词时,何时突出显示这些字词,以及要突出显示多长时间。

注意

事件在输出音频数据变为可用时引发,这样将会比播放到输出设备更快。 调用方必须相应地实时同步流式处理。

以下示例演示如何订阅语音合成的事件。 可以按照快速入门中的说明操作,但请将该 Program.cs 文件的内容替换为以下 C++ 代码:

using Microsoft.CognitiveServices.Speech;

class Program 
{
    // This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
    static string speechKey = Environment.GetEnvironmentVariable("SPEECH_KEY");
    static string speechRegion = Environment.GetEnvironmentVariable("SPEECH_REGION");

    async static Task Main(string[] args)
    {
        var speechConfig = SpeechConfig.FromSubscription(speechKey, speechRegion);
         
        var speechSynthesisVoiceName  = "en-US-AvaMultilingualNeural";  
        var ssml = @$"<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'>
            <voice name='{speechSynthesisVoiceName}'>
                <mstts:viseme type='redlips_front'/>
                The rainbow has seven colors: <bookmark mark='colors_list_begin'/>Red, orange, yellow, green, blue, indigo, and violet.<bookmark mark='colors_list_end'/>.
            </voice>
        </speak>";

        // Required for sentence-level WordBoundary events
        speechConfig.SetProperty(PropertyId.SpeechServiceResponse_RequestSentenceBoundary, "true");

        using (var speechSynthesizer = new SpeechSynthesizer(speechConfig))
        {
            // Subscribe to events

            speechSynthesizer.BookmarkReached += (s, e) =>
            {
                Console.WriteLine($"BookmarkReached event:" +
                    $"\r\n\tAudioOffset: {(e.AudioOffset + 5000) / 10000}ms" +
                    $"\r\n\tText: \"{e.Text}\".");
            };

            speechSynthesizer.SynthesisCanceled += (s, e) =>
            {
                Console.WriteLine("SynthesisCanceled event");
            };

            speechSynthesizer.SynthesisCompleted += (s, e) =>
            {                
                Console.WriteLine($"SynthesisCompleted event:" +
                    $"\r\n\tAudioData: {e.Result.AudioData.Length} bytes" +
                    $"\r\n\tAudioDuration: {e.Result.AudioDuration}");
            };

            speechSynthesizer.SynthesisStarted += (s, e) =>
            {
                Console.WriteLine("SynthesisStarted event");
            };

            speechSynthesizer.Synthesizing += (s, e) =>
            {
                Console.WriteLine($"Synthesizing event:" +
                    $"\r\n\tAudioData: {e.Result.AudioData.Length} bytes");
            };

            speechSynthesizer.VisemeReceived += (s, e) =>
            {
                Console.WriteLine($"VisemeReceived event:" +
                    $"\r\n\tAudioOffset: {(e.AudioOffset + 5000) / 10000}ms" +
                    $"\r\n\tVisemeId: {e.VisemeId}");
            };

            speechSynthesizer.WordBoundary += (s, e) =>
            {
                Console.WriteLine($"WordBoundary event:" +
                    // Word, Punctuation, or Sentence
                    $"\r\n\tBoundaryType: {e.BoundaryType}" +
                    $"\r\n\tAudioOffset: {(e.AudioOffset + 5000) / 10000}ms" +
                    $"\r\n\tDuration: {e.Duration}" +
                    $"\r\n\tText: \"{e.Text}\"" +
                    $"\r\n\tTextOffset: {e.TextOffset}" +
                    $"\r\n\tWordLength: {e.WordLength}");
            };

            // Synthesize the SSML
            Console.WriteLine($"SSML to synthesize: \r\n{ssml}");
            var speechSynthesisResult = await speechSynthesizer.SpeakSsmlAsync(ssml);

            // Output the results
            switch (speechSynthesisResult.Reason)
            {
                case ResultReason.SynthesizingAudioCompleted:
                    Console.WriteLine("SynthesizingAudioCompleted result");
                    break;
                case ResultReason.Canceled:
                    var cancellation = SpeechSynthesisCancellationDetails.FromResult(speechSynthesisResult);
                    Console.WriteLine($"CANCELED: Reason={cancellation.Reason}");

                    if (cancellation.Reason == CancellationReason.Error)
                    {
                        Console.WriteLine($"CANCELED: ErrorCode={cancellation.ErrorCode}");
                        Console.WriteLine($"CANCELED: ErrorDetails=[{cancellation.ErrorDetails}]");
                        Console.WriteLine($"CANCELED: Did you set the speech resource key and region values?");
                    }
                    break;
                default:
                    break;
            }
        }

        Console.WriteLine("Press any key to exit...");
        Console.ReadKey();
    }
}

可在 GitHub 中找到更多文本转语音示例。

使用自定义终结点

从功能上说,自定义终结点与用于文本转语音请求的标准终结点相同。

区别在于,必须指定 EndpointId 才能通过语音 SDK 使用自定义语音。 可以从文本转语音快速入门开始,然后使用 EndpointIdSpeechSynthesisVoiceName 更新代码。

var speechConfig = SpeechConfig.FromSubscription(speechKey, speechRegion);     
speechConfig.SpeechSynthesisVoiceName = "YourCustomVoiceName";
speechConfig.EndpointId = "YourEndpointId";

若要通过语音合成标记语言 (SSML) 使用定制声音,请将模型名称指定为语音名称。 本示例使用 YourCustomVoiceName 语音。

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="YourCustomVoiceName">
        This is the text that is spoken. 
    </voice>
</speak>

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | 包 (NuGet) | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

选择合成语言和语音

语音服务中的文本转语音功能支持 400 多种语音和 140 多种语言和变体。 请参阅受支持的文本转语音区域设置完整列表或在语音库中试用它们。

指定 SpeechConfig 类的语言或语音,使其与输入文本匹配,并使用指定的语音。 以下代码片段显示了此技术的工作原理:

void synthesizeSpeech()
{
    auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
    // Set either the `SpeechSynthesisVoiceName` or `SpeechSynthesisLanguage`.
    speechConfig->SetSpeechSynthesisLanguage("en-US"); 
    speechConfig->SetSpeechSynthesisVoiceName("en-US-AvaMultilingualNeural");
}

所有神经网络声音都是多语言的,并且能够流利地使用自己的语言和英语。 例如,如果英语的输入文本为“I'm excited to try text to speech”并且你选择了 es-ES-ElviraNeural,则该文本将用带西班牙口音的英语讲出。

如果语音没有使用输入文本的语言讲出,则语音服务不会创建合成音频。 有关支持的神经语音的完整列表,请参阅语音服务的语言和语音支持

注意

默认语音是从语音列表 API 根据区域设置返回的第一个语音。

所讲的语音按以下优先顺序确定:

  • 如果你没有设置 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会讲 en-US 的默认语音。
  • 如果你仅设置了 SpeechSynthesisLanguage,则会讲指定区域设置的默认语音。
  • 如果同时设置了 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会忽略 SpeechSynthesisLanguage 设置。 系统会讲你使用 SpeechSynthesisVoiceName 指定的语音。
  • 如果使用语音合成标记语言 (SSML) 设置了 voice 元素,则会忽略 SpeechSynthesisVoiceNameSpeechSynthesisLanguage 设置。

将语音合成到文件中

创建一个 SpeechSynthesizer 对象。 以下片段中显示的此对象可运行文本到语音的转换,并将转换结果输出到扬声器、文件或其他输出流。 SpeechSynthesizer 接受以下对象作为参数:

  1. 创建一个 AudioConfig 实例,以使用 FromWavFileOutput() 函数自动将输出写入到 .wav 文件:

    void synthesizeSpeech()
    {
        auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
        auto audioConfig = AudioConfig::FromWavFileOutput("path/to/write/file.wav");
    }
    
  2. 实例化 SpeechSynthesizer 实例。 将 speechConfig 对象和 audioConfig 对象作为参数传递。 若要合成语音并写入文件,请使用文本字符串运行 SpeakTextAsync()

    void synthesizeSpeech()
    {
        auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
        auto audioConfig = AudioConfig::FromWavFileOutput("path/to/write/file.wav");
        auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig, audioConfig);
        auto result = speechSynthesizer->SpeakTextAsync("A simple test to write to a file.").get();
    }
    

运行程序时,它会创建一个合成的 .wav 文件,该文件将写入到你指定的位置。 此结果是最基本用法的一个很好示例。 接下来,可以自定义输出,并将输出响应作为适用于自定义方案的内存中流进行处理。

合成到扬声器输出

若要将合成语音输出到当前活动输出设备(例如扬声器),请在创建 SpeechSynthesizer 实例时省略 AudioConfig 参数。 下面是一个示例:

void synthesizeSpeech()
{
    auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
    auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
    auto result = speechSynthesizer->SpeakTextAsync("I'm excited to try text to speech").get();
}

获取内存中流形式的结果

可将生成的音频数据用作内存中流,而不是直接写入到文件。 使用内存中流可以构建自定义行为:

  • 抽取生成的字节数组,作为自定义下游服务的可搜寻流。
  • 将结果与其他 API 或服务集成。
  • 修改音频数据,编写自定义 .wav 标头,并执行相关任务。

可以对上一个示例进行此更改。 首先删除 AudioConfig 块,因为从现在起,你将手动管理输出行为,以提高控制度。 在 SpeechSynthesizer 构造函数中为 AudioConfig 传递 NULL

注意

如果为 AudioConfig 传递 NULL,而不是像在前面的扬声器输出示例中那样省略它,则默认不会在当前处于活动状态的输出设备上播放音频。

将结果保存到 SpeechSynthesisResult 变量。 GetAudioData Getter 返回输出数据的 byte [] 实例。 可以手动使用此 byte [] 实例,也可以使用 AudioDataStream 类来管理内存中流。

在此示例中,使用 AudioDataStream.FromResult() 静态函数从结果中获取流:

void synthesizeSpeech()
{
    auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
    auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);

    auto result = speechSynthesizer->SpeakTextAsync("Getting the response as an in-memory stream.").get();
    auto stream = AudioDataStream::FromResult(result);
}

此时,可以使用生成的 stream 对象来实现任何自定义行为。

自定义音频格式

你可以自定义音频输出属性,包括:

  • 音频文件类型
  • 采样速率
  • 位深度

若要更改音频格式,请对 SpeechConfig 对象使用 SetSpeechSynthesisOutputFormat() 函数。 此函数需要一个 SpeechSynthesisOutputFormat 类型的 enum 实例。 使用 enum 选择输出格式。 有关可用格式,请参阅音频格式列表

可根据要求对不同的文件类型使用不同的选项。 根据定义,Raw24Khz16BitMonoPcm 等原始格式不包括音频标头。 仅在以下任一情况下使用原始格式:

  • 你知道下游实现可以解码原始位流。
  • 你打算基于位深度、采样率、通道数等因素手动生成标头。

此示例通过对 SpeechConfig 对象设置 SpeechSynthesisOutputFormat 来指定高保真 RIFF 格式 Riff24Khz16BitMonoPcm。 类似于上一部分中的示例,可以使用 AudioDataStream 获取结果的内存中流,然后将其写入文件。

void synthesizeSpeech()
{
    auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
    speechConfig->SetSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat::Riff24Khz16BitMonoPcm);

    auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
    auto result = speechSynthesizer->SpeakTextAsync("A simple test to write to a file.").get();

    auto stream = AudioDataStream::FromResult(result);
    stream->SaveToWavFileAsync("path/to/write/file.wav").get();
}

运行程序时,它会将 .wav 文件写入指定路径。

使用 SSML 自定义语音特征

借助 SSML,可以通过从 XML 架构中提交请求,来微调文本转语音输出的音节、发音、语速、音量和其他方面。 本部分展示了更改语音的示例。 有关详细信息,请参阅语音合成标记语言概述

若要开始使用 SSML 进行自定义,需要进行一个小的更改来切换语音。

  1. 在根项目目录中为 SSML 配置创建一个新的 XML 文件。

    <speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="en-US">
      <voice name="en-US-AvaMultilingualNeural">
        When you're on the freeway, it's a good idea to use a GPS.
      </voice>
    </speak>
    

    在此示例中,文件为 ssml.xml。 根元素始终是 <speak>。 通过将文本包装在 <voice> 元素中,可以使用 name 参数来更改语音。 有关支持的神经语音的完整列表,请参阅支持的语言

  2. 更改语音合成请求以引用 XML 文件。 请求大致相同。 请使用 SpeakSsmlAsync(),而不是使用 SpeakTextAsync() 函数。 此函数需要 XML 字符串。 首先,将 SSML 配置加载为字符串。 此时,结果对象与前面的示例完全相同。

    void synthesizeSpeech()
    {
        auto speechConfig = SpeechConfig::FromSubscription("YourSpeechKey", "YourSpeechRegion");
        auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);
    
        std::ifstream file("./ssml.xml");
        std::string ssml, line;
        while (std::getline(file, line))
        {
            ssml += line;
            ssml.push_back('\n');
        }
        auto result = speechSynthesizer->SpeakSsmlAsync(ssml).get();
    
        auto stream = AudioDataStream::FromResult(result);
        stream->SaveToWavFileAsync("path/to/write/file.wav").get();
    }
    

注意

若要在不使用 SSML 的情况下更改语音,可使用 SpeechConfig.SetSpeechSynthesisVoiceName("en-US-AndrewMultilingualNeural")SpeechConfig 上设置属性。

订阅合成器事件

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

在对文本转语音使用 SpeechSynthesizer 时,可以订阅此表中的事件:

事件 说明 用例
BookmarkReached 指示已进入书签。 若要触发书签进入事件,需要在 SSML 中指定一个 bookmark 元素。 此事件会报告输出音频在合成开始处到 bookmark 元素之间经历的时间。 该事件的 Text 属性是在书签的 mark 特性中设置的字符串值。 不会说出 bookmark 元素。 可以使用 bookmark 元素在 SSML 中插入自定义标记,以获得音频流中每个标记的偏移量。 bookmark 元素可用于引用文本或标记序列中的特定位置。
SynthesisCanceled 指示已取消语音合成。 可在合成取消后进行确认。
SynthesisCompleted 指示语音合成已完成。 可在合成完成后进行确认。
SynthesisStarted 表示已启动语音合成。 可在合成启动后进行确认。
Synthesizing 指示语音合成正在进行。 每次 SDK 从语音服务收到音频区块,都会触发此事件。 你可以确认合成何时正在进行。
VisemeReceived 指示已收到嘴形视位事件。 视素通常用于表示观察到的语音中的关键姿态。 关键姿态包括在产生特定音素时嘴唇、下巴和舌头的位置。 可以使用嘴形视位来来动画显示播放语音音频时人物的面部。
WordBoundary 指示已收到字边界。 在每个新的讲述字词、标点和句子的开头引发此事件。 该事件报告当前字词从输出音频开始的时间偏移(以时钟周期为单位)。 此事件还会报告输入文本或 SSML 中紧靠在要说出的字词之前的字符位置。 此事件通常用于获取文本和相应音频的相对位置。 你可能想要知道某个新字词,然后根据时间采取操作。 例如,可以获取所需的信息来帮助确定在说出字词时,何时突出显示这些字词,以及要突出显示多长时间。

注意

事件在输出音频数据变为可用时引发,这样将会比播放到输出设备更快。 调用方必须相应地实时同步流式处理。

以下示例演示如何订阅语音合成的事件。 可以按照快速入门中的说明操作,但请将该 main.cpp 文件的内容替换为以下 C++ 代码:

#include <iostream> 
#include <stdlib.h>
#include <speechapi_cxx.h>

using namespace Microsoft::CognitiveServices::Speech;
using namespace Microsoft::CognitiveServices::Speech::Audio;

std::string getEnvironmentVariable(const char* name);

int main()
{
    // This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
    auto speechKey = getEnvironmentVariable("SPEECH_KEY");
    auto speechRegion = getEnvironmentVariable("SPEECH_REGION");

    if ((size(speechKey) == 0) || (size(speechRegion) == 0)) {
        std::cout << "Please set both SPEECH_KEY and SPEECH_REGION environment variables." << std::endl;
        return -1;
    }

    auto speechConfig = SpeechConfig::FromSubscription(speechKey, speechRegion);

    // Required for WordBoundary event sentences.
    speechConfig->SetProperty(PropertyId::SpeechServiceResponse_RequestSentenceBoundary, "true");

    const auto ssml = R"(<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'>
        <voice name = 'en-US-AvaMultilingualNeural'>
            <mstts:viseme type = 'redlips_front' />
            The rainbow has seven colors : <bookmark mark = 'colors_list_begin' />Red, orange, yellow, green, blue, indigo, and violet.<bookmark mark = 'colors_list_end' />.
        </voice>
        </speak>)";

    auto speechSynthesizer = SpeechSynthesizer::FromConfig(speechConfig);

    // Subscribe to events

    speechSynthesizer->BookmarkReached += [](const SpeechSynthesisBookmarkEventArgs& e)
    {
        std::cout << "Bookmark reached. "
            << "\r\n\tAudioOffset: " << round(e.AudioOffset / 10000) << "ms"
            << "\r\n\tText: " << e.Text << std::endl;
    };

    speechSynthesizer->SynthesisCanceled += [](const SpeechSynthesisEventArgs& e)
    {
        std::cout << "SynthesisCanceled event" << std::endl;
    };

    speechSynthesizer->SynthesisCompleted += [](const SpeechSynthesisEventArgs& e)
    {
        auto audioDuration = std::chrono::duration_cast<std::chrono::milliseconds>(e.Result->AudioDuration).count();

        std::cout << "SynthesisCompleted event:"
            << "\r\n\tAudioData: " << e.Result->GetAudioData()->size() << "bytes"
            << "\r\n\tAudioDuration: " << audioDuration << std::endl;
    };

    speechSynthesizer->SynthesisStarted += [](const SpeechSynthesisEventArgs& e)
    {
        std::cout << "SynthesisStarted event" << std::endl;
    };

    speechSynthesizer->Synthesizing += [](const SpeechSynthesisEventArgs& e)
    {
        std::cout << "Synthesizing event:"
            << "\r\n\tAudioData: " << e.Result->GetAudioData()->size() << "bytes" << std::endl;
    };

    speechSynthesizer->VisemeReceived += [](const SpeechSynthesisVisemeEventArgs& e)
    {
        std::cout << "VisemeReceived event:"
            << "\r\n\tAudioOffset: " << round(e.AudioOffset / 10000) << "ms"
            << "\r\n\tVisemeId: " << e.VisemeId << std::endl;
    };

    speechSynthesizer->WordBoundary += [](const SpeechSynthesisWordBoundaryEventArgs& e)
    {
        auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(e.Duration).count();
        
        auto boundaryType = "";
        switch (e.BoundaryType) {
        case SpeechSynthesisBoundaryType::Punctuation:
            boundaryType = "Punctuation";
            break;
        case SpeechSynthesisBoundaryType::Sentence:
            boundaryType = "Sentence";
            break;
        case SpeechSynthesisBoundaryType::Word:
            boundaryType = "Word";
            break;
        }

        std::cout << "WordBoundary event:"
            // Word, Punctuation, or Sentence
            << "\r\n\tBoundaryType: " << boundaryType
            << "\r\n\tAudioOffset: " << round(e.AudioOffset / 10000) << "ms"
            << "\r\n\tDuration: " << duration
            << "\r\n\tText: \"" << e.Text << "\""
            << "\r\n\tTextOffset: " << e.TextOffset
            << "\r\n\tWordLength: " << e.WordLength << std::endl;
    };

    auto result = speechSynthesizer->SpeakSsmlAsync(ssml).get();

    // Checks result.
    if (result->Reason == ResultReason::SynthesizingAudioCompleted)
    {
        std::cout << "SynthesizingAudioCompleted result" << std::endl;
    }
    else if (result->Reason == ResultReason::Canceled)
    {
        auto cancellation = SpeechSynthesisCancellationDetails::FromResult(result);
        std::cout << "CANCELED: Reason=" << (int)cancellation->Reason << std::endl;

        if (cancellation->Reason == CancellationReason::Error)
        {
            std::cout << "CANCELED: ErrorCode=" << (int)cancellation->ErrorCode << std::endl;
            std::cout << "CANCELED: ErrorDetails=[" << cancellation->ErrorDetails << "]" << std::endl;
            std::cout << "CANCELED: Did you set the speech resource key and region values?" << std::endl;
        }
    }

    std::cout << "Press enter to exit..." << std::endl;
    std::cin.get();
}

std::string getEnvironmentVariable(const char* name)
{
#if defined(_MSC_VER)
    size_t requiredSize = 0;
    (void)getenv_s(&requiredSize, nullptr, 0, name);
    if (requiredSize == 0)
    {
        return "";
    }
    auto buffer = std::make_unique<char[]>(requiredSize);
    (void)getenv_s(&requiredSize, buffer.get(), requiredSize, name);
    return buffer.get();
#else
    auto value = getenv(name);
    return value ? value : "";
#endif
}

可在 GitHub 中找到更多文本转语音示例。

使用自定义终结点

从功能上说,自定义终结点与用于文本转语音请求的标准终结点相同。

区别在于,必须指定 EndpointId 才能通过语音 SDK 使用自定义语音。 可以从文本转语音快速入门开始,然后使用 EndpointIdSpeechSynthesisVoiceName 更新代码。

auto speechConfig = SpeechConfig::FromSubscription(speechKey, speechRegion);
speechConfig->SetSpeechSynthesisVoiceName("YourCustomVoiceName");
speechConfig->SetEndpointId("YourEndpointId");

若要通过语音合成标记语言 (SSML) 使用定制声音,请将模型名称指定为语音名称。 本示例使用 YourCustomVoiceName 语音。

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="YourCustomVoiceName">
        This is the text that is spoken. 
    </voice>
</speak>

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | 包 (Go) | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

先决条件

安装语音 SDK

需要先安装适用于 Go 的语音 SDK,然后才能执行其他操作。

文本转语音到扬声器

使用以下代码示例在默认音频输出设备上运行语音合成。 将变量 subscriptionregion 替换为你的语音密钥和位置/区域。 运行该脚本会使用默认扬声器朗读输入文本。

package main

import (
    "bufio"
    "fmt"
    "os"
    "strings"
    "time"

    "github.com/Microsoft/cognitive-services-speech-sdk-go/audio"
    "github.com/Microsoft/cognitive-services-speech-sdk-go/common"
    "github.com/Microsoft/cognitive-services-speech-sdk-go/speech"
)

func synthesizeStartedHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("Synthesis started.")
}

func synthesizingHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Printf("Synthesizing, audio chunk size %d.\n", len(event.Result.AudioData))
}

func synthesizedHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Printf("Synthesized, audio length %d.\n", len(event.Result.AudioData))
}

func cancelledHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("Received a cancellation.")
}

func main() {
    subscription := "YourSpeechKey"
    region := "YourSpeechRegion"

    audioConfig, err := audio.NewAudioConfigFromDefaultSpeakerOutput()
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer audioConfig.Close()
    speechConfig, err := speech.NewSpeechConfigFromSubscription(subscription, region)
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer speechConfig.Close()
    speechSynthesizer, err := speech.NewSpeechSynthesizerFromConfig(speechConfig, audioConfig)
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer speechSynthesizer.Close()

    speechSynthesizer.SynthesisStarted(synthesizeStartedHandler)
    speechSynthesizer.Synthesizing(synthesizingHandler)
    speechSynthesizer.SynthesisCompleted(synthesizedHandler)
    speechSynthesizer.SynthesisCanceled(cancelledHandler)

    for {
        fmt.Printf("Enter some text that you want to speak, or enter empty text to exit.\n> ")
        text, _ := bufio.NewReader(os.Stdin).ReadString('\n')
        text = strings.TrimSuffix(text, "\n")
        if len(text) == 0 {
            break
        }

        task := speechSynthesizer.SpeakTextAsync(text)
        var outcome speech.SpeechSynthesisOutcome
        select {
        case outcome = <-task:
        case <-time.After(60 * time.Second):
            fmt.Println("Timed out")
            return
        }
        defer outcome.Close()
        if outcome.Error != nil {
            fmt.Println("Got an error: ", outcome.Error)
            return
        }

        if outcome.Result.Reason == common.SynthesizingAudioCompleted {
            fmt.Printf("Speech synthesized to speaker for text [%s].\n", text)
        } else {
            cancellation, _ := speech.NewCancellationDetailsFromSpeechSynthesisResult(outcome.Result)
            fmt.Printf("CANCELED: Reason=%d.\n", cancellation.Reason)

            if cancellation.Reason == common.Error {
                fmt.Printf("CANCELED: ErrorCode=%d\nCANCELED: ErrorDetails=[%s]\nCANCELED: Did you set the speech resource key and region values?\n",
                    cancellation.ErrorCode,
                    cancellation.ErrorDetails)
            }
        }
    }
}

运行以下命令,创建一个 go.mod 文件并使其关联到 GitHub 上托管的组件:

go mod init quickstart
go get github.com/Microsoft/cognitive-services-speech-sdk-go

现在生成并运行代码:

go build
go run quickstart

有关类的详细信息,请参阅 SpeechConfigSpeechSynthesizer 参考文档。

文本转语音到内存流

可将生成的音频数据用作内存中流,而不是直接写入到文件。 使用内存中流可以构建自定义行为:

  • 抽取生成的字节数组,作为自定义下游服务的可搜寻流。
  • 将结果与其他 API 或服务集成。
  • 修改音频数据,编写自定义 .wav 标头,并执行相关任务。

可以对上一个示例进行此更改。 删除 AudioConfig 块,因为从现在起,你将手动管理输出行为,以提高控制度。 然后在 SpeechSynthesizer 构造函数中为 AudioConfig 传递 nil

备注

如果为 AudioConfig 传递 nil,而不是像在前面的扬声器输出示例中那样省略它,则默认不会在当前处于活动状态的输出设备上播放音频。

将结果保存到 SpeechSynthesisResult 变量。 AudioData 属性返回输出数据的 []byte 实例。 可以手动使用此 []byte 实例,也可以使用 AudioDataStream 类来管理内存中流。 此示例使用 NewAudioDataStreamFromSpeechSynthesisResult() 静态函数从结果中获取流。

将变量 subscriptionregion 替换为你的语音密钥和位置/区域:

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "strings"
    "time"

    "github.com/Microsoft/cognitive-services-speech-sdk-go/speech"
)

func synthesizeStartedHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("Synthesis started.")
}

func synthesizingHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Printf("Synthesizing, audio chunk size %d.\n", len(event.Result.AudioData))
}

func synthesizedHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Printf("Synthesized, audio length %d.\n", len(event.Result.AudioData))
}

func cancelledHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("Received a cancellation.")
}

func main() {
    subscription := "YourSpeechKey"
    region := "YourSpeechRegion"

    speechConfig, err := speech.NewSpeechConfigFromSubscription(subscription, region)
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer speechConfig.Close()
    speechSynthesizer, err := speech.NewSpeechSynthesizerFromConfig(speechConfig, nil)
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer speechSynthesizer.Close()

    speechSynthesizer.SynthesisStarted(synthesizeStartedHandler)
    speechSynthesizer.Synthesizing(synthesizingHandler)
    speechSynthesizer.SynthesisCompleted(synthesizedHandler)
    speechSynthesizer.SynthesisCanceled(cancelledHandler)

    for {
        fmt.Printf("Enter some text that you want to speak, or enter empty text to exit.\n> ")
        text, _ := bufio.NewReader(os.Stdin).ReadString('\n')
        text = strings.TrimSuffix(text, "\n")
        if len(text) == 0 {
            break
        }

        // StartSpeakingTextAsync sends the result to channel when the synthesis starts.
        task := speechSynthesizer.StartSpeakingTextAsync(text)
        var outcome speech.SpeechSynthesisOutcome
        select {
        case outcome = <-task:
        case <-time.After(60 * time.Second):
            fmt.Println("Timed out")
            return
        }
        defer outcome.Close()
        if outcome.Error != nil {
            fmt.Println("Got an error: ", outcome.Error)
            return
        }

        // In most cases, we want to streaming receive the audio to lower the latency.
        // We can use AudioDataStream to do so.
        stream, err := speech.NewAudioDataStreamFromSpeechSynthesisResult(outcome.Result)
        defer stream.Close()
        if err != nil {
            fmt.Println("Got an error: ", err)
            return
        }

        var all_audio []byte
        audio_chunk := make([]byte, 2048)
        for {
            n, err := stream.Read(audio_chunk)

            if err == io.EOF {
                break
            }

            all_audio = append(all_audio, audio_chunk[:n]...)
        }

        fmt.Printf("Read [%d] bytes from audio data stream.\n", len(all_audio))
    }
}

运行以下命令,创建一个 go.mod 文件并使其关联到 GitHub 上托管的组件:

go mod init quickstart
go get github.com/Microsoft/cognitive-services-speech-sdk-go

现在生成并运行代码:

go build
go run quickstart

有关类的详细信息,请参阅 SpeechConfigSpeechSynthesizer 参考文档。

选择合成语言和语音

语音服务中的文本转语音功能支持 400 多种语音和 140 多种语言和变体。 你可以获取完整列表,或在语音库中试用它们。

指定 SpeechConfig 的语言或语音,使其与输入文本匹配,并使用指定的语音:

speechConfig, err := speech.NewSpeechConfigFromSubscription(key, region)
if err != nil {
    fmt.Println("Got an error: ", err)
    return
}
defer speechConfig.Close()

speechConfig.SetSpeechSynthesisLanguage("en-US")
speechConfig.SetSpeechSynthesisVoiceName("en-US-AvaMultilingualNeural")

所有神经网络声音都是多语言的,并且能够流利地使用自己的语言和英语。 例如,如果英语的输入文本为“I'm excited to try text to speech”并且你选择了 es-ES-ElviraNeural,则该文本将用带西班牙口音的英语讲出。

如果语音没有使用输入文本的语言讲出,则语音服务不会创建合成音频。 有关支持的神经语音的完整列表,请参阅语音服务的语言和语音支持

注意

默认语音是从语音列表 API 根据区域设置返回的第一个语音。

所讲的语音按以下优先顺序确定:

  • 如果你没有设置 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会讲 en-US 的默认语音。
  • 如果你仅设置了 SpeechSynthesisLanguage,则会讲指定区域设置的默认语音。
  • 如果同时设置了 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会忽略 SpeechSynthesisLanguage 设置。 系统会讲你使用 SpeechSynthesisVoiceName 指定的语音。
  • 如果使用语音合成标记语言 (SSML) 设置了 voice 元素,则会忽略 SpeechSynthesisVoiceNameSpeechSynthesisLanguage 设置。

使用 SSML 自定义语音特征

借助语音合成标记语言 (SSML),可以通过从 XML 架构中提交请求来微调文本转语音输出的音高、发音、语速、音量等。 本部分展示了更改语音的示例。 有关详细信息,请参阅语音合成标记语言概述

若要开始使用 SSML 进行自定义,需要进行一个小的更改来切换语音。

首先,在根项目目录中为 SSML 配置创建一个新的 XML 文件。 在此示例中,它是 ssml.xml。 根元素始终是 <speak>。 通过将文本包装在 <voice> 元素中,可以使用 name 参数来更改语音。 有关支持的神经语音的完整列表,请参阅支持的语言

<speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="en-US">
  <voice name="en-US-AvaMultilingualNeural">
    When you're on the freeway, it's a good idea to use a GPS.
  </voice>
</speak>

接下来,需要更改语音合成请求以引用 XML 文件。 该请求基本上保持不变,只不过需要使用 SpeakSsmlAsync() 而不是 SpeakTextAsync() 函数。 此函数需要 XML 字符串,因此,请先加载字符串形式的 SSML 配置。 此时,结果对象与前面的示例完全相同。

注意

若要在不使用 SSML 的情况下设置语音,可使用 speechConfig.SetSpeechSynthesisVoiceName("en-US-AvaMultilingualNeural")SpeechConfig 上设置该属性。

订阅合成器事件

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

在对文本转语音使用 SpeechSynthesizer 时,可以订阅此表中的事件:

事件 说明 用例
BookmarkReached 指示已进入书签。 若要触发书签进入事件,需要在 SSML 中指定一个 bookmark 元素。 此事件会报告输出音频在合成开始处到 bookmark 元素之间经历的时间。 该事件的 Text 属性是在书签的 mark 特性中设置的字符串值。 不会说出 bookmark 元素。 可以使用 bookmark 元素在 SSML 中插入自定义标记,以获得音频流中每个标记的偏移量。 bookmark 元素可用于引用文本或标记序列中的特定位置。
SynthesisCanceled 指示已取消语音合成。 可在合成取消后进行确认。
SynthesisCompleted 指示语音合成已完成。 可在合成完成后进行确认。
SynthesisStarted 表示已启动语音合成。 可在合成启动后进行确认。
Synthesizing 指示语音合成正在进行。 每次 SDK 从语音服务收到音频区块,都会触发此事件。 你可以确认合成何时正在进行。
VisemeReceived 指示已收到嘴形视位事件。 视素通常用于表示观察到的语音中的关键姿态。 关键姿态包括在产生特定音素时嘴唇、下巴和舌头的位置。 可以使用嘴形视位来来动画显示播放语音音频时人物的面部。
WordBoundary 指示已收到字边界。 在每个新的讲述字词、标点和句子的开头引发此事件。 该事件报告当前字词从输出音频开始的时间偏移(以时钟周期为单位)。 此事件还会报告输入文本或 SSML 中紧靠在要说出的字词之前的字符位置。 此事件通常用于获取文本和相应音频的相对位置。 你可能想要知道某个新字词,然后根据时间采取操作。 例如,可以获取所需的信息来帮助确定在说出字词时,何时突出显示这些字词,以及要突出显示多长时间。

注意

事件在输出音频数据变为可用时引发,这样将会比播放到输出设备更快。 调用方必须相应地实时同步流式处理。

以下示例演示如何订阅语音合成的事件。 可以按照快速入门中的说明操作,但请将该 speech-synthesis.go 文件的内容替换为以下 Go 代码:

package main

import (
    "fmt"
    "os"
    "time"

    "github.com/Microsoft/cognitive-services-speech-sdk-go/audio"
    "github.com/Microsoft/cognitive-services-speech-sdk-go/common"
    "github.com/Microsoft/cognitive-services-speech-sdk-go/speech"
)

func bookmarkReachedHandler(event speech.SpeechSynthesisBookmarkEventArgs) {
    defer event.Close()
    fmt.Println("BookmarkReached event")
}

func synthesisCanceledHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("SynthesisCanceled event")
}

func synthesisCompletedHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("SynthesisCompleted event")
    fmt.Printf("\tAudioData: %d bytes\n", len(event.Result.AudioData))
    fmt.Printf("\tAudioDuration: %d\n", event.Result.AudioDuration)
}

func synthesisStartedHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("SynthesisStarted event")
}

func synthesizingHandler(event speech.SpeechSynthesisEventArgs) {
    defer event.Close()
    fmt.Println("Synthesizing event")
    fmt.Printf("\tAudioData %d bytes\n", len(event.Result.AudioData))
}

func visemeReceivedHandler(event speech.SpeechSynthesisVisemeEventArgs) {
    defer event.Close()
    fmt.Println("VisemeReceived event")
    fmt.Printf("\tAudioOffset: %dms\n", (event.AudioOffset+5000)/10000)
    fmt.Printf("\tVisemeID %d\n", event.VisemeID)
}

func wordBoundaryHandler(event speech.SpeechSynthesisWordBoundaryEventArgs) {
    defer event.Close()
    boundaryType := ""
    switch event.BoundaryType {
    case 0:
        boundaryType = "Word"
    case 1:
        boundaryType = "Punctuation"
    case 2:
        boundaryType = "Sentence"
    }
    fmt.Println("WordBoundary event")
    fmt.Printf("\tBoundaryType %v\n", boundaryType)
    fmt.Printf("\tAudioOffset: %dms\n", (event.AudioOffset+5000)/10000)
    fmt.Printf("\tDuration %d\n", event.Duration)
    fmt.Printf("\tText %s\n", event.Text)
    fmt.Printf("\tTextOffset %d\n", event.TextOffset)
    fmt.Printf("\tWordLength %d\n", event.WordLength)
}

func main() {
    // This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
    speechKey := os.Getenv("SPEECH_KEY")
    speechRegion := os.Getenv("SPEECH_REGION")

    audioConfig, err := audio.NewAudioConfigFromDefaultSpeakerOutput()
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer audioConfig.Close()
    speechConfig, err := speech.NewSpeechConfigFromSubscription(speechKey, speechRegion)
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer speechConfig.Close()

    // Required for WordBoundary event sentences.
    speechConfig.SetProperty(common.SpeechServiceResponseRequestSentenceBoundary, "true")

    speechSynthesizer, err := speech.NewSpeechSynthesizerFromConfig(speechConfig, audioConfig)
    if err != nil {
        fmt.Println("Got an error: ", err)
        return
    }
    defer speechSynthesizer.Close()

    speechSynthesizer.BookmarkReached(bookmarkReachedHandler)
    speechSynthesizer.SynthesisCanceled(synthesisCanceledHandler)
    speechSynthesizer.SynthesisCompleted(synthesisCompletedHandler)
    speechSynthesizer.SynthesisStarted(synthesisStartedHandler)
    speechSynthesizer.Synthesizing(synthesizingHandler)
    speechSynthesizer.VisemeReceived(visemeReceivedHandler)
    speechSynthesizer.WordBoundary(wordBoundaryHandler)

    speechSynthesisVoiceName := "en-US-AvaMultilingualNeural"

    ssml := fmt.Sprintf(`<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'>
            <voice name='%s'>
                <mstts:viseme type='redlips_front'/>
                The rainbow has seven colors: <bookmark mark='colors_list_begin'/>Red, orange, yellow, green, blue, indigo, and violet.<bookmark mark='colors_list_end'/>.
            </voice>
        </speak>`, speechSynthesisVoiceName)

    // Synthesize the SSML
    fmt.Printf("SSML to synthesize: \n\t%s\n", ssml)
    task := speechSynthesizer.SpeakSsmlAsync(ssml)

    var outcome speech.SpeechSynthesisOutcome
    select {
    case outcome = <-task:
    case <-time.After(60 * time.Second):
        fmt.Println("Timed out")
        return
    }
    defer outcome.Close()
    if outcome.Error != nil {
        fmt.Println("Got an error: ", outcome.Error)
        return
    }

    if outcome.Result.Reason == common.SynthesizingAudioCompleted {
        fmt.Println("SynthesizingAudioCompleted result")
    } else {
        cancellation, _ := speech.NewCancellationDetailsFromSpeechSynthesisResult(outcome.Result)
        fmt.Printf("CANCELED: Reason=%d.\n", cancellation.Reason)

        if cancellation.Reason == common.Error {
            fmt.Printf("CANCELED: ErrorCode=%d\nCANCELED: ErrorDetails=[%s]\nCANCELED: Did you set the speech resource key and region values?\n",
                cancellation.ErrorCode,
                cancellation.ErrorDetails)
        }
    }
}

可在 GitHub 中找到更多文本转语音示例。

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

选择合成语言和语音

语音服务中的文本转语音功能支持 400 多种语音和 140 多种语言和变体。 你可以获取完整列表,或在语音库中试用它们。

指定 SpeechConfig 的语言或语音,使其与输入文本匹配,并使用指定的语音。 以下代码片段显示了此技术的工作原理:

public static void main(String[] args) {
    SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    // Set either the `SpeechSynthesisVoiceName` or `SpeechSynthesisLanguage`.
    speechConfig.setSpeechSynthesisLanguage("en-US"); 
    speechConfig.setSpeechSynthesisVoiceName("en-US-AvaMultilingualNeural");
}

所有神经网络声音都是多语言的,并且能够流利地使用自己的语言和英语。 例如,如果英语的输入文本为“I'm excited to try text to speech”并且你选择了 es-ES-ElviraNeural,则该文本将用带西班牙口音的英语讲出。

如果语音没有使用输入文本的语言讲出,则语音服务不会创建合成音频。 有关支持的神经语音的完整列表,请参阅语音服务的语言和语音支持

注意

默认语音是从语音列表 API 根据区域设置返回的第一个语音。

所讲的语音按以下优先顺序确定:

  • 如果你没有设置 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会讲 en-US 的默认语音。
  • 如果你仅设置了 SpeechSynthesisLanguage,则会讲指定区域设置的默认语音。
  • 如果同时设置了 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会忽略 SpeechSynthesisLanguage 设置。 系统会讲你使用 SpeechSynthesisVoiceName 指定的语音。
  • 如果使用语音合成标记语言 (SSML) 设置了 voice 元素,则会忽略 SpeechSynthesisVoiceNameSpeechSynthesisLanguage 设置。

将语音合成到文件中

创建 SpeechSynthesizer 对象。 此对象返回文本到语音的转换,并将转换结果输出到扬声器、文件或其他输出流。 SpeechSynthesizer 接受以下对象作为参数:

  1. 创建一个 AudioConfig 实例,以使用 fromWavFileOutput() 静态函数自动将输出写入到 .wav 文件:

    public static void main(String[] args) {
        SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
        AudioConfig audioConfig = AudioConfig.fromWavFileOutput("path/to/write/file.wav");
    }
    
  2. 实例化 SpeechSynthesizer 实例。 将 speechConfig 对象和 audioConfig 对象作为参数传递。 若要合成语音并写入文件,请使用文本字符串运行 SpeakText()

    public static void main(String[] args) {
        SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
        AudioConfig audioConfig = AudioConfig.fromWavFileOutput("path/to/write/file.wav");
    
        SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(speechConfig, audioConfig);
        speechSynthesizer.SpeakText("I'm excited to try text to speech");
    }
    

运行程序时,它会创建一个合成的 .wav 文件,该文件将写入到你指定的位置。 此结果是最基本用法的一个很好示例。 接下来,可以自定义输出,并将输出响应作为适用于自定义方案的内存中流进行处理。

合成到扬声器输出

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

若要将合成语音输出到当前活动输出设备(例如扬声器),请使用 fromDefaultSpeakerOutput() 静态函数实例化 AudioConfig。 下面是一个示例:

public static void main(String[] args) {
    SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    AudioConfig audioConfig = AudioConfig.fromDefaultSpeakerOutput();

    SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(speechConfig, audioConfig);
    speechSynthesizer.SpeakText("I'm excited to try text to speech");
}

获取内存中流形式的结果

可将生成的音频数据用作内存中流,而不是直接写入到文件。 使用内存中流可以构建自定义行为:

  • 抽取生成的字节数组,作为自定义下游服务的可搜寻流。
  • 将结果与其他 API 或服务集成。
  • 修改音频数据,编写自定义 .wav 标头,并执行相关任务。

可以对上一个示例进行此更改。 首先删除 AudioConfig 块,因为从现在起,你将手动管理输出行为,以提高控制度。 然后在 SpeechSynthesizer 构造函数中为 AudioConfig 传递 null

注意

如果为 AudioConfig 传递 null,而不是像在前面的扬声器输出示例中那样省略它,则默认不会在当前处于活动状态的输出设备上播放音频。

将结果保存到 SpeechSynthesisResult 变量。 SpeechSynthesisResult.getAudioData() 函数返回输出数据的 byte [] 实例。 可以手动使用此 byte [] 实例,也可以使用 AudioDataStream 类来管理内存中流。

在此示例中,使用 AudioDataStream.fromResult() 静态函数从结果中获取流:

public static void main(String[] args) {
    SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(speechConfig, null);

    SpeechSynthesisResult result = speechSynthesizer.SpeakText("I'm excited to try text to speech");
    AudioDataStream stream = AudioDataStream.fromResult(result);
    System.out.print(stream.getStatus());
}

此时,可以使用生成的 stream 对象来实现任何自定义行为。

自定义音频格式

你可以自定义音频输出属性,包括:

  • 音频文件类型
  • 采样速率
  • 位深度

若要更改音频格式,请对 SpeechConfig 对象使用 setSpeechSynthesisOutputFormat() 函数。 此函数需要一个 SpeechSynthesisOutputFormat 类型的 enum 实例。 使用 enum 选择输出格式。 有关可用格式,请参阅音频格式列表

可根据要求对不同的文件类型使用不同的选项。 根据定义,Raw24Khz16BitMonoPcm 等原始格式不包括音频标头。 仅在以下任一情况下使用原始格式:

  • 你知道下游实现可以解码原始位流。
  • 你打算基于位深度、采样率、通道数等因素手动生成标头。

此示例通过对 SpeechConfig 对象设置 SpeechSynthesisOutputFormat 来指定高保真 RIFF 格式 Riff24Khz16BitMonoPcm。 类似于上一部分中的示例,可以使用 AudioDataStream 获取结果的内存中流,然后将其写入文件。

public static void main(String[] args) {
    SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");

    // set the output format
    speechConfig.setSpeechSynthesisOutputFormat(SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm);

    SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(speechConfig, null);
    SpeechSynthesisResult result = speechSynthesizer.SpeakText("I'm excited to try text to speech");
    AudioDataStream stream = AudioDataStream.fromResult(result);
    stream.saveToWavFile("path/to/write/file.wav");
}

运行程序时,它会将 .wav 文件写入指定路径。

使用 SSML 自定义语音特征

借助 SSML,可以通过从 XML 架构中提交请求,来微调文本转语音输出的音节、发音、语速、音量和其他方面。 本部分展示了更改语音的示例。 有关详细信息,请参阅 SSML 操作指南文章

若要开始使用 SSML 进行自定义,需要进行一个小的更改来切换语音。

  1. 在根项目目录中为 SSML 配置创建一个新的 XML 文件。

    <speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="en-US">
      <voice name="en-US-AvaMultilingualNeural">
        When you're on the freeway, it's a good idea to use a GPS.
      </voice>
    </speak>
    

    在此示例中,文件为 ssml.xml。 根元素始终是 <speak>。 通过将文本包装在 <voice> 元素中,可以使用 name 参数来更改语音。 有关支持的神经语音的完整列表,请参阅支持的语言

  2. 更改语音合成请求以引用 XML 文件。 请求大致相同。 请使用 SpeakSsml(),而不是使用 SpeakText() 函数。 此函数需要 XML 字符串,因此,请先创建一个加载 XML 文件并将其作为字符串返回的函数:

    private static String xmlToString(String filePath) {
        File file = new File(filePath);
        StringBuilder fileContents = new StringBuilder((int)file.length());
    
        try (Scanner scanner = new Scanner(file)) {
            while(scanner.hasNextLine()) {
                fileContents.append(scanner.nextLine() + System.lineSeparator());
            }
            return fileContents.toString().trim();
        } catch (FileNotFoundException ex) {
            return "File not found.";
        }
    }
    

    此时,结果对象与前面的示例完全相同:

    public static void main(String[] args) {
        SpeechConfig speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
        SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(speechConfig, null);
    
        String ssml = xmlToString("ssml.xml");
        SpeechSynthesisResult result = speechSynthesizer.SpeakSsml(ssml);
        AudioDataStream stream = AudioDataStream.fromResult(result);
        stream.saveToWavFile("path/to/write/file.wav");
    }
    

注意

若要在不使用 SSML 的情况下更改语音,请使用 SpeechConfig.setSpeechSynthesisVoiceName("en-US-AvaMultilingualNeural");SpeechConfig 上设置属性。

订阅合成器事件

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

在对文本转语音使用 SpeechSynthesizer 时,可以订阅此表中的事件:

事件 说明 用例
BookmarkReached 指示已进入书签。 若要触发书签进入事件,需要在 SSML 中指定一个 bookmark 元素。 此事件会报告输出音频在合成开始处到 bookmark 元素之间经历的时间。 该事件的 Text 属性是在书签的 mark 特性中设置的字符串值。 不会说出 bookmark 元素。 可以使用 bookmark 元素在 SSML 中插入自定义标记,以获得音频流中每个标记的偏移量。 bookmark 元素可用于引用文本或标记序列中的特定位置。
SynthesisCanceled 指示已取消语音合成。 可在合成取消后进行确认。
SynthesisCompleted 指示语音合成已完成。 可在合成完成后进行确认。
SynthesisStarted 表示已启动语音合成。 可在合成启动后进行确认。
Synthesizing 指示语音合成正在进行。 每次 SDK 从语音服务收到音频区块,都会触发此事件。 你可以确认合成何时正在进行。
VisemeReceived 指示已收到嘴形视位事件。 视素通常用于表示观察到的语音中的关键姿态。 关键姿态包括在产生特定音素时嘴唇、下巴和舌头的位置。 可以使用嘴形视位来来动画显示播放语音音频时人物的面部。
WordBoundary 指示已收到字边界。 在每个新的讲述字词、标点和句子的开头引发此事件。 该事件报告当前字词从输出音频开始的时间偏移(以时钟周期为单位)。 此事件还会报告输入文本或 SSML 中紧靠在要说出的字词之前的字符位置。 此事件通常用于获取文本和相应音频的相对位置。 你可能想要知道某个新字词,然后根据时间采取操作。 例如,可以获取所需的信息来帮助确定在说出字词时,何时突出显示这些字词,以及要突出显示多长时间。

注意

事件在输出音频数据变为可用时引发,这样将会比播放到输出设备更快。 调用方必须相应地实时同步流式处理。

以下示例演示如何订阅语音合成的事件。 可以按照快速入门中的说明操作,但请将该 SpeechSynthesis.java 文件的内容替换为以下 Java 代码:

import com.microsoft.cognitiveservices.speech.*;
import com.microsoft.cognitiveservices.speech.audio.*;

import java.util.Scanner;
import java.util.concurrent.ExecutionException;

public class SpeechSynthesis {
    // This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
    private static String speechKey = System.getenv("SPEECH_KEY");
    private static String speechRegion = System.getenv("SPEECH_REGION");

    public static void main(String[] args) throws InterruptedException, ExecutionException {

        SpeechConfig speechConfig = SpeechConfig.fromSubscription(speechKey, speechRegion);
        
        // Required for WordBoundary event sentences.
        speechConfig.setProperty(PropertyId.SpeechServiceResponse_RequestSentenceBoundary, "true");

        String speechSynthesisVoiceName = "en-US-AvaMultilingualNeural"; 
        
        String ssml = String.format("<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'>"
            .concat(String.format("<voice name='%s'>", speechSynthesisVoiceName))
            .concat("<mstts:viseme type='redlips_front'/>")
            .concat("The rainbow has seven colors: <bookmark mark='colors_list_begin'/>Red, orange, yellow, green, blue, indigo, and violet.<bookmark mark='colors_list_end'/>.")
            .concat("</voice>")
            .concat("</speak>"));

        SpeechSynthesizer speechSynthesizer = new SpeechSynthesizer(speechConfig);
        {
            // Subscribe to events

            speechSynthesizer.BookmarkReached.addEventListener((o, e) -> {
                System.out.println("BookmarkReached event:");
                System.out.println("\tAudioOffset: " + ((e.getAudioOffset() + 5000) / 10000) + "ms");
                System.out.println("\tText: " + e.getText());
            });

            speechSynthesizer.SynthesisCanceled.addEventListener((o, e) -> {
                System.out.println("SynthesisCanceled event");
            });

            speechSynthesizer.SynthesisCompleted.addEventListener((o, e) -> {
                SpeechSynthesisResult result = e.getResult();                
                byte[] audioData = result.getAudioData();
                System.out.println("SynthesisCompleted event:");
                System.out.println("\tAudioData: " + audioData.length + " bytes");
                System.out.println("\tAudioDuration: " + result.getAudioDuration());
                result.close();
            });
            
            speechSynthesizer.SynthesisStarted.addEventListener((o, e) -> {
                System.out.println("SynthesisStarted event");
            });

            speechSynthesizer.Synthesizing.addEventListener((o, e) -> {
                SpeechSynthesisResult result = e.getResult();
                byte[] audioData = result.getAudioData();
                System.out.println("Synthesizing event:");
                System.out.println("\tAudioData: " + audioData.length + " bytes");
                result.close();
            });

            speechSynthesizer.VisemeReceived.addEventListener((o, e) -> {
                System.out.println("VisemeReceived event:");
                System.out.println("\tAudioOffset: " + ((e.getAudioOffset() + 5000) / 10000) + "ms");
                System.out.println("\tVisemeId: " + e.getVisemeId());
            });

            speechSynthesizer.WordBoundary.addEventListener((o, e) -> {
                System.out.println("WordBoundary event:");
                System.out.println("\tBoundaryType: " + e.getBoundaryType());
                System.out.println("\tAudioOffset: " + ((e.getAudioOffset() + 5000) / 10000) + "ms");
                System.out.println("\tDuration: " + e.getDuration());
                System.out.println("\tText: " + e.getText());
                System.out.println("\tTextOffset: " + e.getTextOffset());
                System.out.println("\tWordLength: " + e.getWordLength());
            });

            // Synthesize the SSML
            System.out.println("SSML to synthesize:");
            System.out.println(ssml);
            SpeechSynthesisResult speechSynthesisResult = speechSynthesizer.SpeakSsmlAsync(ssml).get();

            if (speechSynthesisResult.getReason() == ResultReason.SynthesizingAudioCompleted) {
                System.out.println("SynthesizingAudioCompleted result");
            }
            else if (speechSynthesisResult.getReason() == ResultReason.Canceled) {
                SpeechSynthesisCancellationDetails cancellation = SpeechSynthesisCancellationDetails.fromResult(speechSynthesisResult);
                System.out.println("CANCELED: Reason=" + cancellation.getReason());

                if (cancellation.getReason() == CancellationReason.Error) {
                    System.out.println("CANCELED: ErrorCode=" + cancellation.getErrorCode());
                    System.out.println("CANCELED: ErrorDetails=" + cancellation.getErrorDetails());
                    System.out.println("CANCELED: Did you set the speech resource key and region values?");
                }
            }
        }
        speechSynthesizer.close();

        System.exit(0);
    }
}

可在 GitHub 中找到更多文本转语音示例。

使用自定义终结点

从功能上说,自定义终结点与用于文本转语音请求的标准终结点相同。

区别在于,必须指定 EndpointId 才能通过语音 SDK 使用自定义语音。 可以从文本转语音快速入门开始,然后使用 EndpointIdSpeechSynthesisVoiceName 更新代码。

SpeechConfig speechConfig = SpeechConfig.fromSubscription(speechKey, speechRegion);
speechConfig.setSpeechSynthesisVoiceName("YourCustomVoiceName");
speechConfig.setEndpointId("YourEndpointId");

若要通过语音合成标记语言 (SSML) 使用定制声音,请将模型名称指定为语音名称。 本示例使用 YourCustomVoiceName 语音。

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="YourCustomVoiceName">
        This is the text that is spoken. 
    </voice>
</speak>

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | 包 (npm) | GitHub 上的其他示例 | 库源代码

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

选择合成语言和语音

语音服务中的文本转语音功能支持 400 多种语音和 140 多种语言和变体。 你可以获取完整列表,或在语音库中试用它们。

指定 SpeechConfig 的语言或语音,使其与输入文本匹配,并使用指定的语音:

function synthesizeSpeech() {
    const speechConfig = sdk.SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    // Set either the `SpeechSynthesisVoiceName` or `SpeechSynthesisLanguage`.
    speechConfig.speechSynthesisLanguage = "en-US"; 
    speechConfig.speechSynthesisVoiceName = "en-US-AvaMultilingualNeural";
}

synthesizeSpeech();

所有神经网络声音都是多语言的,并且能够流利地使用自己的语言和英语。 例如,如果英语的输入文本为“I'm excited to try text to speech”并且你选择了 es-ES-ElviraNeural,则该文本将用带西班牙口音的英语讲出。

如果语音没有使用输入文本的语言讲出,则语音服务不会创建合成音频。 有关支持的神经语音的完整列表,请参阅语音服务的语言和语音支持

注意

默认语音是从语音列表 API 根据区域设置返回的第一个语音。

所讲的语音按以下优先顺序确定:

  • 如果你没有设置 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会讲 en-US 的默认语音。
  • 如果你仅设置了 SpeechSynthesisLanguage,则会讲指定区域设置的默认语音。
  • 如果同时设置了 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会忽略 SpeechSynthesisLanguage 设置。 系统会讲你使用 SpeechSynthesisVoiceName 指定的语音。
  • 如果使用语音合成标记语言 (SSML) 设置了 voice 元素,则会忽略 SpeechSynthesisVoiceNameSpeechSynthesisLanguage 设置。

将文本合成为语音

若要将合成语音输出到当前活动输出设备(例如扬声器),请使用 fromDefaultSpeakerOutput() 静态函数实例化 AudioConfig。 下面是一个示例:

function synthesizeSpeech() {
    const speechConfig = sdk.SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    const audioConfig = sdk.AudioConfig.fromDefaultSpeakerOutput();

    const speechSynthesizer = new SpeechSynthesizer(speechConfig, audioConfig);
    speechSynthesizer.speakTextAsync(
        "I'm excited to try text to speech",
        result => {
            if (result) {
                speechSynthesizer.close();
                return result.audioData;
            }
        },
        error => {
            console.log(error);
            speechSynthesizer.close();
        });
}

运行程序时,扬声器会播放合成音频。 此结果是最基本用法的一个很好示例。 接下来,可以自定义输出,并将输出响应作为适用于自定义方案的内存中流进行处理。

获取内存中流形式的结果

可将生成的音频数据用作内存中流,而不是直接写入到文件。 使用内存中流可以构建自定义行为:

  • 抽取生成的字节数组,作为自定义下游服务的可搜寻流。
  • 将结果与其他 API 或服务集成。
  • 修改音频数据,编写自定义 .wav 标头,并执行相关任务。

可以对上一个示例进行此更改。 删除 AudioConfig 块,因为从现在起,你将手动管理输出行为,以提高控制度。 然后在 SpeechSynthesizer 构造函数中为 AudioConfig 传递 null

注意

如果为 AudioConfig 传递 null,而不是像在前面的扬声器输出示例中那样省略它,则默认不会在当前处于活动状态的输出设备上播放音频。

将结果保存到 SpeechSynthesisResult 变量。 SpeechSynthesisResult.audioData 属性返回输出数据的 ArrayBuffer 值,即默认的浏览器流类型。 对于服务器端代码,请将 ArrayBuffer 转换为缓冲区流。

以下代码适用于客户端:

function synthesizeSpeech() {
    const speechConfig = sdk.SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    const speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig);

    speechSynthesizer.speakTextAsync(
        "I'm excited to try text to speech",
        result => {
            speechSynthesizer.close();
            return result.audioData;
        },
        error => {
            console.log(error);
            speechSynthesizer.close();
        });
}

可以使用生成的 ArrayBuffer 对象来实现任何自定义行为。 ArrayBuffer 是浏览器中接收的并使用此格式播放的常见类型。

对于基于服务器的任何代码,如果需要以流的形式使用数据,则需要将 ArrayBuffer 对象转换为流:

function synthesizeSpeech() {
    const speechConfig = sdk.SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
    const speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig);

    speechSynthesizer.speakTextAsync(
        "I'm excited to try text to speech",
        result => {
            const { audioData } = result;

            speechSynthesizer.close();

            // convert arrayBuffer to stream
            // return stream
            const bufferStream = new PassThrough();
            bufferStream.end(Buffer.from(audioData));
            return bufferStream;
        },
        error => {
            console.log(error);
            speechSynthesizer.close();
        });
}

自定义音频格式

你可以自定义音频输出属性,包括:

  • 音频文件类型
  • 采样速率
  • 位深度

若要更改音频格式,请使用 SpeechConfig 对象的 speechSynthesisOutputFormat 属性。 此属性需要一个 SpeechSynthesisOutputFormat 类型的 enum 实例。 使用 enum 选择输出格式。 有关可用格式,请参阅音频格式列表

可根据要求对不同的文件类型使用不同的选项。 根据定义,Raw24Khz16BitMonoPcm 等原始格式不包括音频标头。 仅在以下任一情况下使用原始格式:

  • 你知道下游实现可以解码原始位流。
  • 你打算基于位深度、采样率、通道数等因素手动生成标头。

此示例通过对 SpeechConfig 对象设置 speechSynthesisOutputFormat 来指定高保真 RIFF 格式 Riff24Khz16BitMonoPcm。 与上一部分中的示例类似,获取音频 ArrayBuffer 数据并与之进行交互。

function synthesizeSpeech() {
    const speechConfig = SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");

    // Set the output format
    speechConfig.speechSynthesisOutputFormat = sdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm;

    const speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig, null);
    speechSynthesizer.speakTextAsync(
        "I'm excited to try text to speech",
        result => {
            // Interact with the audio ArrayBuffer data
            const audioData = result.audioData;
            console.log(`Audio data byte size: ${audioData.byteLength}.`)

            speechSynthesizer.close();
        },
        error => {
            console.log(error);
            speechSynthesizer.close();
        });
}

使用 SSML 自定义语音特征

借助 SSML,可以通过从 XML 架构中提交请求,来微调文本转语音输出的音节、发音、语速、音量和其他方面。 本部分展示了更改语音的示例。 有关详细信息,请参阅语音合成标记语言概述

若要开始使用 SSML 进行自定义,需要进行一个小的更改来切换语音。

  1. 在根项目目录中为 SSML 配置创建一个新的 XML 文件。

    <speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="en-US">
      <voice name="en-US-AvaMultilingualNeural">
        When you're on the freeway, it's a good idea to use a GPS.
      </voice>
    </speak>
    

    在此示例中,它是 ssml.xml。 根元素始终是 <speak>。 通过将文本包装在 <voice> 元素中,可以使用 name 参数来更改语音。 有关支持的神经语音的完整列表,请参阅支持的语言

  2. 更改语音合成请求以引用 XML 文件。 该请求基本上保持不变,只不过需要使用 speakSsmlAsync() 而不是 speakTextAsync() 函数。 此函数需要 XML 字符串。 创建一个加载 XML 文件并将其作为字符串返回的函数:

    function xmlToString(filePath) {
        const xml = readFileSync(filePath, "utf8");
        return xml;
    }
    

    有关 readFileSync 的详细信息,请参阅 Node.js 文件系统

    结果对象与前面的示例完全相同:

    function synthesizeSpeech() {
        const speechConfig = sdk.SpeechConfig.fromSubscription("YourSpeechKey", "YourSpeechRegion");
        const speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig, null);
    
        const ssml = xmlToString("ssml.xml");
        speechSynthesizer.speakSsmlAsync(
            ssml,
            result => {
                if (result.errorDetails) {
                    console.error(result.errorDetails);
                } else {
                    console.log(JSON.stringify(result));
                }
    
                speechSynthesizer.close();
            },
            error => {
                console.log(error);
                speechSynthesizer.close();
            });
    }
    

注意

若要在不使用 SSML 的情况下更改语音,可使用 SpeechConfig.speechSynthesisVoiceName = "en-US-AvaMultilingualNeural";SpeechConfig 上设置属性。

订阅合成器事件

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

在对文本转语音使用 SpeechSynthesizer 时,可以订阅此表中的事件:

事件 说明 用例
BookmarkReached 指示已进入书签。 若要触发书签进入事件,需要在 SSML 中指定一个 bookmark 元素。 此事件会报告输出音频在合成开始处到 bookmark 元素之间经历的时间。 该事件的 Text 属性是在书签的 mark 特性中设置的字符串值。 不会说出 bookmark 元素。 可以使用 bookmark 元素在 SSML 中插入自定义标记,以获得音频流中每个标记的偏移量。 bookmark 元素可用于引用文本或标记序列中的特定位置。
SynthesisCanceled 指示已取消语音合成。 可在合成取消后进行确认。
SynthesisCompleted 指示语音合成已完成。 可在合成完成后进行确认。
SynthesisStarted 表示已启动语音合成。 可在合成启动后进行确认。
Synthesizing 指示语音合成正在进行。 每次 SDK 从语音服务收到音频区块,都会触发此事件。 你可以确认合成何时正在进行。
VisemeReceived 指示已收到嘴形视位事件。 视素通常用于表示观察到的语音中的关键姿态。 关键姿态包括在产生特定音素时嘴唇、下巴和舌头的位置。 可以使用嘴形视位来来动画显示播放语音音频时人物的面部。
WordBoundary 指示已收到字边界。 在每个新的讲述字词、标点和句子的开头引发此事件。 该事件报告当前字词从输出音频开始的时间偏移(以时钟周期为单位)。 此事件还会报告输入文本或 SSML 中紧靠在要说出的字词之前的字符位置。 此事件通常用于获取文本和相应音频的相对位置。 你可能想要知道某个新字词,然后根据时间采取操作。 例如,可以获取所需的信息来帮助确定在说出字词时,何时突出显示这些字词,以及要突出显示多长时间。

注意

事件在输出音频数据变为可用时引发,这样将会比播放到输出设备更快。 调用方必须相应地实时同步流式处理。

以下示例演示如何订阅语音合成的事件。 可以按照快速入门中的说明操作,但请将该 SpeechSynthesis.js 文件的内容替换为以下 JavaScript 代码。

(function() {

    "use strict";

    var sdk = require("microsoft-cognitiveservices-speech-sdk");

    var audioFile = "YourAudioFile.wav";
    // This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
    const speechConfig = sdk.SpeechConfig.fromSubscription(process.env.SPEECH_KEY, process.env.SPEECH_REGION);
    const audioConfig = sdk.AudioConfig.fromAudioFileOutput(audioFile);

    var speechSynthesisVoiceName  = "en-US-AvaMultilingualNeural";  
    var ssml = `<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'> \r\n \
        <voice name='${speechSynthesisVoiceName}'> \r\n \
            <mstts:viseme type='redlips_front'/> \r\n \
            The rainbow has seven colors: <bookmark mark='colors_list_begin'/>Red, orange, yellow, green, blue, indigo, and violet.<bookmark mark='colors_list_end'/>. \r\n \
        </voice> \r\n \
    </speak>`;
    
    // Required for WordBoundary event sentences.
    speechConfig.setProperty(sdk.PropertyId.SpeechServiceResponse_RequestSentenceBoundary, "true");

    // Create the speech speechSynthesizer.
    var speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig);

    speechSynthesizer.bookmarkReached = function (s, e) {
        var str = `BookmarkReached event: \
            \r\n\tAudioOffset: ${(e.audioOffset + 5000) / 10000}ms \
            \r\n\tText: \"${e.text}\".`;
        console.log(str);
    };

    speechSynthesizer.synthesisCanceled = function (s, e) {
        console.log("SynthesisCanceled event");
    };
    
    speechSynthesizer.synthesisCompleted = function (s, e) {
        var str = `SynthesisCompleted event: \
                    \r\n\tAudioData: ${e.result.audioData.byteLength} bytes \
                    \r\n\tAudioDuration: ${e.result.audioDuration}`;
        console.log(str);
    };

    speechSynthesizer.synthesisStarted = function (s, e) {
        console.log("SynthesisStarted event");
    };

    speechSynthesizer.synthesizing = function (s, e) {
        var str = `Synthesizing event: \
            \r\n\tAudioData: ${e.result.audioData.byteLength} bytes`;
        console.log(str);
    };
    
    speechSynthesizer.visemeReceived = function(s, e) {
        var str = `VisemeReceived event: \
            \r\n\tAudioOffset: ${(e.audioOffset + 5000) / 10000}ms \
            \r\n\tVisemeId: ${e.visemeId}`;
        console.log(str);
    };

    speechSynthesizer.wordBoundary = function (s, e) {
        // Word, Punctuation, or Sentence
        var str = `WordBoundary event: \
            \r\n\tBoundaryType: ${e.boundaryType} \
            \r\n\tAudioOffset: ${(e.audioOffset + 5000) / 10000}ms \
            \r\n\tDuration: ${e.duration} \
            \r\n\tText: \"${e.text}\" \
            \r\n\tTextOffset: ${e.textOffset} \
            \r\n\tWordLength: ${e.wordLength}`;
        console.log(str);
    };

    // Synthesize the SSML
    console.log(`SSML to synthesize: \r\n ${ssml}`)
    console.log(`Synthesize to: ${audioFile}`);
    speechSynthesizer.speakSsmlAsync(ssml,
        function (result) {
      if (result.reason === sdk.ResultReason.SynthesizingAudioCompleted) {
        console.log("SynthesizingAudioCompleted result");
      } else {
        console.error("Speech synthesis canceled, " + result.errorDetails +
            "\nDid you set the speech resource key and region values?");
      }
      speechSynthesizer.close();
      speechSynthesizer = null;
    },
        function (err) {
      console.trace("err - " + err);
      speechSynthesizer.close();
      speechSynthesizer = null;
    });
}());

可在 GitHub 中找到更多文本转语音示例。

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | 包(下载) | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

先决条件

安装语音 SDK 和示例

Azure-Samples/cognitive-services-speech-sdk 库包含适用于 iOS 和 Mac 且以 Objective-C 编写的示例。 选择链接可查看每个示例的安装说明:

使用自定义终结点

从功能上说,自定义终结点与用于文本转语音请求的标准终结点相同。

区别在于,必须指定 EndpointId 才能通过语音 SDK 使用自定义语音。 可以从文本转语音快速入门开始,然后使用 EndpointIdSpeechSynthesisVoiceName 更新代码。

SPXSpeechConfiguration *speechConfig = [[SPXSpeechConfiguration alloc] initWithSubscription:speechKey region:speechRegion];
speechConfig.speechSynthesisVoiceName = @"YourCustomVoiceName";
speechConfig.EndpointId = @"YourEndpointId";

若要通过语音合成标记语言 (SSML) 使用定制声音,请将模型名称指定为语音名称。 本示例使用 YourCustomVoiceName 语音。

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="YourCustomVoiceName">
        This is the text that is spoken. 
    </voice>
</speak>

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | 包(下载) | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

先决条件

安装语音 SDK 和示例

Azure-Samples/cognitive-services-speech-sdk 库包含适用于 iOS 和 Mac 且以 Swift 编写的示例。 选择链接可查看每个示例的安装说明:

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

参考文档 | 包 (PyPi) | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

选择合成语言和语音

语音服务中的文本转语音功能支持 400 多种语音和 140 多种语言和变体。 你可以获取完整列表,或在语音库中试用它们。

指定 SpeechConfig 的语言或语音,使其与输入文本匹配,并使用指定的语音:

# Set either the `SpeechSynthesisVoiceName` or `SpeechSynthesisLanguage`.
speech_config.speech_synthesis_language = "en-US" 
speech_config.speech_synthesis_voice_name ="en-US-AvaMultilingualNeural"

所有神经网络声音都是多语言的,并且能够流利地使用自己的语言和英语。 例如,如果英语的输入文本为“I'm excited to try text to speech”并且你选择了 es-ES-ElviraNeural,则该文本将用带西班牙口音的英语讲出。

如果语音没有使用输入文本的语言讲出,则语音服务不会创建合成音频。 有关支持的神经语音的完整列表,请参阅语音服务的语言和语音支持

注意

默认语音是从语音列表 API 根据区域设置返回的第一个语音。

所讲的语音按以下优先顺序确定:

  • 如果你没有设置 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会讲 en-US 的默认语音。
  • 如果你仅设置了 SpeechSynthesisLanguage,则会讲指定区域设置的默认语音。
  • 如果同时设置了 SpeechSynthesisVoiceNameSpeechSynthesisLanguage,则会忽略 SpeechSynthesisLanguage 设置。 系统会讲你使用 SpeechSynthesisVoiceName 指定的语音。
  • 如果使用语音合成标记语言 (SSML) 设置了 voice 元素,则会忽略 SpeechSynthesisVoiceNameSpeechSynthesisLanguage 设置。

将语音合成到文件中

创建一个 SpeechSynthesizer 对象。 此对象返回文本到语音的转换,并将转换结果输出到扬声器、文件或其他输出流。 SpeechSynthesizer 接受以下对象作为参数:

  1. 创建一个 AudioOutputConfig 实例,以使用 filename 构造函数参数自动将输出写入到 .wav 文件:

    audio_config = speechsdk.audio.AudioOutputConfig(filename="path/to/write/file.wav")
    
  2. 通过将 speech_config 对象和 audio_config 对象作为参数传递来实例化 SpeechSynthesizer。 若要合成语音并写入文件,请使用文本字符串运行 speak_text_async()

    speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=audio_config)
    speech_synthesizer.speak_text_async("I'm excited to try text to speech")
    

运行程序时,它会创建一个合成的 .wav 文件,该文件将写入到你指定的位置。 此结果是最基本用法的一个很好示例。 接下来,可以自定义输出,并将输出响应作为适用于自定义方案的内存中流进行处理。

合成到扬声器输出

若要将合成语音输出到当前活动输出设备(例如扬声器),请在创建 AudioOutputConfig 实例时设置 use_default_speaker 参数。 下面是一个示例:

audio_config = speechsdk.audio.AudioOutputConfig(use_default_speaker=True)

获取内存中流形式的结果

可将生成的音频数据用作内存中流,而不是直接写入到文件。 使用内存中流可以构建自定义行为:

  • 抽取生成的字节数组,作为自定义下游服务的可搜寻流。
  • 将结果与其他 API 或服务集成。
  • 修改音频数据,编写自定义 .wav 标头,并执行相关任务。

可以对上一个示例进行此更改。 首先删除 AudioConfig,因为从现在起,你将手动管理输出行为,以提高控制度。 在 SpeechSynthesizer 构造函数中为 AudioConfig 传递 None

注意

如果为 AudioConfig 传递 None,而不是像在前面的扬声器输出示例中那样省略它,则默认不会在当前处于活动状态的输出设备上播放音频。

将结果保存到 SpeechSynthesisResult 变量。 audio_data 属性包含输出数据的 bytes 对象。 可以手动使用此对象,也可以使用 AudioDataStream 类来管理内存中流。

在此示例中,使用 AudioDataStream 构造函数获取结果中的流:

speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None)
result = speech_synthesizer.speak_text_async("I'm excited to try text to speech").get()
stream = AudioDataStream(result)

此时,可以使用生成的 stream 对象来实现任何自定义行为。

自定义音频格式

你可以自定义音频输出属性,包括:

  • 音频文件类型
  • 采样速率
  • 位深度

若要更改音频格式,请对 SpeechConfig 对象使用 set_speech_synthesis_output_format() 函数。 此函数需要一个 SpeechSynthesisOutputFormat 类型的 enum 实例。 使用 enum 选择输出格式。 有关可用格式,请参阅音频格式列表

可根据要求对不同的文件类型使用不同的选项。 根据定义,Raw24Khz16BitMonoPcm 等原始格式不包括音频标头。 仅在以下任一情况下使用原始格式:

  • 你知道下游实现可以解码原始位流。
  • 你打算基于位深度、采样率、通道数等因素手动生成标头。

此示例通过对 SpeechConfig 对象设置 SpeechSynthesisOutputFormat 来指定高保真 RIFF 格式 Riff24Khz16BitMonoPcm。 类似于上一部分中的示例,可以使用 AudioDataStream 获取结果的内存中流,然后将其写入文件。

speech_config.set_speech_synthesis_output_format(speechsdk.SpeechSynthesisOutputFormat.Riff24Khz16BitMonoPcm)
speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None)

result = speech_synthesizer.speak_text_async("I'm excited to try text to speech").get()
stream = speechsdk.AudioDataStream(result)
stream.save_to_wav_file("path/to/write/file.wav")

运行程序时,它会将 .wav 文件写入指定路径。

使用 SSML 自定义语音特征

借助 SSML,可以通过从 XML 架构中提交请求,来微调文本转语音输出的音节、发音、语速、音量和其他方面。 本部分展示了更改语音的示例。 有关详细信息,请参阅语音合成标记语言概述

若要开始使用 SSML 进行自定义,需要进行一个小的更改来切换语音。

  1. 在根项目目录中为 SSML 配置创建一个新的 XML 文件。

    <speak version="1.0" xmlns="https://www.w3.org/2001/10/synthesis" xml:lang="en-US">
      <voice name="en-US-AvaMultilingualNeural">
        When you're on the freeway, it's a good idea to use a GPS.
      </voice>
    </speak>
    

    在此示例中,文件为 ssml.xml。 根元素始终是 <speak>。 通过将文本包装在 <voice> 元素中,可以使用 name 参数来更改语音。 有关支持的神经语音的完整列表,请参阅支持的语言

  2. 更改语音合成请求以引用 XML 文件。 请求大致相同。 请使用 speak_ssml_async(),而不是使用 speak_text_async() 函数。 此函数需要 XML 字符串。 首先,将 SSML 配置读取为字符串。 此时,结果对象与前面的示例完全相同。

    注意

    如果 ssml_string 在字符串开头包含 ,则需要去除 BOM 格式,否则服务会返回错误。 为此,可按如下所示设置 encoding 参数:open("ssml.xml", "r", encoding="utf-8-sig")

    speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=None)
    
    ssml_string = open("ssml.xml", "r").read()
    result = speech_synthesizer.speak_ssml_async(ssml_string).get()
    
    stream = speechsdk.AudioDataStream(result)
    stream.save_to_wav_file("path/to/write/file.wav")
    

注意

若要在不使用 SSML 的情况下更改语音,可使用 speech_config.speech_synthesis_voice_name = "en-US-AvaMultilingualNeural"SpeechConfig 上设置属性。

订阅合成器事件

你可能想要了解更多关于文本转语音处理和结果的见解。 例如,你可能想知道合成器何时启动和停止,或者在合成过程中遇到了其他哪些事件。

在对文本转语音使用 SpeechSynthesizer 时,可以订阅此表中的事件:

事件 说明 用例
BookmarkReached 指示已进入书签。 若要触发书签进入事件,需要在 SSML 中指定一个 bookmark 元素。 此事件会报告输出音频在合成开始处到 bookmark 元素之间经历的时间。 该事件的 Text 属性是在书签的 mark 特性中设置的字符串值。 不会说出 bookmark 元素。 可以使用 bookmark 元素在 SSML 中插入自定义标记,以获得音频流中每个标记的偏移量。 bookmark 元素可用于引用文本或标记序列中的特定位置。
SynthesisCanceled 指示已取消语音合成。 可在合成取消后进行确认。
SynthesisCompleted 指示语音合成已完成。 可在合成完成后进行确认。
SynthesisStarted 表示已启动语音合成。 可在合成启动后进行确认。
Synthesizing 指示语音合成正在进行。 每次 SDK 从语音服务收到音频区块,都会触发此事件。 你可以确认合成何时正在进行。
VisemeReceived 指示已收到嘴形视位事件。 视素通常用于表示观察到的语音中的关键姿态。 关键姿态包括在产生特定音素时嘴唇、下巴和舌头的位置。 可以使用嘴形视位来来动画显示播放语音音频时人物的面部。
WordBoundary 指示已收到字边界。 在每个新的讲述字词、标点和句子的开头引发此事件。 该事件报告当前字词从输出音频开始的时间偏移(以时钟周期为单位)。 此事件还会报告输入文本或 SSML 中紧靠在要说出的字词之前的字符位置。 此事件通常用于获取文本和相应音频的相对位置。 你可能想要知道某个新字词,然后根据时间采取操作。 例如,可以获取所需的信息来帮助确定在说出字词时,何时突出显示这些字词,以及要突出显示多长时间。

注意

事件在输出音频数据变为可用时引发,这样将会比播放到输出设备更快。 调用方必须相应地实时同步流式处理。

以下示例演示如何订阅语音合成的事件。 可以按照快速入门中的说明操作,但请将该 speech-synthesis.py 文件的内容替换为以下 Python 代码:

import os
import azure.cognitiveservices.speech as speechsdk

def speech_synthesizer_bookmark_reached_cb(evt: speechsdk.SessionEventArgs):
    print('BookmarkReached event:')
    print('\tAudioOffset: {}ms'.format((evt.audio_offset + 5000) / 10000))
    print('\tText: {}'.format(evt.text))

def speech_synthesizer_synthesis_canceled_cb(evt: speechsdk.SessionEventArgs):
    print('SynthesisCanceled event')

def speech_synthesizer_synthesis_completed_cb(evt: speechsdk.SessionEventArgs):
    print('SynthesisCompleted event:')
    print('\tAudioData: {} bytes'.format(len(evt.result.audio_data)))
    print('\tAudioDuration: {}'.format(evt.result.audio_duration))

def speech_synthesizer_synthesis_started_cb(evt: speechsdk.SessionEventArgs):
    print('SynthesisStarted event')

def speech_synthesizer_synthesizing_cb(evt: speechsdk.SessionEventArgs):
    print('Synthesizing event:')
    print('\tAudioData: {} bytes'.format(len(evt.result.audio_data)))

def speech_synthesizer_viseme_received_cb(evt: speechsdk.SessionEventArgs):
    print('VisemeReceived event:')
    print('\tAudioOffset: {}ms'.format((evt.audio_offset + 5000) / 10000))
    print('\tVisemeId: {}'.format(evt.viseme_id))

def speech_synthesizer_word_boundary_cb(evt: speechsdk.SessionEventArgs):
    print('WordBoundary event:')
    print('\tBoundaryType: {}'.format(evt.boundary_type))
    print('\tAudioOffset: {}ms'.format((evt.audio_offset + 5000) / 10000))
    print('\tDuration: {}'.format(evt.duration))
    print('\tText: {}'.format(evt.text))
    print('\tTextOffset: {}'.format(evt.text_offset))
    print('\tWordLength: {}'.format(evt.word_length))

# This example requires environment variables named "SPEECH_KEY" and "SPEECH_REGION"
speech_config = speechsdk.SpeechConfig(subscription=os.environ.get('SPEECH_KEY'), region=os.environ.get('SPEECH_REGION'))

# Required for WordBoundary event sentences.
speech_config.set_property(property_id=speechsdk.PropertyId.SpeechServiceResponse_RequestSentenceBoundary, value='true')

audio_config = speechsdk.audio.AudioOutputConfig(use_default_speaker=True)
speech_synthesizer = speechsdk.SpeechSynthesizer(speech_config=speech_config, audio_config=audio_config)

# Subscribe to events
speech_synthesizer.bookmark_reached.connect(speech_synthesizer_bookmark_reached_cb)
speech_synthesizer.synthesis_canceled.connect(speech_synthesizer_synthesis_canceled_cb)
speech_synthesizer.synthesis_completed.connect(speech_synthesizer_synthesis_completed_cb)
speech_synthesizer.synthesis_started.connect(speech_synthesizer_synthesis_started_cb)
speech_synthesizer.synthesizing.connect(speech_synthesizer_synthesizing_cb)
speech_synthesizer.viseme_received.connect(speech_synthesizer_viseme_received_cb)
speech_synthesizer.synthesis_word_boundary.connect(speech_synthesizer_word_boundary_cb)

# The language of the voice that speaks.
speech_synthesis_voice_name='en-US-AvaMultilingualNeural'

ssml = """<speak version='1.0' xml:lang='en-US' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='http://www.w3.org/2001/mstts'>
    <voice name='{}'>
        <mstts:viseme type='redlips_front'/>
        The rainbow has seven colors: <bookmark mark='colors_list_begin'/>Red, orange, yellow, green, blue, indigo, and violet.<bookmark mark='colors_list_end'/>.
    </voice>
</speak>""".format(speech_synthesis_voice_name)

# Synthesize the SSML
print("SSML to synthesize: \r\n{}".format(ssml))
speech_synthesis_result = speech_synthesizer.speak_ssml_async(ssml).get()

if speech_synthesis_result.reason == speechsdk.ResultReason.SynthesizingAudioCompleted:
    print("SynthesizingAudioCompleted result")
elif speech_synthesis_result.reason == speechsdk.ResultReason.Canceled:
    cancellation_details = speech_synthesis_result.cancellation_details
    print("Speech synthesis canceled: {}".format(cancellation_details.reason))
    if cancellation_details.reason == speechsdk.CancellationReason.Error:
        if cancellation_details.error_details:
            print("Error details: {}".format(cancellation_details.error_details))
            print("Did you set the speech resource key and region values?")

可在 GitHub 中找到更多文本转语音示例。

使用自定义终结点

从功能上说,自定义终结点与用于文本转语音请求的标准终结点相同。

区别在于,必须指定 endpoint_id 才能通过语音 SDK 使用自定义语音。 可以从文本转语音快速入门开始,然后使用 endpoint_idspeech_synthesis_voice_name 更新代码。

speech_config = speechsdk.SpeechConfig(subscription=os.environ.get('SPEECH_KEY'), region=os.environ.get('SPEECH_REGION'))
speech_config.endpoint_id = "YourEndpointId"
speech_config.speech_synthesis_voice_name = "YourCustomVoiceName"

若要通过语音合成标记语言 (SSML) 使用定制声音,请将模型名称指定为语音名称。 本示例使用 YourCustomVoiceName 语音。

<speak version="1.0" xmlns="http://www.w3.org/2001/10/synthesis" xml:lang="en-US">
    <voice name="YourCustomVoiceName">
        This is the text that is spoken. 
    </voice>
</speak>

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

语音转文本 REST API 参考 | 适用于短音频的语音转文本 REST API 参考 | GitHub 上的其他示例

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

先决条件

将文本转换为语音

请在命令提示符处运行以下命令。 将这些值插入到命令中:

  • 你的语音资源密钥
  • 你的语音资源区域

可能还需要更改以下值:

  • X-Microsoft-OutputFormat 标头值,该值控制音频输出格式。 可以在文本转语音 REST API 参考中查找受支持的音频输出格式列表。
  • 输出的语音。 若要获取可用于语音服务终结点的语音列表,请参阅语音列表 API
  • 输出文件。 在此示例中,我们将服务器的响应定向到名为 output.mp3 的文件中。
curl --location --request POST 'https://YOUR_RESOURCE_REGION.tts.speech.microsoft.com/cognitiveservices/v1' \
--header 'Ocp-Apim-Subscription-Key: YOUR_RESOURCE_KEY' \
--header 'Content-Type: application/ssml+xml' \
--header 'X-Microsoft-OutputFormat: audio-16khz-128kbitrate-mono-mp3' \
--header 'User-Agent: curl' \
--data-raw '<speak version='\''1.0'\'' xml:lang='\''en-US'\''>
    <voice name='\''en-US-AvaMultilingualNeural'\''>
        I am excited to try text to speech
    </voice>
</speak>' > output.mp3

在此操作指南中,你将了解进行文本转语音合成的常见设计模式。

有关以下领域的详细信息,请参阅什么是文本转语音?

  • 获取内存中流形式的响应。
  • 自定义输出采样率和比特率。
  • 使用语音合成标记语言 (SSML) 提交合成请求。
  • 使用神经网络声音。
  • 订阅事件并处理结果。

先决条件

下载和安装

请按照以下步骤操作,并参阅语音 CLI 快速入门,了解适用于你的平台的其他要求。

  1. 运行以下 .NET CLI 命令以安装语音 CLI:

    dotnet tool install --global Microsoft.CognitiveServices.Speech.CLI
    
  2. 运行以下命令以配置你的语音资源密钥和区域。 将 SUBSCRIPTION-KEY 替换为语音资源密钥,将 REGION 替换为语音资源区域。

    spx config @key --set SUBSCRIPTION-KEY
    spx config @region --set REGION
    

将语音合成到扬声器

现在,可以运行语音 CLI,以从文本合成语音。

  • 在控制台窗口中,更改为包含语音 CLI 二进制文件的目录。 然后,运行以下命令:

    spx synthesize --text "I'm excited to try text to speech"
    

该语音 CLI 将通过计算机扬声器生成英语的自然语言。

将语音合成到文件中

  • 运行以下命令,将扬声器的输出更改为 .wav 文件:

    spx synthesize --text "I'm excited to try text to speech" --audio output greetings.wav
    

该语音 CLI 将采用英语为 greetings.wav 音频文件生成自然语言。

运行和使用容器

语音容器提供通过语音 SDK 和语音 CLI 访问的基于 Websocket 的查询终结点 API。 默认情况下,语音 SDK 和语音 CLI 使用公共语音服务。 若要使用该容器,需要更改初始化方法。 使用容器主机 URL 而不是密钥和区域。

有关容器的详细信息,请参阅使用 Docker 安装和运行语音容器

后续步骤