Grabar un WAV con el micrófono en Windows Phone 7


Descarga el código de ejemplo aquí: WindowsPhoneMicrophone.zip.

Desde la primera versión de WP7 tenemos acceso al micrófono a través del framework XNA. En algunas ocasiones nos bastará con poder leer el canal de audio, pero en otras muchas vamos a querer guardar el audio a disco e incluso enviarlo a un servicio web. Esto que parece tan natural representa un verdadero problema pues:

  • Estamos forzados a simular el ciclo de XNA  en nuestras aplicaciones Silverlight
  • El stream de audio nos da un PCM crudo y tenemos que añadir nosotros mismos las cabeceras para crear un WAV
  • No podemos crear fácilmente otros formatos más convenientes (wma, mp3, etc…)
Hoy vamos a ver cómo resolver los dos primeros. El primer punto es el más sencillo pues tenemos múltiples ejemplos de cómo llamar al dispatcher de XNA para hacerle creer al sistema que estamos dentro del ciclo. En Mango tenemos una nueva forma de integrar XNA y Silverlight con la clase GameTimer, pero para este caso no necesitamos una integración completa, así que seguiremos usando el sistema “antiguo” con un DispatcherTimer:
DispatcherTimer dt = new DispatcherTimer();
dt.Interval = TimeSpan.FromMilliseconds(33);
dt.Tick += delegate { try { FrameworkDispatcher.Update(); } catch { } };
Una vez instanciado el micrófono podremos empezar a guardar audio en un stream cualquiera. Para ello tendremos que reservar un buffer donde el micrófono irá guardando el audio y nos avisará cuando esté lleno para que lo podamos volcar al stream.
Microphone _microphone = Microphone.Default;
_microphone.BufferReady += 
   new EventHandler<EventArgs>(microphone_BufferReady);

_microphone.BufferDuration =
   TimeSpan.FromMilliseconds(500);

byte[] _buffer =
   new byte[_microphone.GetSampleSizeInBytes(_microphone.BufferDuration)];

_microphone.Start();
Así, en el evento BufferReady podremos ir guardando los datos a nuestro stream.
void microphone_BufferReady(object sender, EventArgs e)
{
    // Retrieve audio data
    int length = _microphone.GetData(_buffer);

    // Store the audio data in a stream
    _bwOutput.Write(_buffer, 0, length);
    _rawDataLength += (uint)length;
}
Si se nos ocurre guardar esos datos directamente a un fichero, usando la clase SoundEffect podremos volver a escuchar ese audio, pero, ¿Qué pasa si queremos enviar nuestro audio a un servicio? El audio que hemos guardado está en formato PCM en crudo y un programa de reproducción de sonido no sabrá cómo interpretarlo, pues le hace falta información de cómo es ese audio: bitrate, número de canales, etc…
Una solución muy rápida es crear una cabecera al PCM para darle formato WAV, hay ejemplos por la red, pero son genéricos y tenemos el problema de que no tenemos acceso a la clase AudioFormat, aunque en 7.1 está escondida dentro de la clase del micrófono. Por ahora tendremos que poner algunos datos a mano en la cabecera:
He utilizado uno de los ejemplos que hay para Silverlight y he modificado los valores para que coincidan con los del micrófono del WP7:
public void WriteHeader()
{
    // Write down the WAV header.
    // Refer to http://technology.niagarac.on.ca/courses/ctec1631/WavFileFormat.html
    // for details on the format.

    // Note that we use ToCharArray() when writing fixed strings
    // to force using the char[] overload because
    // Write(string) writes the string prefixed by its length.

    // -- RIFF chunk
    _bwOutput.Write("RIFF".ToCharArray());

    // Total Length Of Package To Follow
    // Computed as data length plus the header length without the data
    // we have written so far and this data (44 - 4 ("RIFF") - 4 (this data))
    _bwOutput.Write((uint)(_rawDataLength + 36));

    _bwOutput.Write("WAVE".ToCharArray());

    // -- FORMAT chunk

    _bwOutput.Write("fmt ".ToCharArray());

    // Length Of FORMAT Chunk (Binary, always 0x10)
    _bwOutput.Write((uint)0x10);

    // Always 0x01
    _bwOutput.Write((ushort)0x01);

    // Channel Numbers (Always 0x01=Mono, 0x02=Stereo)
    _bwOutput.Write((ushort)0x01);// audioFormat.Channels);

    // Sample Rate (Binary, in Hz)
    _bwOutput.Write((uint)_microphone.SampleRate);//audioFormat.SamplesPerSecond);

    // Bytes Per Second
    _bwOutput.Write((uint)(16 * _microphone.SampleRate * 1 / 8));//  audioFormat.BitsPerSample * audioFormat.SamplesPerSecond * audioFormat.Channels / 8));

    // Bytes Per Sample: 1=8 bit Mono, 2=8 bit Stereo or 16 bit Mono, 4=16 bit Stereo
    _bwOutput.Write((ushort)2);// audioFormat.BitsPerSample * audioFormat.Channels / 8));

    // Bits Per Sample
    _bwOutput.Write((ushort)16);

    // -- DATA chunk

    _bwOutput.Write("data".ToCharArray());

    // Length Of Data To Follow
    _bwOutput.Write((uint)_rawDataLength);
}
Y ahora si, ya podemos crear el WAV dentro del stream, tendremos que llamar a la escritura de la cabecera al principio de la grabación y al final para guardar de nuevo la longitud. Una vez hecho esto tendremos un archivo WAV completamente válido.
public void Start(Stream output)
{
    OnDispatcher();
    dt.Start();

    _bwOutput = new BinaryWriter(output);
    this.WriteHeader();
    _microphone.Start();
}

