Junio de 2016

Volumen 31, número 6

Aplicaciones modernas: Modificación de audio en UWP

Por Frank La La

La Plataforma universal de Windows (UWP) tiene sofisticadas API para grabar audio y vídeo. Sin embargo, el conjunto de características no se limita a la grabación. En solo unas líneas de código, los desarrolladores pueden aplicar efectos especiales al audio en tiempo real. Los efectos como la reverberación y el eco están integrados en las API y son muy sencillos de utilizar. En este artículo, examinaré algunos de los conceptos básicos de la grabación de audio y la aplicación de efectos especiales. Crearé una aplicación para UWP que pueda grabar audio, guardarlo y aplicarle varios filtros y efectos especiales.

Configuración del proyecto para grabar audio

La grabación de audio requiere que la aplicación tenga permiso para acceder al micrófono, lo que requiere modificar el archivo de manifiesto de la aplicación. En el Explorador de soluciones, haga doble clic en el archivo Package.appxmanifest. Siempre será la raíz interna del proyecto.

Cuando se abra la ventana del editor de archivos del manifiesto de la aplicación, haga clic en la pestaña Funcionalidades. En el cuadro de lista Funcionalidades, seleccione la funcionalidad Micrófono. Esto permitirá que la aplicación acceda al micrófono del usuario final. Sin ella, la aplicación generará una excepción cuando intente acceder al micrófono.

Grabación de audio

Antes de comenzar a agregar efectos especiales al audio, primero querrá poder grabar audio. Es un proceso bastante sencillo. En primer lugar, agregue una clase al proyecto para encapsular todo el código de grabación de audio. Esta clase tendrá como nombre AudioRecorder. Dispondrá de métodos públicos para iniciar y detener la grabación, así como para reproducir el documento de audio que acaba de grabar. Para hacerlo, deberá agregar algunos miembros a la clase. El primero de ellos será MediaCapture, que ofrece funcionalidades para capturar audio, vídeo e imágenes de un dispositivo de captura, como un micrófono o una cámara web:

private MediaCapture _mediaCapture;

También querrá agregar un elemento InMemoryRandomAccessStream para capturar la entrada del micrófono en memoria.

private InMemoryRandomAccessStream _memoryBuffer;

Para hacer un seguimiento del estado de la grabación, agregará a la clase una propiedad Boolean accesible públicamente:

public bool IsRecording { get; set; }

La grabación de audio requiere que compruebe si ya está grabando y, si lo está, el código generará una excepción. En otro caso, deberá inicializar la secuencia de memoria, eliminar el archivo de grabación anterior e iniciar la grabación.

Como la clase MediaCapture ofrece varias funciones, tendrá que especificar que quiere capturar audio. Para ello, creará una instancia de MediaCaptureInitializationSettings. Después, el código crea una instancia de un objeto MediaCapture y pasa la instancia de MediaCaptureInitializationSettings al método InitializeAsync, como se muestra en la Figura 1.

Figura 1. Creación de una instancia de un objeto MediaCapture

public async void Record()
  {
  if (IsRecording)
  {
    throw new InvalidOperationException("Recording already in progress!");
  }
  await Initialize();
  await DeleteExistingFile();
  MediaCaptureInitializationSettings settings =
    new MediaCaptureInitializationSettings
  {
    StreamingCaptureMode = StreamingCaptureMode.Audio
  };
  _mediaCapture = new MediaCapture();
  await _mediaCapture.InitializeAsync(settings);
  await _mediaCapture.StartRecordToStreamAsync(
    MediaEncodingProfile.CreateMp3(AudioEncodingQuality.Auto), _memoryBuffer);
  IsRecording = true;
}

Por último, indicará al objeto MediaCapture que comience la grabación, pasará parámetros para indicar que grabe en formato MP3 y dónde se deben almacenar los datos.

La detención de la grabación necesita muchas menos líneas de código:

public async void StopRecording()
{
  await _mediaCapture.StopRecordAsync();
  IsRecording = false;
  SaveAudioToFile();
}

El método StopRecording hace tres cosas: indica al objeto Media­Capture que detenga la grabación, establece el estado de grabación a un valor de false y guarda los datos de la secuencia de audio en un archivo MP3 en el disco.

Almacenamiento de datos de audio en disco

