65.9K
CodeProject 正在变化。 阅读更多。
Home

一款可以为您朗读文本的应用

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.80/5 (20投票s)

2015年6月17日

CPOL

16分钟阅读

viewsIcon

31583

downloadIcon

632

在本文中,我将解释如何创建一个简单的 WPF 应用程序,该应用程序可以为您朗读文本;使用 .NET 框架的语音 API 和已安装的语音。

引言

在本文中,我将向您展示如何在 WPF 中创建一个应用程序,该应用程序可以很好地利用 .NET 框架中的语音 API,为我们提供的书面消息生成语音回复。此外,作为奖励,我还将展示如何更改语音及其语速。

今天是我的计算机科学学士课程的最后一天,昨天我还在想,我应该尝试一下文学或类似的东西,但我的脑子里只有一件事……*我该如何阅读所有那些教科书*?突然,我想,为什么不创建一个软件来为我输入的文本朗读呢,这样我就可以享受别人为我朗读了。因此,我创建了这个应用程序,并想与他人分享。 :)

该应用程序的源代码包含构建这个非常直观的朗读消息的应用程序所需的程序集和其他工具。源代码演示了如何更改语速、音频的语音以及如何在需要停止音频播放时停止朗读过程。

Application UI
应用程序界面。包含 TextBox 中的示例文本,默认语速(零),第一个
已安装的语音已选定,以及三个用于三个不同功能的按钮。

要求

阅读本文的要求是,有互联网连接和网络浏览器。但要使用该应用程序,您需要先构建该应用程序,因为我已经删除了所有二进制文件和对象文件,以便您可以在自己的平台上使用源代码并进行构建。我的环境是,

  1. Microsoft Visual Studio 2013(*Ultimate版*)
  2. .NET framework 4.5

您当然可以尝试在您自己的 IDE 和环境中运行该应用程序的源代码。*最多可能无法编译,**没什么大不了**!*

入门...

首先,我们需要知道我们的应用程序要做什么,或者程序实际上会为我们做什么。嗯,该程序是一个简单的输入/输出程序,其中输入是字符串文本,输出是我们将会听到的语音,这是我们提供给应用程序的消息的输出。

获取文本

输入只是我们想听的消息,或者整个文章。当然,它将是字符串类型的数据,如果我们想自己输入消息(段落或任何内容),我们将在我们的应用程序中使用 `TextBox` 控件来保存要朗读的消息内容。一个简单的 `TextBox` 控件就足够了,如果我们愿意,我们可以为其添加其他属性,使其非常适合我们的应用程序。对于我们的应用程序,以下 XAML 标记足以生成用于从用户那里获取输入的文本框。

<TextBox Name="text" Height="200" 
         Text="Hello there, enter some text and I would read it for you!"
         TextWrapping="Wrap"></TextBox>

这个输入部分非常简单,也很简短。最耗时的部分是语音部分,以及开始朗读、停止朗读或更改应用程序输出的事件。在本文中,我将向您展示两种输出类型,

  1. 通过默认设备朗读输出;在大多数情况下,这是扬声器、免提设备或其他设备,如果您在控制面板中进行了配置。此时适合朗读文本。
  2. 将输出保存为波形格式文件(.wav)作为音频,以便稍后播放。适合通过网络共享“文本转语音”文件或稍后播放。

继续阅读,源代码将非常直观,以便您可以通过阅读源代码来理解过程。

生成语音

首先,让我们谈谈输入部分。输入可以是任何内容,但最具体地说,由于我们将使用 `System.Speech` 命名空间,我们将尽量坚持尽可能多的特定于命名空间的方法和解决问题的最佳方式。此外,由于我们将识别任何输入,而我们的输入仅仅是一个纯字符串类型的消息,我们只需要包含 `System.Speech.Synthesis` 命名空间,该命名空间保存了*为用户朗读输出*所需的​​对象。现在我们对上下文有了了解;命名空间(`System.Speech.Synthesis`)和应用程序开发框架(**Windows Presentation Foundation**),我们现在可以继续输入和输出部分了。

