[Universal Apps] Utilizando Text to Speech

Introducción

Una de las novedades más interesantes que tuvimos con el SDK de Windows Phone 8 fueron las Speech
APIs (Text to Speech, Speech to Text, Comandos de voz, etc.). Nos
permiten ofrecer en nuestras aplicaciones una mayor accesible a un mayor
número de usuarios con problemas de visión por ejemplo.

NOTA: En Windows Phone 7 teníamos la posibilidad
de de utilizar Text to Speech mediante el uso de Bing Speech API. Era
una opción mucho más limitada además que requería conexión a internet
(para conectar con los servicios de Bing).

En Windows Phone 8…

Recordemos como se utilizaba Text To Speech en Windows Phone 8.
Nuestro objetivo será añadir un TextBox donde se pueda escribir un texto
a leer junto a un botón para provocar la lectura del mismo. Tambien
añadiremos la posibilidad de elegir voz (masculina o femenina). Nos
centramos en el archivo MainPage.xaml para preparar la interfaz de
nuestra aplicación. Dentro del Grid principal de la aplicación
(ContentPanel) añadimos:

<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
     <StackPanel>
          <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
               <RadioButton x:Name="rbMale" Content="Masculino" IsChecked="true"/>
               <RadioButton x:Name="rbFemale" Content="Femenino"/>
          </StackPanel>
          <TextBox HorizontalAlignment="Left" Height="450" TextWrapping="Wrap" Width="450" Text="Prueba de lectura desde Windows Phone 8!" Name="inputTextBox"/>
          <Button Content="Leer" HorizontalAlignment="Left" Width="456"/>
     </StackPanel>
</Grid>

Al pulsar el botón:

SpeechSynthesizer synth = new SpeechSynthesizer();
  
var voices = InstalledVoices.All.Where(v => v.Language == "es-ES").OrderByDescending(v => v.Gender);
  
VoiceGender gender = VoiceGender.Male;
  
if (rbMale.IsChecked == true)
     gender = VoiceGender.Male;
else
     gender = VoiceGender.Female;
  
synth.SetVoice(voices.Where(v => v.Gender == gender).FirstOrDefault());
  
await synth.SpeakTextAsync(inputTextBox.Text);

Instanciamos un objeto de tipo SpeechSynthesizer. Con LINQ seleccionamos el conjunto de sintetizadores españoles de la clase InstalledVoices.
Utilizamos los CheckBox para determinar el género elegido para la voz.
Establecemos el sintetizador elegido utilizando el método SetVoice. Por último, bastará con llamar al método SpeakTextAsync que recibirá como parámetro el texto a sintetizar en voz.

NOTA: Debemos añadir la capacidad de Reconocimiento de voz (ID_CAP_SPEECH_RECOGNITION).

Ahora

En Windows Phone 8.1 asi como en Windows 8.1 contamos con una nueva
API compartida. Para aprender a utilizarla vamos a crear un nuevo
proyecto:

Utilizaremos el patrón MVVM en nuestra aplicación. En la carpeta ViewModels añadimos una clase llamada MainViewModel donde crearemos una propiedad pública de tipo string que contendrá el texto a sintetizar en voz:

public string Message
{
     get { return "Hola!.
Este ejemplo muestra como crear un servicio para poder realizar Text to
Speech en aplicaciones universales utilizando el patrón MVVM."
; }
}

En nuestra vista, añadiremos un TextBlock que bindearemos al texto para mostrarlo en pantalla:

<Grid>
     <TextBlock Text="{Binding Message}"
                TextWrapping="WrapWholeWords"
                FontSize="32"
                Margin="15"/>
</Grid>

Por supuesto, necesitaremos un botón para que al pulsarlo realicemos el
speech del mensaje. Añadiremos una CommandBar con un AppBarButton para
ello:

<Page.BottomAppBar>
     <CommandBar>
          <AppBarButton Label="speech">
               <AppBarButton.Icon>
                    <BitmapIcon UriSource="ms-appx:///Assets/Speech.png" />
               </AppBarButton.Icon>
          </AppBarButton>
     </CommandBar>
</Page.BottomAppBar>

Al pulsar el botón debemos realizar una acción, es decir, en nuestro lenguaje ejecutaremos un comando de nuestra viewmodel.

public class MainViewModel : ViewModelBase
{
 
}

Bindeamos un comando al AppBarButton además de pasar el mensaje como parámetro del comando:

<Page.BottomAppBar>
     <CommandBar>
          <AppBarButton Label="speech" Command="{Binding SpeechCommand}" CommandParameter="{Binding Message}">
               <AppBarButton.Icon>
                    <BitmapIcon UriSource="ms-appx:///Assets/Speech.png" />
               </AppBarButton.Icon>
          </AppBarButton>
     </CommandBar>
</Page.BottomAppBar>

Definimos el comando en nuestra viewmodel:

private ICommand _speechCommand;
 
public ICommand SpeechCommand
{
     get { return _speechCommand = _speechCommand ?? new DelegateCommandAsync<string>(SpeechCommandDelegate); }
}
 
