Nov 15 2010

Writing a Proper Wave File

Category: MobileJoel Ivory Johnson @ 07:22

Currently one of the reoccuring questions I see in the Windows Phone 7 forums deals with playing back data that was recorded from the Microphone. Often time developers will write the sound bytes that they receive from a microphone to a file and then try to export them for playback or play them back using the media classes on the phone only to find that the file can't be processed. During my lunch break today I had a chance to throw something together that I think will point those developers in the right direction.

Why Won't the File Play

The file won't play because none of the components or software to which it has has been given know anything about the file. If you record from the Microphone and dump the raw bytes to a file the things you are not writing include the sample rate, number of bits per sample, the file format, and so on. You need to prepend the file with all of these things for it to be usable by the media classes. Having done a quick Bing search I found a description of the needed header on https://ccrma.stanford.edu/courses/422/projects/WaveFormat/. Using that I put together a quick desktop application that produces a playable wave file. I targeted the desktop because the computer I'm using doesn't have the phone developer tools. But the code will pretty much be the same for the desktop as on the phone. The only difference will be in the creation of your file. While I am creating a file stream directly you would create a stream in isolated storage.

Simulating Audio Data

I need some data to write to my file. As is my preference I've created a function that will populate an array of bytes with the output of the Sine function. As it's parameters it takes the sample rate, the length of time that I want the sound to play, the wave's frequency, and it's magnitude (with 0 being the lowest magnitude and 1 being the greatest) and returns the data in a byte array. You would populate your array with the bytes from the recording instead. The code I used to do this follows.

public static byte[] CreateSinWave( 
        int sampleRate, 
        double frequency, 
        TimeSpan length, 
        double magnitude
    )
{
    int sampleCount = (int)(((double)sampleRate) * length.TotalSeconds);
    short[] tempBuffer = new short[sampleCount];
    byte[] retVal = new byte[sampleCount*2];
    double step = Math.PI*2.0d/frequency;
    double current = 0;
            
    for(int i=0;i<tempBuffer.Length;++i)
    {
        tempBuffer[i] = (short)(Math.Sin(current) * magnitude * ((double)short.MaxValue));
        current += step;
    }

    Buffer.BlockCopy(tempBuffer,0,retVal,0,retVal.Length);
    return retVal;
}

Populating the Wave Header

There are better ways to do this, much better ways. But I'm just trying to create something satisficing in a short period of time.

Trival Fact: Satisficing is a phrase coined by Herbert Simon to mean sufficiently satisfying. A satisficing solution may not be the best solution, but it get's the job done!

.

Looking on the chart that describes a wave header I wrote either literal bytes or calculated values, where the calculated values are based on sample rate, number of channels, and a few other factors. There's not a lot to say about it, but the code follows.

static byte[] RIFF_HEADER = new byte[] { 0x52, 0x49, 0x46, 0x46 };
static byte[] FORMAT_WAVE = new byte[] { 0x57, 0x41, 0x56, 0x45 };
static byte[] FORMAT_TAG  = new byte[] { 0x66, 0x6d, 0x74, 0x20 };
static byte[] AUDIO_FORMAT = new byte[] {0x01, 0x00};
static byte[] SUBCHUNK_ID  = new byte[] { 0x64, 0x61, 0x74, 0x61 };
private const int BYTES_PER_SAMPLE = 2;

public static void WriteHeader(
     System.IO.Stream targetStream, 
     int byteStreamSize, 
     int channelCount, 
     int sampleRate)
{

    int byteRate = sampleRate*channelCount*BYTES_PER_SAMPLE;
    int blockAlign = channelCount*BYTES_PER_SAMPLE;

    targetStream.Write(RIFF_HEADER,0,RIFF_HEADER.Length);
    targetStream.Write(PackageInt(byteStreamSize+44-8, 4), 0, 4);

    targetStream.Write(FORMAT_WAVE, 0, FORMAT_WAVE.Length);
    targetStream.Write(FORMAT_TAG, 0, FORMAT_TAG.Length);
    targetStream.Write(PackageInt(16,4), 0, 4);//Subchunk1Size    

    targetStream.Write(AUDIO_FORMAT, 0, AUDIO_FORMAT.Length);//AudioFormat   
    targetStream.Write(PackageInt(channelCount, 2), 0, 2);
    targetStream.Write(PackageInt(sampleRate, 4), 0, 4);
    targetStream.Write(PackageInt(byteRate, 4), 0, 4);
    targetStream.Write(PackageInt(blockAlign, 2), 0, 2);
    targetStream.Write(PackageInt(BYTES_PER_SAMPLE*8), 0, 2);
    //targetStream.Write(PackageInt(0,2), 0, 2);//Extra param size
    targetStream.Write(SUBCHUNK_ID, 0, SUBCHUNK_ID.Length);
    targetStream.Write(PackageInt(byteStreamSize, 4), 0, 4);
}

static byte[] PackageInt(int source, int length=2)
{
    if((length!=2)&&(length!=4))
        throw new ArgumentException("length must be either 2 or 4", "length");
    var retVal = new byte[length];
    retVal[0] = (byte)(source & 0xFF);
    retVal[1] = (byte)((source >> 8) & 0xFF);
    if (length == 4)
    {
        retVal[2] = (byte) ((source >> 0x10) & 0xFF);
        retVal[3] = (byte) ((source >> 0x18) & 0xFF);
    }
    return retVal;
}

That's pretty much all you need to know. To use the code I wrote a simple console mode program.

static void Main(string[] args)
{
    var soundData = WaveHeaderWriter.CreateSinWave(44000, 120, TimeSpan.FromSeconds(60), 1d);
    using(FileStream fs = new FileStream("MySound2.wav", FileMode.Create))
    {
        WaveHeaderWriter.WriteHeader(fs, soundData.Length, 1, 44100);
        fs.Write(soundData,0,soundData.Length);
        fs.Close();
    }
}

I opened the resulting output in Audacity and the results are what I expected.

And of course as a final test I double clicked on the file. It opened in Windows Media Player and played the Sine wave.

So there you have it, the program works! When I get a chance I will try to make a version of this in Windows Phone 7. Those of you that have WPDT without the full version of Visual Studio will not be able to compile this program directly. But the binary is included in the source code if you want to run it.

Tags: ,

Comments

1.
pingback supermallamerica.com says:

Pingback from supermallamerica.com

How to Choose the Correct Knife Set | Supermallamerica.com Blog

2.
pingback montereybayjobexpo.com says:

Pingback from montereybayjobexpo.com

Wonderful Free Date Ideas | Montereybayjobexpo.com Job Blog