在继续阅读本文之前,您应该知道的一件事是我们将只使用该命名空间中的一个对象来创建整个应用程序,`SpeechSynthesizer`。此对象继承自 `IDisposable` 接口,因此我们可以在完成工作后立即调用其 `Dispose` 方法。换句话说,我们可以使用 `using` 块来处理此对象。如下所示:

using (var reader = new SpeechSynthesizer()) {
   // Use the object, .NET framework automatically clear the resources.
}

但是,**不要这样写**。我们将在*Windows Presentation Foundation* 中使用该对象,它只使用一个线程来执行业务逻辑并更新用户界面。如果您以最高效的方式编写应用程序,如下所示:

using (var reader = new SpeechSynthesizer()) {
   // Get the message value
   reader.Speak(message); // Speak it out!
}

上面的代码将自行处理资源,一旦不再需要它们就会清除它们,它还将朗读消息。应用程序将按预期工作。但是,*应用程序会冻结*。在大多数情况下,Windows Presentation Foundation 会冻结,因为另一个函数或线程当前正在处理并且尚未返回到事件处理程序。按钮事件、网络资源访问、长时间循环以及类似我们的 `Speak` 函数的操作会冻结应用程序,朗读消息,然后将控件返回给线程以更新用户界面。

如果我使用 `SpeakAsync` 而不是 `Speak` 呢?

SpeechSynthesizer 提供了两个函数,`Speak` 和 `SpeakAsync`,可用于朗读我们传入的消息。如果使用 SpeakAsync 而不是 Speak,则甚至听不到任何声音(*如果使用上面的代码示例*)。这是因为,一旦代码命中 SpeakAsync,代码就会从调用它的地方返回,而不是执行整个函数然后继续,而是它(*执行下一个,然后下一个,然后*)在对象 `reader` 上调用 `Dispose` 函数。这使得其他线程无法访问该对象,因为该对象已被处置。

**请记住**:`SpeakAsync` 无法被 `await`。

这使您需要创建自己的函数来异步处理应用程序的朗读,同时允许用户仍然可以访问按钮和其他功能。继续阅读本文,最后我们将能够创建异步的后端代码(即,它不会冻结 UI 线程)并且还可以访问,以便可以停止音频,更改输出等等。

要完整了解 `SpeakAsync` 的工作原理,请参阅下图。


上图演示了用户如何编写一个完全高效且内存友好的代码来朗读一般文本,但仍然会遇到问题。步骤指南说明了为什么没有输出。

还有一件事……`Prompt` 与 `String`

`SpeechSynthesizer` 允许我们使用 `string` 类型值或 `Prompt` 对象来为用户生成语音响应。我们可以同时使用两者,并且响应*将相同*。

字符串是 .NET 框架中的一种数据类型,每个开发人员都理解字符串类型数据。而且,如果您使用字符串类型数据,如纯文本常量字符串,您将能够以与使用 Prompt 对象相同的方式生成语音。另一方面,`Prompt` 是 `System.Speech.Synthesis` 中存在的一个对象(类)类型。

string message = "Hello, world";
Prompt prompt = new Prompt("Hello, world");

reader.Speak(message); 
reader.Speak(prompt);

上面两个函数都会生成相同的输出,那么*区别在哪里*?区别在于字符串只是要朗读的纯文本,而 Prompt 是一个对象,它可以使用 `PromptBuilder` 对象生成,并且可以包含段落、预录音频文件、更改语音和/或渲染和朗读语音的语速的定义。

如果您想生成一个应用程序,例如,朗读两人之间的对话,您应该使用 `PromptBuilder`(而不是每次都创建一个 `Prompt`)。例如,

来自此链接的示例

“他现在在这儿了,”我喊道。“看在上帝的份上,赶快下来!一定要待在树林里,直到他完全进去。”

“我得走了,凯茜,”希斯克利夫说,试图挣脱他的同伴的怀抱。“我不会离开你的窗户五码……”

“一个小时,”他恳切地恳求道。

“一分钟都不行,”她回答道。

“我必须——林顿马上就上来了,”闯入者坚持道。”