public async Task SpeechCommandDelegate(string message)
{
 
}

En el comando recibimos el mensaje a leer como parámetro. Vamos a añadir
la parte importante, la sintetización de texto en voz. Para ello, vamos
a crear un servicio que inyectaremos por inyección de dependencias en
nuestra viewmodel y la usaremos en el comando. Definimos el servicio:

/// <summary>
/// SpeechService Contract.
/// </summary>
public interface ISpeechService
{
     /// <summary>
     /// Synthetize a text into a speech and pronounces it.
     /// </summary>
     /// <param name="text"></param>
     /// <returns></returns>
     Task TextToSpeech(string text);
 
     /// <summary>
     /// Stop the current speech running.
     /// </summary>
     void StopSpeech();
}

Como podéis ver, muy sencillo, un servicio con dos métodos, uno para comenzar a sintetizar texto en voz y otro para detenerlo.

/// <summary>
/// SpeechService Implementation.
/// Allow share data with another applications.
/// </summary>
public class SpeechService : ISpeechService
{
 
}

Pasamos a la definición del servicio en si. Para realizar text to speech utilizaremos el namespace Windows.Media.SpeechSynthesis:

//Consts
const VoiceGender gender = VoiceGender.Female;
 
//Variables
static private SpeechSynthesizer _speech = null;
MediaElement _mediaElement;
 
/// <summary>
/// Provides access to the functionality of an installed a speech synthesis engine.
/// </summary>
static private SpeechSynthesizer Speech
{
     get { return _speech ?? (_speech = InitializeSpeech()); }
}
 
/// <summary>
/// Initialize the SpeechSynthesizer.
/// </summary>
/// <returns></returns>
static private SpeechSynthesizer InitializeSpeech()
{
     // Initialize a new instance of the SpeechSynthesizer.
     var speech = new SpeechSynthesizer();
     var language = CultureInfo.CurrentCulture.ToString();
     var voices = SpeechSynthesizer.AllVoices.Where(v => v.Language == language).OrderByDescending(v => v.Gender);
     speech.Voice = voices.FirstOrDefault(v => v.Gender == gender);
 
     return speech;
}

Creamos una propiedad detipo SpeechSynthesizer.
Esta clase nos da acceso a la funcionalidad de síntesis de voz. Creamos
un método InitializeSpeech que se encarga de crear una nueva instancia
de la clase SpeechSynthesizer y de configurar los parámetros básicos.
Accedemos a la colección de voces instaladas en el dispositivo gracias a
la propiedad SpeechSynthesizer.AllVoices.
Obtenemos la cultura actual del dispositivo para seleccionar la
priemera voz de dicha cultura y del género (masculino o femenino)
indicado mediante una enumeración del tipo VoiceGender.
El método de inicialización lo llamaremos desde una propiedad pública
que devuelve un objeto de tipo SpeechSynthesizer incializado.

Pasamos al método TextToSpeech, protagonista de nuestro servicio:

/// <summary>
/// Synthetize a text into a speech and pronounces it.
/// </summary>
/// <param name="message">The message to be pronounced.</param>
public async Task TextToSpeech(string message)
{
     try
     {
          if (!string.IsNullOrEmpty(message))
          {
               // Speak a string.
               var result = await Speech.SynthesizeTextToStreamAsync(message);
               _mediaElement = new MediaElement();
               _mediaElement.SetSource(result, result.ContentType);
               _mediaElement.Play();
          }
     }
     catch (Exception ex)
     {
          Debug.WriteLine("SpeechService", ex);
     }
}

Accede a la propiedad Speech que devuelve el objeto de tipo SpeechSynthesizer que utilizamos para llamar al método SynthesizeTextToStreamAsync. Método asíncrono que devuelve un stream.

¿Que hacemos con el resultado?

Pues realmente con el stream podemos hacer una gran variedad de
procesos. Lo más simple sería asignarlo como fuente a un control de tipo
MediaElement y lanzamos la reproducción con el método Play.

Continuamos con el segundo método StopSpeech:

/// <summary>
/// Stop the current speech running.
/// </summary>
public void StopSpeech()
{
     if (_speech != null && _mediaElement != null)
          _mediaElement.Stop();
}

Como podemos ver en lsa líneas anteriores es sumamente sencillo, utilizamos el método Stop del control MediaElement que reproduce el audio para detenerlo.

Para utilizar el servicio lo registramos en nuestro contenedor Ioc y lo inyectamos en la viewmodel por el punto de entrada, el contructor:

private ISpeechService _speechService;
 
public MainViewModel(ISpeechService speechService)
{
     _speechService = speechService;
}

En el comando creado previamente utilizamos el servicio:

await _speechService.TextToSpeech(message);

Y… oops!. ¿Que ocurre?

Obtenemos una expceción de acceso denegado. Tranquilos, solo nos falta recordar que debemos añadir el micrófono como una de las capabilities marcadas:

Podéis descargar el ejemplo realizado a continuación:

Recordar, cualquier duda o sugerencia será bienvenida en los comentarios de la entrada.

Más información

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *