Fronteiras da interface do usuário

Gravação de sons no Windows Phone 7

Charles Petzold

Baixar o código de exemplo

Em um dos primeiros anúncios impressos da apresentação do Macintosh em 1984, a Apple elogiou o design de seu mouse com uma observação excepcionalmente convincente: “Alguns mouses têm dois botões. O Macintosh tem um. Portanto, é extremamente difícil pressionar o botão errado.”

Isso não é totalmente verdadeiro, é claro. Sobrecarregar um único botão com várias funções pode ser tão confuso como vários botões. Mas a possibilidade de pressionar o botão errado é certamente um argumento persuasivo para a simplicidade no design da interface do usuário.

Manter apenas as funções essenciais da interface do usuário é ainda mais importante ao programar para um smartphone. Os telefones não são muito grandes. Eles simplesmente não podem ter muitos botões, e os dedos que pressionam esses botões não são tão precisos como um mouse. Com botões em excesso é mais fácil ainda pressionar o botão errado.

O lado negativo é que limitar a interface do usuário limita a funcionalidade do programa e, portanto, decidir onde estabelecer limites é uma grande dificuldade. A vida é cheia de concessões.

Evolução do design

Pensei que seria divertido escrever um programa do Windows Phone 7 que permitisse gravar memorandos vocais curtos como “Lembrar de buscar a roupa na lavanderia” e “Tive uma grande ideia para um filme: Garoto encontra garota.”

Esse tipo de programa é útil, é claro, e fornece outra desculpa para mostrar nossos novos Windows Phones com o uso em locais públicos. O mais importante para mim, é que essa seria uma ótima oportunidade de obter alguma experiência prática com o uso das classes de gravação e reprodução de sons que têm suporte no telefone.

No entanto, o design do programa se tornou mais problemático do que eu previa. Antes mesmo de eu escrever uma única linha de código, o programa passou por várias iterações de design e redesign em minha cabeça.

Em princípio, pensei que seria suficiente ter apenas dois botões intitulados Gravar e Reproduzir, com os dois funcionando como alternadores. Pressionar o botão Gravar para iniciar a gravação e pressioná-lo novamente para parar. O programa salva os dados de áudio em armazenamento isolado. Pressionar o botão Reproduzir para reproduzir novamente. Cada botão Gravar substitui o memorando anterior, de forma que o programa não precisa de um botão Excluir.

Eu até brinquei um pouco para reduzir o programa para apenas um botão Reproduzir com a implementação de um recurso de ativação de voz. O programa gravaria continuamente e apenas salvaria os dados quando contivessem alguns sons. Mas diferenciar sons de plano de fundo de dados reais de voz pareceu ser um trabalho extremamente difícil sem a introdução de algum tipo de configuração manual de limite. E eu abandonei o design de um único botão.

Meu plano original funcionava bem para um memorando, mas não para vários memorandos. Então pensei que o programa deveria manter um único arquivo de áudio e adicionar cada novo memorando no final dos memorandos anteriores. Em se tratando de um único arquivo, o botão Reproduzir reproduziria todos os memorandos em sequência. É claro que o programa não pode permitir que esse arquivo cresça indefinidamente e, portanto, esse design precisa de um botão Excluir que limpe o arquivo inteiro e, consequentemente, todos os memorandos.

Não, isso não era bom. Eu realmente precisava manter arquivos separados para cada memorando e permitir que esses memorandos fossem excluídos individualmente. Mas isso implicaria em apresentar de alguma maneira todos os arquivos separados ao usuário para reprodução e exclusão, e de repente o programa se tornou muito mais complexo. Eu definitivamente precisava de uma Caixa de Listagem e de uma maneira de identificar cada memorando para o usuário, talvez com palavras-chave fornecidas pelo usuário ou, pior ainda, com um nome de arquivo real.

Não, não, não, isso não! Olhei rapidamente para minha secretária eletrônica. Cada chamada ou memorando é gravado separadamente, mas eles são numerados em um único visor. O botão Reproduzir é complementado com botões de transporte Anterior e Próxima para ir para a chamada anterior ou para a próxima chamada. Quando cada memorando ou chamada é excluído, no entanto, eles são renumerados. Eu sabia que não queria numerar os memorandos, mas eu podia tirar proveito do visor maior do telefone para mostrar mais detalhes sobre cada um, inclusive a data de gravação, a duração e o tamanho do arquivo.

O verdadeiro progresso veio quando percebi que podia colocar a Caixa de Listagem na tela principal do programa e usá-la não apenas para seleção, mas também para reprodução.

Usando o programa

Meu design final foi, é claro, uma concessão entre a simplicidade máxima e um sistema completo de gerenciamento de memorandos. O projeto SpeakMemo que pode ser baixado é escrito para Silverlight para Windows Phone e requer as Ferramentas de Desenvolvimento do Windows Phone 7. Você pode executar o programa no emulador de telefone e ele parecerá estar funcionando adequadamente, mas na verdade ele não gravará ou reproduzirá nenhum som.

Na primeira vez que você executar o programa SpeakMemo, ele exibirá a tela mostrada na Figura 1.

Figura 1 A SpeakMemoScreen

Um botão! Ou, pelo menos um botão habilitado em uma tela absolutamente organizada. O botão mostra quanto espaço existe no armazenamento isolado e como isso corresponde a um arquivo de som gravado. (Não, o programa não permitirá que você grave um memorando de 17 horas de duração!)

Pressione o botão Record e ele será alterado para uma exibição em vermelho brilhante com um indicador de duração, conforme mostrado na Figura 2.

Figura 2 SpeakMemo durante uma gravação

Pressione o botão Record novamente, e o memorando gravado será mostrado na tela com a data e a hora, a duração, o espaço de armazenamento e o botão Play, conforme mostrado na Figura 3.

Figura 3 SpeakMemo com um memorando

Naturalmente, você pode pressionar o botão Play para reproduzi-lo e o botão será alternado entre os modos Play e Pause.

Isso talvez não seja tão óbvio com apenas um memorando, mas os memorandos registrados são armazenados em uma caixa de listagem em ordem cronológica reversa, conforme mostrado na Figura 4, portanto, conforme você acumular muitos memorandos, poderá rolar pela lista e reproduzi-los individualmente.

Figura 4 A caixa de listagem do SpeakMemo

Um dos recursos avançados do Silverlight é o DataTemplate que permite definir a aparência dos itens em uma caixa de listagem. Esse DataTemplate pode incluir outros controles, como botões. Fiquei satisfeito por encontrar uma aplicação prática da colocação de um botão em um DataTemplate.

Também é possível gerenciar os memorandos coletados com a exclusão de memorandos individuais. Quando um memorando é selecionado o botão Delete é habilitado. Talvez inspirado pela colocação de um botão em um DataTemplate, executei outro truque do Silverlight com a colocação de dois botões adicionais dentro do botão Delete. Esses botões se tornam visíveis quando você pressiona Delete, e eles executam a tradicional função de confirmação, conforme mostrado na Figura 5.

Figura 5 Confirmando uma exclusão

A reprodução de um memorando faz com que ele seja selecionado, mas um item é reproduzido quando você o seleciona pressionando a área à direita do botão Play. O programa permite reproduzir um memorando, gravar outro e ainda excluir outro, tudo ao mesmo tempo.

Telefone e som

Antigamente, o Windows Phone 7 era suposto ter algo do reconhecimento de voz e o suporte à síntese encontrados nos namespaces System.Speech do Microsoft .NET Framework. Talvez ainda vejamos esse suporte no futuro.

Até lá, você pode capturar som do microfone e reproduzi-lo no alto-falante do telefone com o uso de classes do namespace Microsoft.Xna.Framework.Audio. Essas são classes XNA, mas você também pode usá-las em programas Silverlight. Para usar as classes XNA em um projeto Silverlight, basta adicionar uma referência a Microsoft.Xna.Framework.dll às referências do projeto e ignorar a mensagem de aviso.

As classes do namespace Microsoft.Xna.Framework.Audio são completamente separadas das do namespace Microsoft.Xna.Framework.Media. O namespace Media contém classes para reprodução de música da biblioteca de música do telefone, que são arquivos de áudio compactados em formato MP3 ou WMA que se tornam objetos do tipo Song. Eu mostro como acessar a biblioteca de música no Capítulo 18 de meu livro, “Programming Windows Phone 7” (Microsoft Press, 2010), que pode ser baixado gratuitamente em bit.ly/dr0Hdz. Em uma entrada do blog de meu site, também demonstro como reproduzir arquivos MP3 ou WMA que são armazenados dentro do próprio programa ou que podem ser baixados na Internet (bit.ly/ea73Fz).

Em contrapartida, classes do namespace Microsoft.Xna.Framework.Audio funcionam com dados de áudio não compactados no formato PCM padrão, que é o mesmo método usado por CDs de áudio e arquivos WAV do Windows. Com o PCM, a amplitude analógica do som é amostrada em uma taxa uniforme (normalmente no intervalo de 8.000 a 48.000 exemplos por segundo) e cada exemplo é normalmente armazenado como um valor de 8 ou 16 bits. O armazenamento necessário para um determinado som é o produto da duração em segundos, da taxa do exemplo e do número de bytes por exemplo (multiplicado por dois para estéreo).

Se precisar de suporte para o reconhecimento de fala em seu aplicativo do Windows Phone 7, você precisará fornecê-lo você mesmo, muito provavelmente por meio de um serviço Web. De maneira semelhante, um programa que requeira conversão de texto em fala provavelmente usará um serviço Web ou aguardará até que o telefone forneça esse suporte. O aplicativo Microsoft Translator para Windows faz isso com o serviço Microsoft Translator (microsofttranslator.com). O código e a documentação do Kit de Início do Translator está sendo liberado no MSDN (msdn.microsoft.com/library/gg521144(VS.92).aspx) e no AppHub (create.msdn.com/education/catalog/sample/translatorstarterkit).

Ao usar os serviços de áudio XNA, um programa Silverlight deve chamar o método estático FrameworkDispatcher.Update aproximadamente na mesma taxa que a atualização de vídeo, o que, no Windows Phone 7, é de aproximadamente 30 vezes por segundo. Há uma descrição de como fazer isso no artigo “Habilitar eventos da estrutura XNA em aplicativos do Windows Phone” na documentação online do XNA (msdn.microsoft.com/library/ff842408). No SpeakMemo, a classe XnaFrameworkDispatcherService trata esse trabalho. Essa classe é instanciada no arquivo App.xaml.

Gravação de som

Para gravar som por meio do microfone do telefone, você usa a classe Microphone. Você provavelmente criará uma instância dessa classe com a propriedade estática Default:

Microphone microphone = Microphone.Default;

Como alternativa, a propriedade estática All fornece uma coleção de objetos Microphone, mas então você provavelmente desejará apresentar a lista ao usuário para uma seleção.

A taxa de exemplo é fixa, não pode ser alterada e é relatada pela propriedade SampleRate como sendo de 16.000 exemplos por segundo. De acordo com o teorema de exemplo Nyquist, isso é adequado para gravar sons de até 8.000 Hz de frequência. Isso é adequado para voz, mas não espere grandes resultados com música. Cada exemplo tem 2 bytes de largura e é monaural, o que significa que cada segundo de som gravado exige 32.000 bytes, e cada minuto exige 1,9 MB.

Os dados do microfone são entregues ao programa em buffers que são simplesmente matrizes de bytes. Você instalará um manipulador para o evento BufferReader e chamará Start para iniciar a gravação. Quando o objeto Microphone acionar o evento BufferReady, seu código chamará GetData com uma matriz de bytes. No retorno de GetData, o buffer foi preenchido com dados de PCM. Quando o programa desejar interromper a gravação, chamará GetData mais uma vez para obter o último buffer parcial. O método retorna o número de bytes transferidos para a matriz. Em seguida chame Stop.

A única opção que o Microphone permite é que você especifique o tamanho em bytes do buffer que você passa para GetData. A propriedade BufferSize é um valor de TimeSpan que deve ser entre 100 ms e 1.000 ms (um segundo) em incrementos de 10 ms. No SpeakMemo, mantive o valor padrão de 1.000.

Para sua conveniência, a classe Microphone tem dois métodos para conversão entre tamanhos de buffer e tempo. Infelizmente, esses métodos são um pouco confusos porque os nomes fazem referência ao “exemplo”. O método GetSampleDuration basicamente divide um tamanho em bytes por 32.000 e retorna um TimeSpan que indica essa quantidade de segundos. GetSampleSizeInBytes multiplica a duração de um TimeSpan em segundos por 32.000.

Quando o SpeakMemo está gravando, ele acumula vários buffers de 32.000 bytes em um Conjunto de listas genérico. Ao gravar interrupções, o programa salva todos os buffers individuais em um arquivo no armazenamento isolado.

Depois que decidi não incluir um recurso de palavra-chave para identificar memorandos, eu queria que o arquivo contivesse apenas os dados do PCM sem nenhuma informação suplementar. No entanto, fiquei completamente chocado ao perceber que a classe IsolatedStorageFile no Silverlight para Windows não dá suporte a métodos para acessar a hora de criação ou da última gravação do arquivo, e senti que essas informações eram essenciais da perspectiva do usuário.

Isso significava que o próprio nome do arquivo precisava incluir a data e a hora. Primeiro, tentei criar um nome de arquivo a partir de um objeto DateTime com as opções de formatação “s” e “u”, mas isso não funcionou. (Deixarei o motivo disso não funcionar como um simples exercício para o leitor.) Em seguida, criei minha própria cadeia de caracteres de nome de arquivo compondo os vários componentes de data e hora em conjunto.

Reprodução de som XNA

O namespace Microsoft.Xna.Framework.Audio permite que você reproduza sons pré-gravados usando as classes SoundEffect e SoundEffectInstance, cujos nomes certamente traem sua função comum no contexto de um jogo XNA! Mas o método SoundEffect.FromStream requer um objeto Stream que faça referência a um arquivo WAV padrão do Windows completo com cabeçalho RIFF, e eu não queria me preocupar com formatos de arquivos.

Para trabalhar com dados brutos do PCM em vez de com arquivos WAV, você desejará usar a classe DynamicSoundEffectInstance, que deriva de SoundEffectInstance. Essa classe é ideal para dados gerados da classe Microphone ou de programas que criam dinamicamente seus próprios dados em forma de onda, como programas sintetizadores de música.

O construtor DynamicSoundEffectInstance requer uma taxa de exemplo e vários canais. Se você estiver usando essa classe com dados gerados no microfone, é óbvio que desejará manter sua consistência:

DynamicSoundEffectInstance playback = 
  new DynamicSoundEffectInstance(
  microphone.SampleRate, AudioChannels.Mono);

Por outro lado, se você desejar que a reprodução se assemelhe a um esquilo falante, basta multiplicar esse primeiro argumento por dois. O DynamicSoundEffectInstance espera que os dados tenham um tamanho de exemplo de 16 bits. A classe tem os métodos Play, Pause, Resume e Stop para controlar a reprodução, e uma propriedade State indica o estado atual. A classe funciona de maneira um pouco oposta do Microphone: Ela aciona um evento BufferNeeded quando requer um novo buffer. Seu trabalho é preencher um buffer com dados do PCM e chamar SubmitBuffer.

Para evitar intervalos audíveis no som, em geral, você desejará manter uma fila de buffers na classe DynamicSoundEffectInstance e enviar um novo buffer enquanto o buffer anterior ainda estiver sendo reproduzido. A classe ajuda com uma propriedade PendingBufferCount que indica o número de buffers que estão na fila. O evento BufferNeeded será acionado quando PendingBufferCount for alterado e for menor ou igual a dois.

No entanto, se você precisar apenas reproduzir uma parte dos dados do PCM, é possível chamar SubmitBuffer sem se preocupar com o evento BufferNeeded. No início, essa era a maneira como eu estava usando a classe no programa SpeakMemo, mas descobri que não era possível determinar quando o buffer tinha concluído a reprodução. Não há um evento “state changed” e, mesmo que houvesse, o DynamicSoundEffectInstance não muda do estado Play para o estado Stop quando o buffer é concluído. Ele ainda está esperando mais buffers. Por não conhecer essa informação o programa não pôde alternar corretamente os visuais do botão Play/Pause.

Acabei manipulando o evento BufferNeeded, mas apenas para ter a oportunidade de verificar a propriedade PendingBufferCount. Quando PendingBufferCount chega a zero, o buffer terá concluído a reprodução.

Problemas de armazenamento

O SpeakMemo armazena memorandos gravados em armazenamento isolado. Conceitualmente, o armazenamento isolado é privativo para o aplicativo, mas, fisicamente, ele faz parte de uma área de armazenamento total que é análoga ao disco rígido de um computador desktop. Todos os executáveis do aplicativo são armazenados ali, bem como as bibliotecas de fotos, de música e de vídeos do telefone além de outras coisas. A especificação do hardware do Windows Phone 7 exige que o telefone tenha pelo menos 8 GB de memória flash para essa área de armazenamento, e o próprio telefone alertará o usuário quando o armazenamento estiver com espaço insuficiente.

O armazenamento de arquivos de memorando não era minha principal preocupação. Eu estava mais preocupado com o heap do programa. Além do armazenamento em memória flash, a especificação de hardware do Windows Phone 7 também exige 256 MB de RAM. Essa é a memória que um aplicativo ocupa quando está em execução e que fornece o heap local do programa. Minhas experiências revelaram que o SpeakMemo podia alocar uma matriz de até 90 MB de tamanho antes de gerar uma exceção de memória insuficiente. Isso é equivalente a 47 minutos de som no microfone.

Isso não significa que um programa Windows Phone 7 é necessariamente limitado a 47 minutos de tempo de gravação. Mas um programa que deseje gravar essa quantidade contínua de som deve salvar buffers progressivamente no armazenamento isolado para liberar memória e, em seguida, carregar o arquivo incrementalmente ao reproduzi-lo. Essa não era a maneira como o SpeakMemo estava estruturado. Em vez disso, o programa salvava e carregava arquivos inteiros, e não me senti inclinado a abandonar essa estrutura tão simples.

Por esse motivo, simplesmente defini um máximo de 10 minutos para a duração do memorando. Quando uma gravação atinge essa duração, ela simplesmente é interrompida e salva (o que por si só exige vários segundos). Para manter o programa simples, não há nenhum aviso. A gravação simplesmente é interrompida como se o usuário tivesse pressionado o botão. Essa função automática de interromper e salvar ocorre quando o programa é encerrado ou de outra forma desativado. Por exemplo, durante marcação.

Naturalmente, a reprodução de um memorando de 10 minutos também não é exatamente conveniente. O botão Play é alternado entre os modos de reprodução e pausa, mas não há uma função de retrocesso ou de avanço. Esses recursos poderiam ser adicionados, mas você sabe o que isso exigiria, certo?

Sim: mais botões. Ou talvez até um controle deslizante.

Charles Petzold é editor colaborador da MSDN Magazine há muito tempo. Seu novo livro, “Programming Windows Phone 7” (Microsoft Press, 2010) está disponível gratuitamente para download em bit.ly/dr0Hdz.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Mark Hopkins