在这种情况下,您将需要大量的提示,或者一个 `PromptBuilder`(连同段落、音频样本、语音和其他 Say-as 设置的定义)传递给 Prompt 构造函数,然后该构造函数会为我们的渲染目的创建一个新的 Prompt 对象。

直接将上述段落(或对话)作为字符串传递并不是一个好主意,更改输出或输入类型,或多次更改语音也不是一个高效的解决方案。在这些情况下,传递 Prompt 是高效的方式。而如果您只想朗读纯文本,如文章或段落,那么字符串就足够了。 :)

构建应用程序

在以上各节中,我已经为您解释了应用程序的后台,使其更容易理解。现在是时候使用对象,并构建一个可以为我们的文本输入生成音频输出的应用程序了。

创建窗口

在 WPF 框架中,您创建窗口或页面来渲染用户界面的控件。我们需要一些控件,

  1. `TextBox` 控件
    用于从用户那里获取输入文本。
  2. `Slider` 控件
    用于获取语音的语速。(*范围从 -**10** 到 **10***)
  3. `ComboBox` 控件
    用于获取朗读的语音。(*我们将将其绑定到当前安装的语音*)
  4. `Button` 控件
    用于触发不同的功能。(我们的应用程序中有三个:
    1. 朗读 — 用于朗读文本,发出声音。
    2. 停止 — 用于停止朗读过程。
    3. 保存 — 用于将输出保存到波形格式文件。

我应用程序中的 XAML 标记是,

<Window x:Class="ApplicationToRead.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Read out for me" Height="380" Width="525">
    <Grid Margin="10">
        <StackPanel>
            <TextBlock FontSize="23" HorizontalAlignment="Center" Margin="0, 0, 0, 10">
                  App that reads out for you
            </TextBlock>
            <TextBox Name="text" Height="200" Text="Hello there, enter some text and I would read it for you!" TextWrapping="Wrap"></TextBox>
            <TextBlock FontStyle="Italic">Reading rate</TextBlock>
            <Slider Minimum="-10" Maximum="10" Margin="80, -15, 0, 0"
                    Ticks="2" HorizontalAlignment="Left" Width="400"
                    TickFrequency="5" TickPlacement="BottomRight"
                    Name="slider"
                    ></Slider>
            <TextBlock FontStyle="Italic">Select voice</TextBlock>
            <ComboBox Margin="80, -18, 0, 0" Name="comboBox" ItemsSource="{Binding}"></ComboBox>
            <Grid Margin="10">
                <Grid.ColumnDefinitions>
                    <ColumnDefinition />
                    <ColumnDefinition />
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <Button Width="80" Name="read" Click="read_Click">Read</Button>
                <Button Width="80" Name="stop" Click="stop_Click" Grid.Column="1">Stop</Button>
                <Button Width="80" Name="save" Click="save_Click" Grid.Column="2">Save</Button>
            </Grid>
        </StackPanel>
    </Grid>
</Window>

GUI 已经在本文的介绍部分共享过了。您可以在那里查看。

后端代码

现在该编写后端代码了,这样我们的应用程序才能*真正为我们做一些有用的事情*。我们需要一些对象来保存应用程序的状态。

  1. 一个变量来存储应用程序的状态,即 `reader` 是否正在朗读。
    private bool reading { get; set; }
  2. 我们还需要在我们想要的时候停止朗读。因此,我们将创建一个私有的类似句柄的变量来存储当前朗读的提示。
    private Prompt activePrompt { get; set; }
  3. 我们还需要 SpeechSynthesizer 对象。
    private SpeechSynthesizer reader { get; set; }

如前所述,我们需要在应用程序的整个生命周期中都使用该对象,因为我们需要它来渲染响应并提供音频样本供我们收听。但是,如果我们每次都创建对象,那么我们就会面临两种情况。

  1. 在第一种情况下,我们将不得不使用 `Speak` 方法(而不是 `SpeakAsync`)。它将按我们的要求去做,它将确保在执行任何其他操作之前完全朗读文本。但这会带来其他问题,即我们的应用程序会冻结,直到整个文本都朗读完毕。**糟糕的应用程序编写方式**。
  2. 在第二种情况下,我们创建一个新对象(如上图所示的 `SpeakAsync`),并使用它来异步朗读文本,这解决了我们的应用程序冻结的问题。但它带来了另一个问题,用户听不到任何声音。这已经在上面的图像中解释过了,请阅读。

因此,我们剩下了需要创建一个可以通过不同函数访问的对象的情况。因此,`private` 对象是一个很好的合适选择。同时请记住,变量越少越好,这是一种内存效率高的解决方案,但一种创建过多对象并在一个或两个语句后删除它们的解决方案也不是一个好程序,因为它需要大量的 CPU 来管理内存。 CPU 也是一种资源,更少的 RAM + 更少的 CPU 是一个好的解决方案,只管理内存而浪费大量 CPU 也是最糟糕的模式。将它们一起管理以创建一个好的应用程序。

请注意,我们可以随时调用对象上的 `.Dispose` 函数,因此我们不一定需要 `using` 块,`using` 块只是一个快捷方式,让我们不必担心清理内存资源,而可以专注于如何使用对象。因此,我们移除了 using 块,并创建了一个私有对象,该对象可以被不同函数访问,并在应用程序关闭时被处置。

// Dispose the object manually when the app is closing.
System.ComponentModel.CancelEventHandler closingHandler = (sender, e) =>
{
    reader.Dispose();
};

this.Closing += closingHandler;

上面的代码为一个 lambda 表达式(*事实上它是一个 lambda 表达式*)附加了一个 `closingHandler` 作为 WPF 的 `Window` 对象的 `Closing` 事件的事件处理程序。然后它调用 Dispose 函数,以便在*不再需要时*处置该对象。因此,这对于*内存效率极客*来说将是“*哥们,拜托!*”。 :) 之后您可以使用 `-+` 运算符删除 `closingHandler`。

