Compartilhar via



Junho de 2016

Volume 31 - Número 6

Aplicativos Modernos - Experimentando o áudio na UWP

Por Frank La La

A UWP (Plataforma Universal do Windows) possui APIs avançadas para gravação de áudio e de vídeo. No entanto, o conjunto de recursos não para na gravação. Com apenas algumas linhas de código, os desenvolvedores podem aplicar efeitos especiais ao áudio em tempo real. Efeitos como reverberação e eco são integrados à API e são bastante fáceis de implementar. Neste artigo, explorarei alguns conceitos básicos da gravação de áudio e aplicação de efeitos especiais. Criarei um aplicativo de UWP que pode gravar áudio, salvá-lo e aplicar vários filtros e efeitos especiais.

Configuração do projeto para gravar áudio

A gravação de áudio exige que o aplicativo tenha permissão para acessar o microfone e exige a modificação do arquivo de manifesto do aplicativo. No Gerenciador de Soluções, clique duas vezes no arquivo Package.appxmanifest. Ele sempre estará na raiz do projeto.

Assim que a janela do editor de arquivo de manifesto for aberta, clique na guia Recursos. Na caixa de listagem Recursos, verifique o recurso Microfone. Isso permitirá que seu aplicativo acesse o microfone do usuário final. Sem isso, seu aplicativo lançará uma exceção quando você tentar acessar o microfone.

Gravando áudio

Antes de você começar a adicionar efeitos especiais ao áudio, primeiro você precisa conseguir gravar o áudio. Isso é algo muito simples. Primeiro, adicione uma classe ao seu projeto para encapsular todo o código de gravação de áudio. Você chamará essa classe de AudioRecorder. Ela terá métodos públicos para começar e parar a gravação, e também para reproduzir o clipe de áudio que você acabou de gravar. Para fazer isso, será necessário adicionar alguns membros à sua classe. O primeiro será MediaCapture, que fornece recursos para captura de áudio, vídeo e imagens de um dispositivo de captura, como um microfone ou webcam:

private MediaCapture _mediaCapture;

Convém adicionar um InMemoryRandomAccessStream para capturar a entrada do microfone na memória:

private InMemoryRandomAccessStream _memoryBuffer;

Para acompanhar o estado de sua gravação, você adicionará um booliano de propriedade publicamente acessível à sua classe:

public bool IsRecording { get; set; }

A gravação do áudio exige que você verifique se já está gravando e, se estiver, o código emitirá uma exceção. Caso contrário, será necessário inicializar seu fluxo de memória, excluir o arquivo de gravação anterior e começar a gravação.

Como a classe MediaCapture fornece várias funções, será necessário especificar que você deseja capturar o áudio. Você criará uma instância de MediaCaptureInitializationSettings para fazer exatamente isso. Em seguida, o código cria uma instância de um objeto MediaCapture e passa o MediaCaptureInitializationSettings para o método InitializeAsync, como mostra a Figura 1.

Figura 1 - Criando uma instância de um 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 fim, você informará ao objeto MediaCapture para começar a gravação, passando parâmetros que informam que a gravação será no formato MP3 e onde armazenar os dados.

A interrupção da gravação exige muito menos linhas de código:

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

O método StopRecording faz três coisas: ele informa ao objeto Media­Capture para interromper a gravação, define o estado da gravação como falso e salva os dados do fluxo de áudio em um arquivo MP3 no disco.

Gravando dados de áudio em disco

Quando os dados do áudio capturado estiverem no InMemoryRandom­AccessStream, convém salvar o conteúdo no disco, como mostra a Figura 2. A gravação de dados de áudio de um fluxo in-memory exige a cópia do conteúdo sobre outro fluxo e depois o envio desses dados por push para o disco. Com os utilitários no namespace Windows.ApplicationModel.Package, você é capaz de obter o caminho até o diretório de instalação de seu aplicativo. (Durante o desenvolvimento, isso estará no diretório \bin\x86\Debug do projeto.) É nesse local que você deve gravar o arquivo. Você pode modificar facilmente o código para salvar em outro lugar ou fazer com que o usuário escolha onde salvar o arquivo.