public void Stop()
{
    _microphone.Stop();

    dt.Stop();
    OnDispatcher();

    long pos = _bwOutput.BaseStream.Position;
    _bwOutput.Seek(0, SeekOrigin.Begin);
    //write the lenght into the header
    this.WriteHeader();
    _bwOutput.Seek(0, SeekOrigin.End);
}
El código fuente de ejemplo disponible aquí está preparado para Windows Phone 7.1 y utiliza alguna característica nueva, como por ejemplo la propiedad command en los botones :D, pero eso lo explicaré en otro post más completo.

El ejemplo está basado en el ejemplo de ondrejsv hecho en Silverlight 4, adaptado a las características de WP7.

Anuncios

  1. Pingback: Usar el micrófono de Windows Phone 7 desde Silverlight « Mouseless Me
  2. Pierre Antony G

    Buen Articulo muy muy bueno :D!! Sirvo de mucho, pero tengo una consulta, como podría hacer para esa grabación almanecarla de forma volátil en el celular, pero q se almacene en un servidor como por ejemplo “Windows Azure”.

    • jmservera

      Gracias! Pues la verdad es que eso daría para un artículo entero. Tendrías que enviar la grabación a un servicio web que hagas en Azure para recibir el audio y a partir del servicio guardarlo en el almacenamiento de blobs. Sería recomendable que primero lo comprimieras, pero tendrás que encontrar una librería de compresión adecuada para tu tipo de audio (música, voz, etc..). Intentaré escribir algo sobre esto en el futuro.

      • Pierre Antony Gutierrez Yupanqui

        Aya, Jmservera, otra consulta, para mandar al servicio web de Azure, seria necesario capturar al archivo que esta en almacenamiento aislado. Como o en que parte del codigo puedo capturar esa “direccion” o como puedo acceder a ella para recien mandarlo a el Servicio Web ?…ES necesraio Servicio Web..o puede ser con MObile SErvices o WCF =O?

      • jmservera

        Hola Pierre Antony,
        Para mandar al servicio es mejor esperar a haber acabado la grabación. Una vez acabada y cerrado el archivo puedes volver a abrirlo para obtener el Stream que puedes escribir sobre un Stream de tu servicio.
        Para abrir el stream mira el método play del archivo RecorderViewModel.cs del código del ejemplo.
        En el lado del servidor puedes usar webservice, un handler ashx o un servicio WCF, para cada uno tendrás que usar métodos diferentes para conectar con ellos. Por ejemplo, para el ashx tendrás que utilizar un System.Net.WebClient para escribir en el stream de dalida.
        Con Mobile Services también puedes hacerlo, pero te recomiendo que utilices el espacio de almacenamiento en BLOB, pues en la base de datos te saldrá muy caro. Tienes un ejemplo de cómo acceder desde Windows 8 aquí http://www.windowsazure.com/en-us/develop/mobile/tutorials/upload-images-to-storage-dotnet aunque tendrás que adaptarlo para Windows Phone.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s