现在我们需要函数(作为 Button 控件的事件处理程序)来执行我们想要的操作,并进行一些额外的调整,使我们的应用程序能够工作。

选择已安装的语音

首先,我们需要列出我们现在拥有的语音。请注意,语音作为软件、库或实用程序进行安装。您只能使用已安装的语音,而不是您期望或想要听到的语音。为此,我们将选择语音。

// Binding the Combobox to the names
List<string> names = new List<string>();

// For every voice installed
foreach (var voice in reader.GetInstalledVoices())
{
    // Only add Enabled voices, otherwise it is of no use
    if (voice.Enabled)
    {
        names.Add(voice.VoiceInfo.Name);
    }
}

comboBox.DataContext = names; // Set the DataContext for binding purposes
comboBox.SelectedIndex = 0; // Select the first one

请注意,`InstalledVoice` 对象中有一个 `Enabled` 字段,它告诉您一个语音是否已启用(可供使用)或未启用。如果语音未启用,则不会被使用。这就是为什么我有一个条件来加载仅启用语音以供使用。在我的例子中,它们等于`Enabled` 标志为`true` 的语音。然后将列表绑定到我们拥有的 `comboBox`,这样我们的 `ComboBox` 现在就会显示已安装语音的名称。

在第一张图片中,您将看到**Microsoft David Desktop**,这是一个已安装的语音(不是我安装的,可能是 .NET 或 Microsoft.Speech 库,我不确定),还有另外两个,**Microsoft Hazel Desktop** 和 **Microsoft Zira Desktop**。此外,**Microsoft David Desktop** 会自动被选中,这是因为我们的代码;例如,*请参阅上面代码块的最后一行*。

朗读文本

在此函数中,我将向您展示可用于生成朗读文本的语音响应的代码。

请看下面的代码块,并阅读添加的注释,

private void read_Click(object sender, RoutedEventArgs e)
{
    // Call the speak function
    string message = text.Text;
    string voiceName = "";

    // Voice must be selected
    if (comboBox.SelectedIndex != -1)
    {
        voiceName = (comboBox.SelectedItem).ToString();
    }

    // Rate for the rendering
    int rate = (int)slider.Value;
    reader.Rate = rate;

    // Name must be full-qualified string
    reader.SelectVoice(voiceName);

    // Reads one paragraph or passage at a time. Why read two?
    if (!reading)
    {
        reader.SetOutputToDefaultAudioDevice();
    }
    else
    {
        MessageBox.Show("Previous reader is currently reading. Press 'Stop' to try stopping it and try again.");
        return; // return and stop doing anything
    }

    reading = true;
    // Get the prompt that is being read out right now; for stopping it later.
    activePrompt = reader.SpeakAsync(message);

    // Handle the event, remove when not required
    EventHandler<SpeakCompletedEventArgs> handler = (sander, ev) =>
    {
        reading = false;
    };

    reader.SpeakCompleted += handler;
}