Figura 2 - Gravando dados de áudio em 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();
  }
}

Reproduzindo áudio

Agora que seus dados de áudio estão dentro de um buffer na memória e no disco, você tem duas opções de reprodução: da memória e do disco.

O código para reprodução no áudio da memória é bastante simples. Você cria uma nova instância do controle MediaElement, define sua origem para o buffer na memória, passa um tipo MIME e chama o método Play.

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

A reprodução a partir do disco exige pouco código extra, pois abrir arquivos é uma tarefa assíncrona. Para que o thread da interface do usuário se comunique com uma tarefa em execução em outro thread, será necessário usar o CoreDispatcher. O CoreDispatcher envia mensagens entre o thread no qual uma determinada parte do código está em execução e o thread de interface do usuário. Com ele, o código pode obter o contexto da interface do usuário de outro thread. Para obter uma descrição excelente do CoreDispatcher, leia a postagem de blog do David Crook sobre o assunto em bit.ly/1SbJ6up.

Além das etapas extras para lidar com o código assíncrono, o método lembra o anterior que usa o buffer na memória:

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();
  });
}

Criando a interface do usuário

Com a classe AudioRecorder completa, a única coisa que resta fazer é compilar a interface para o aplicativo. A interface para esse projeto é bastante simples, pois tudo o que você precisa é de um botão para gravar e de um botão para reproduzir o áudio gravado, como mostra a Figura 3. Da mesma maneira, o XAML é simples: um TextBlock e um painel de pilha com dois botões:

<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>

Interface do usuário do AudioRecorder
Figura 3 - Interface do usuário do AudioRecorder

Na classe codebehind, você cria uma variável de membro do Audio­Recorder. Isso será o objeto usado por seu aplicativo para gravar e reproduzir o áudio:

AudioRecorder _audioRecorder;

Você instanciará a classe AudioRecorder no construtor da MainPage de seu aplicativo:

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

O botão btnRecord alterna o início e o término da gravação de áudio. Para manter o usuário informado sobre o estado atual do AudioRecorder, o método btnRecord_Click altera o conteúdo do botão btnRecord, e começa e termina a gravação.

Você tem duas opções para o manipulador de evento para o botão btnPlay: reproduzir a partir do buffer na memória ou a partir de um arquivo armazenado em disco.

Para reproduzir a partir do buffer na memória, o código é bem simples:

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

Como eu mencionei anteriormente, a reprodução do arquivo a partir do disco ocorre de forma assíncrona. Isso significa que a tarefa será executada em um thread diferente do thread de interface do usuário. O agendador do sistema operacional determinará em qual thread a tarefa será executada no tempo de execução. Passar o objeto Dispatcher para o método PlayFromDisk permite que o thread obtenha o acesso ao contexto da interface do usuário do thread de interface do usuário:

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

Aplicando efeitos especiais

Agora que seu aplicativo está gravando e reproduzindo áudio, chegou a hora de explorar alguns desses recursos menos conhecidos na UWP: efeitos especiais de áudio em tempo real. Há diversos efeitos especiais incluídos nas APIs no namespace Windows.Media.Audio que podem dar um toque extra aos aplicativos.

Para este projeto, você colocará todo o código de efeitos especiais em sua própria classe. No entanto, antes de você criar a nova classe, será necessário fazer uma última modificação na classe AudioRecorder. Adicionarei o seguinte método:

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

O método GetStorageFile retorna um objeto StorageFile ao arquivo de áudio salvo. É assim que minha classe de efeitos especiais acessará os dados de áudio.

Introduzindo o AudioGraph

A classe AudioGraph é central para os cenários de áudio avançado na UWP. Uma AudioGraph pode rotear dados de áudio dos nós de origem da entrada para os nós de origem da saída por meio de vários nós de mixagem. Todos os recursos e capacidade da AudioGraph estão além do escopo deste artigo, mas é algo que eu planejo detalhar em artigos futuros. Por enquanto, o ponto importante é que todo nó no gráfico de áudio pode ter vários efeitos de áudio aplicados. Para saber mais sobre AudioGraph, não deixe de ler o artigo no Centro de Desenvolvimento do Windows em bit.ly/1VCIBfD.

Primeiro, convém adicionar uma classe chamada AudioEffects ao seu projeto e adicionar os seguintes membros:

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

Para criar uma instância da classe AudioGraph, você precisa criar um objeto AudioGraphSettings, que contém as configurações para o AudioGraph. Em seguida, você pode chamar o método AudioGraph.Create­Async passando estas configurações. O método CreateAsync retorna um objeto CreateAudioGraphResult. Essa classe fornece acesso ao gráfico de áudio criado e um valor de status se a criação do gráfico de áudio tiver falhado ou não.

Você também precisa criar um nó de saída para reproduzir o áudio. Para fazer isso, chame o método CreateDeviceOutputNodeAsync na classe AudioGraph e defina a variável do membro para a propriedade DeviceOutputNode do CreateAudioDeviceOutputNodeResult. O código para inicializar o AudioGraph e o AudioDeviceOutputNode reside no método InitializeAudioGraph aqui:

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;
}

A reprodução de áudio de um objeto AudioGraph é fácil; basta chamar o método Play. Como o AudioGraph é um membro privado de sua classe AudioEffects, será necessário envolvê-lo em um método público para torná-lo acessível:

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

Agora que o nó do dispositivo de saída está criado no Audio­Graph, você precisa criar um nó de entrada a partir do arquivo de áudio armazenado no disco. Também será necessário adicionar uma conexão de saída ao FileInputNode. Nesse caso, convém que o nó de saída seja seu dispositivo de saída de áudio. É exatamente isso que você faz no método LoadFileIntoGraph:

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

Você também perceberá uma referência ao método CreateAndAddEchoEffect, que eu discutirei em seguida.

Adicionando o efeito de áudio

Há quatro efeitos de áudio internos na API do gráfico de áudio: eco, reverberação, equalizador e limitador. Nesse caso, convém adicionar um eco ao som gravado. A adição desse efeito é tão fácil quanto criar um objeto EchoEffectDefition e configurar as propriedades do efeito. Após a criação, será necessário adicionar a definição de efeito a um nó. Nesse caso, convém adicionar o efeito ao _fileInputNode, que contém os dados de áudio gravados e salvos no 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);
}

Unindo todos os elementos

Agora que a classe AudioEffect está concluída, você pode usá-la a partir da interface do usuário. Primeiro, adicione um botão à página principal de seu aplicativo:

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

E, dentro do manipulador de eventos de clique, você obtém o arquivo onde os dados de áudio estão armazenados, cria uma instância da classe AudioEffects e a passa ao arquivo de dados de áudio. Quando tudo isso estiver pronto, tudo o que você precisará fazer para reproduzir o som é chamar o 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();
}

Execute o aplicativo e clique em Record para gravar um pequeno clipe. Para escutá-lo da forma como foi gravado, clique no botão Play. Para escutar o mesmo áudio com a adição de um eco, clique em Play with Special Effect (Gavar com efeitos especiais).

Conclusão

A UWP não apenas tem um suporte avançado para captura de áudio, mas também tem alguns recursos incríveis para aplicar efeitos especiais à mídia em tempo real. Há vários efeitos incluídos com a plataforma que podem ser aplicados ao áudio. Entre eles: eco, reverberação, equalizador e limitador. Esses efeitos podem ser aplicados de forma individual ou em várias combinações. O único limite é sua imaginação.


Frank La Vigneé evangelista de tecnologia que faz parte da equipe de Tecnologia e Compromisso Cívico da Microsoft, onde ajuda os usuários a aproveitar a tecnologia para criar uma comunidade melhor. Ele tem um blog em FranksWorld.com e um canal do YouTube chamado “Frank’s World TV” (youtube.com/FranksWorldTV).

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Drew Batchelor e Jose Luis Manners