Cuando los datos del audio capturado se encuentran en el elemento InMemoryRandom­AccessStream, querrá guardar su contenido en disco, como se muestra en la Figura 2. El almacenamiento de datos de audio de una secuencia en memoria requiere que copie el contenido en otra secuencia y luego inserte esos datos en el disco. Con las utilidades del espacio de nombres Windows.ApplicationModel.Package, podrá obtener la ruta de acceso del directorio de instalación de la aplicación (durante el desarrollo, será el directorio \bin\x86\Debug del proyecto). Es ahí donde querrá que se grabe el archivo. Puede modificar el código sin dificultad para que se almacene en otro sitio o permitir que el usuario decida dónde quiere guardar el archivo.

Figura 2. Almacenamiento de datos de audio en disco

private async void SaveAudioToFile()
{
  IRandomAccessStream audioStream = _memoryBuffer.CloneStream();
  StorageFolder storageFolder = Package.Current.InstalledLocation;
  StorageFile storageFile = await storageFolder.CreateFileAsync(
    DEFAULT_AUDIO_FILENAME, CreationCollisionOption.GenerateUniqueName);
  this._fileName = storageFile.Name;
  using (IRandomAccessStream fileStream =
    await storageFile.OpenAsync(FileAccessMode.ReadWrite))
  {
    await RandomAccessStream.CopyAndCloseAsync(
      audioStream.GetInputStreamAt(0), fileStream.GetOutputStreamAt(0));
    await audioStream.FlushAsync();
    audioStream.Dispose();
  }
}

Reproducción de audio

Ahora que ya tiene los datos de audio en un búfer en memoria y en el disco, tiene dos opciones desde las que puede reproducirlo: la memoria y el disco.

El código para reproducir el audio desde la memoria es muy sencillo. Cree una instancia del control MediaElement, establezca su origen como el búfer en memoria, pásele un tipo MIME y después llame al método Play.

public void Play()
{
  MediaElement playbackMediaElement = new MediaElement();
  playbackMediaElement.SetSource(_memoryBuffer, "MP3");
  playbackMediaElement.Play();
}

La reproducción desde el disco requiere algo de código adicional, ya que la apertura de archivos es una tarea asincrónica. Para que el subproceso de la interfaz de usuario se comunique con una tarea que se ejecuta en otro subproceso, deberá usar CoreDispatcher. CoreDispatcher envía mensajes entre el subproceso en el que está ejecutándose una parte del código y el subproceso de la interfaz de usuario. Con él, el código puede obtener el contexto de la interfaz de usuario de otro subproceso. Para leer una excelente descripción de CoreDispatcher, consulte la publicación de blog de David Crook sobre este tema en bit.ly/1SbJ6up.

Aparte de los pasos adicionales para controlar el código asincrónico, el método se parece al anterior en que utiliza el búfer en memoria:

public async Task PlayFromDisk(CoreDispatcher dispatcher)
{
  await dispatcher.RunAsync(CoreDispatcherPriority.Normal, async () =>
  {
    MediaElement playbackMediaElement = new MediaElement();
    StorageFolder storageFolder = Package.Current.InstalledLocation;
    StorageFile storageFile = await storageFolder.GetFileAsync(this._fileName);
    IRandomAccessStream stream = await storageFile.OpenAsync(FileAccessMode.Read);
    playbackMediaElement.SetSource(stream, storageFile.FileType);
    playbackMediaElement.Play();
  });
}

Creación de la interfaz de usuario

Con la clase AudioRecorder completa, lo único que falta es crear la interfaz de la aplicación. La interfaz de este proyecto es muy simple; tan solo es necesario un botón para grabar y otro para reproducir el audio grabado, como se muestra en la Figura 3. Por consiguiente, el código XAML es sencillo: un elemento TextBlock y un panel StackPanel con dos botones:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
  <Grid.RowDefinitions>
    <RowDefinition Height="43"/>
    <RowDefinition Height="*"/>
  </Grid.RowDefinitions>
<TextBlock FontSize="24">Audio in UWP</TextBlock>
<StackPanel HorizontalAlignment="Center" Grid.Row="1" >
  <Button Name="btnRecord" Click="btnRecord_Click">Record</Button>
  <Button Name="btnPlay" Click="btnPlay_Click">Play</Button>
</StackPanel>
</Grid>

Interfaz de usuario de AudioRecorder
Figura 3. Interfaz de usuario de AudioRecorder

En la clase codebehind, debe crear una variable de miembro de Audio­Recorder. Este será el objeto que utilice la aplicación para grabar y reproducir audio:

AudioRecorder _audioRecorder;

Debe crear una instancia de la clase AudioRecorder en el constructor del método MainPage de la aplicación:

public MainPage()
{
  this.InitializeComponent();
  this._audioRecorder = new AudioRecorder();
}

El botón btnRecord alterna el inicio y la detención de la grabación de audio. Para mantener al usuario informado del estado actual de AudioRecorder, el método btnRecord_Click cambia el contenido del botón btnRecord, además de iniciar y detener la grabación.

Tiene dos opciones para el controlador de eventos del botón btnPlay: reproducir desde el búfer en memoria o reproducir desde un archivo almacenado en disco.

Para reproducir desde el búfer en memoria, el código es sencillo:

private void btnPlay_Click(object sender, RoutedEventArgs e)
{
  this._audioRecorder.Play();
}

Como mencioné antes, la reproducción del archivo desde el disco se produce de forma asincrónica. Esto implica que la tarea se ejecutará en un subproceso distinto al de la interfaz de usuario. El programador del sistema operativo determinará el subproceso en el que se ejecutará la tarea en tiempo de ejecución. Si se pasa el objeto Dispatcher al método PlayFromDisk, se permite que el subproceso obtenga acceso al contexto de la interfaz de usuario del subproceso de la interfaz de usuario:

private async void btnPlay_Click(object sender, RoutedEventArgs e)
{
  await this._audioRecorder.PlayFromDisk(Dispatcher);
}

Aplicación de efectos especiales

Ahora que la aplicación graba y reproduce audio, ha llegado el momento de explorar algunas de las características menos conocidas de UWP: los efectos especiales de audio en tiempo real. En las API del espacio de nombres Windows.Media.Audio se incluyen una serie de efectos especiales que pueden agregar un toque complementario a las aplicaciones.

Para este proyecto, colocará todo el código de los efectos especiales en su propia clase. No obstante, antes de crear la nueva clase, realizará una última modificación a la clase AudioRecorder. Agregaré el método siguiente:

public async Task<StorageFile>
   GetStorageFile(CoreDispatcher dispatcher)
{
  StorageFolder storageFolder =
    Package.Current.InstalledLocation;
  StorageFile storageFile =
    await storageFolder.GetFileAsync(this._fileName);
  return storageFile;
}

El método GetStorageFile devuelve un objeto StorageFile al archivo de audio almacenado. Es así cómo accederá a los datos de audio la clase de efectos especiales.

Información sobre AudioGraph

La clase AudioGraph es fundamental para los escenarios de audio avanzado en UWP. Un elemento AudioGraph puede enrutar datos de audio de nodos de origen de entrada a nodos de origen de salida mediante distintos nodos de mezcla. En este artículo no abarcaré AudioGraph en toda su extensión, pero es algo en lo que tengo intención de profundizar en artículos futuros. Por ahora, lo importante es que a todos los nodos de un elemento AudioGraph se les pueden aplicar varios efectos de audio. Para más información sobre AudioGraph, asegúrese de leer el artículo sobre el Centro de desarrollo de Windows en bit.ly/1VCIBfD.

En primer lugar, querrá agregar una clase denominada AudioEffects al proyecto y agregar los miembros siguientes:

private AudioGraph _audioGraph;
private AudioFileInputNode _fileInputNode;
private AudioDeviceOutputNode _deviceOutputNode;

Para crear una instancia de la clase AudioGraph, es necesario crear un objeto AudioGraphSettings, que contiene los ajustes de configuración para el elemento AudioGraph. Después puede llamar al método AudioGraph.Create­Async y pasarle esos ajustes de configuración. El método CreateAsync devuelve un objeto CreateAudioGraphResult. Esta clase ofrece acceso al elemento AudioGraph creado y un valor de estado que indica si la creación del elemento AudioGraph se realizó correctamente o no.

También es necesario crear un nodo de salida para reproducir el audio. Para hacerlo, llame al método CreateDevice­OutputNodeAsync en la clase AudioGraph y establezca la variable de miembro a la propiedad DeviceOutputNode del elemento CreateAudioDeviceOutputNodeResult. Todo el código para inicializar AudioGraph y Audio­DeviceOutputNode reside en el método InitializeAudioGraph que se muestra a continuación:

public async Task InitializeAudioGraph()
{
  AudioGraphSettings settings = new AudioGraphSettings(AudioRenderCategory.Media);
  CreateAudioGraphResult result = await AudioGraph.CreateAsync(settings);
  this._audioGraph = result.Graph;
  CreateAudioDeviceOutputNodeResult outputDeviceNodeResult =
    await this._audioGraph.CreateDeviceOutputNodeAsync();
  _deviceOutputNode = outputDeviceNodeResult.DeviceOutputNode;
}

La reproducción de audio desde un objeto AudioGraph es sencilla; simplemente llame al método Play. Como el objeto AudioGraph es un miembro privado de la clase AudioEffects, deberá encapsular un método público a su alrededor para que sea accesible:

public void Play()
{
this._audioGraph.Start();
}

Ahora que ha creado el nodo de dispositivo de salida en el objeto Audio­Graph, deberá crear un nodo de entrada del archivo de audio almacenado en el disco. También deberá agregar una conexión saliente al elemento FileInputNode. En este caso, querrá que el nodo saliente sea el dispositivo de salida de audio. Eso es exactamente lo que se hace en el método LoadFileIntoGraph:

public async Task LoadFileIntoGraph(StorageFile audioFile)
{
  CreateAudioFileInputNodeResult audioFileInputResult =
    await this._audioGraph.CreateFileInputNodeAsync(audioFile);
  _fileInputNode = audioFileInputResult.FileInputNode;
  _fileInputNode.AddOutgoingConnection(_deviceOutputNode);
  CreateAndAddEchoEffect();
}

Observará también una referencia al método CreateAndAddEchoEffect, del cual hablaré a continuación.

Agregar el efecto de audio

Existen cuatro efectos de audio integrados en la API de AudioGraph: eco, reverberación, ecualizador y limitador. En este caso, quiere agregar un eco al sonido grabado. Agregar este efecto es tan sencillo como crear un objeto EchoEffectDefition y establecer las propiedades del efecto. Una vez creado, tendrá que agregar la definición del efecto a un nodo. En este caso, quiere agregar el efecto a _fileInputNode, que contiene los datos de audio grabados y almacenados en el disco:

private void CreateAndAddEchoEffect()
{
  EchoEffectDefinition echoEffectDefinition = new EchoEffectDefinition(this._audioGraph);
  echoEffectDefinition.Delay = 100.0f;
  echoEffectDefinition.WetDryMix = 0.7f;
  echoEffectDefinition.Feedback = 0.5f;
  _fileInputNode.EffectDefinitions.Add(echoEffectDefinition);
}

En resumen

Ahora que ya dispone de la clase AudioEffect completa, puede usarla desde la interfaz de usuario. Primero, agregará un botón a la página principal de la aplicación:

<Button Content="Play with Special Effect" Click="btnSpecialEffectPlay_Click" />

Y dentro del controlador de evento click, obtendrá el archivo donde está almacenado el audio, creará una instancia de la clase AudioEffects y la pasará al archivo de datos de audio. Cuando haya hecho todo eso, lo único necesario para reproducir el sonido es llamar al método Play:

private async void btnSpecialEffectPlay_Click(object sender, RoutedEventArgs e)
{
  var storageFile = await this._audioRecorder.GetStorageFile(Dispatcher);
  AudioEffects effects = new AudioEffects();
  await effects.InitializeAudioGraph();
  await effects.LoadFileIntoGraph(storageFile);
  effects.Play();
}

Puede ejecutar la aplicación y hacer clic en Record para grabar un pequeño audio. Para oírlo tal y como se grabó, haga clic en el botón Play. Para oír el mismo audio con un eco agregado, haga clic en "Play with Special Effect".

Resumen

UWP no solo tiene una amplia compatibilidad para capturar audio, sino que también dispone de extraordinarias características que permiten aplicar efectos especiales a elementos multimedia en tiempo real. Con la plataforma se incluyen varios efectos que se pueden aplicar a audio. Entre ellos se encuentran el eco, la reverberación, el ecualizador y el limitador. Estos efectos se pueden aplicar de forma individual o combinados de cualquier forma. El único límite es su imaginación.


Frank La Vignees un experto en tecnología del equipo Microsoft Technology and Civic Engagement, donde ayuda a que los usuarios aprovechen la tecnología a fin de crear una comunidad mejor. Escribe publicaciones con regularidad en FranksWorld.com y tiene un canal de YouTube llamado Frank’s World TV (youtube.com/FranksWorldTV).

Gracias a los siguientes expertos técnicos por revisar este artículo: Drew Batchelor y Jose Luis Manners