因此,当用户按下“朗读”按钮时,它将设置朗读配置,然后将输出类型更改为默认音频设备;这可以通过控制面板更改,查找选项。在大多数情况下,默认设备是扬声器,否则是耳机(如果已连接)或其他类似设备。

我在本文中确实说过,我将向您展示如何通过扬声器(或*默认设备*)生成语音,或者生成波形格式文件中的音频样本,以便在网络上共享或稍后收听,或*出于任何其他目的*。为此,我已在此函数中隐式地将输出更改为扬声器,因为我们将在稍后的函数中更改输出类型为文件。继续阅读...

我只希望 Sander 对这个参数没有意见。:laugh: (附注:*Sander* 是一位非常优秀的作者,我喜欢读他的文章!)

停止朗读

在上面的代码中,您会看到每次启动语音时,都会捕获当前 Prompt 对象的句柄。稍后将使用该句柄来停止朗读过程。

以下事件处理程序可以完成此操作,

private void stop_Click(object sender, RoutedEventArgs e)
{
    // Stop the reading process
    if (reading)
    {
        reader.SpeakAsyncCancel(activePrompt);
    }
}

传入提示,并将其取消。如果您(*不知何故*)想允许朗读多个提示,那么您也可以调用 `SpeakAsyncCancelAll()`,它将取消所有正在运行的提示实例。

生成波形文件

此库的另一个用途是您可以生成音频样本,用于您的*文本转语音*输出。它可以跨网络共享,流式传输给您的用户,存储在文件系统中以备后用。也许文件生成还有许多其他用途。

我只会展示如何创建文件,您可以通过 `System.Diagnostics.Process.Start("file-path.wav");` 以编程方式收听它,或者通过 Windows 资源管理器打开它。

以下代码可以做到这一点,

private void save_Click(object sender, RoutedEventArgs e)
{
    string message = text.Text;

    // Store the audio
    if (!reading)
    {
        reader.SetOutputToWaveFile("E:\\MyAudioFile.wav");
        reader.SpeakAsync(message);
    }
    else
    {
        MessageBox.Show("Previous reader is currently reading. Press 'Stop' to try stopping it and try again.");
    }
}

它会将输出更改为文件(**请记住**:*程序将占用该文件,并且在程序引用该文件用于输出之前,其他程序将无法访问该文件*),并将音频写入文件。您以后可以随时收听该文件。

您还可以将输出作为流获取,请阅读MSDN 上`SpeechSynthesizer` 对象的文档以获取更多详细信息。

关注点

这是一个非常有趣的话题,因为我近两个月以来一直感到沮丧,我无法回答任何问题,写任何有用的东西,创造任何特别的东西。我记得 Bill Woodruff 曾告诉我不要过度关注声誉。而是要努力为社区提供帮助。

在撰写本文时,我学到了很多东西。我学到了很多关于语音、声音和其他文本转语音深度概念的知识。通过下载上面的项目样本来使用应用程序项目,与朋友分享,是的,别忘了下一步:

  1. 尝试将 PDF 文件加载到其中。
  2. 尝试使用 Prompt 对象创建对话朗读应用程序。 :)

祝大家好运,是的,编码愉快。 :) 希望这篇文章对您有所帮助。

版本控制

文章*第一个版本*。

第二个版本,事件处理程序已从直接的 lambda 表达式移至一个处理程序对象,该对象以后可以使用 `-+` 运算符移除。

第三个版本,移除了导致应用程序朗读多个实例并可能导致运行时错误的错误,如果在尝试更改输出时发生。

第四个版本,移除了文章中错误的 C# 代码段并进行了修复。

© . All rights reserved.