Reinventing The Wheel

How to reverse a .wav file using C#

For those of you who don't know me, I have a unique skill where I can speak backwards phonetically.

Although it's quite a useless skill, there is a Guinness World Record for it which I'm trying to beat.

To train for this, I needed to make a backwards player app so I could test myself.

Of course there are already many free apps out there, but instead of just downloading one of those, I thought I'd reinvent the wheel, and learn heaps in the process.

The format of a .wav file

According to http://www.topherlee.com/software/pcm-tut-wavformat.html, a .wav file is made up of a header and audio data. The header looks like this:

Positions Sample Value Description
0 - 3 "RIFF" Marks the file as a riff file. Characters are each 1 byte long.
4 - 7 File size (integer) Size of the overall file - 8 bytes, in bytes (32-bit integer). Typically, you'd fill this in after creation.
8 -11 "WAVE" File Type Header. For our purposes, it always equals "WAVE".
12-15 "fmt " Format chunk marker. Includes trailing null
16-19 16 Length of format data as listed above
20-21 1 Type of format (1 is PCM) - 2 byte integer
22-23 2 Number of Channels - 2 byte integer
24-27 44100 Sample Rate - 32 byte integer. Common values are 44100 (CD), 48000 (DAT). Sample Rate = Number of Samples per second, or Hertz.
28-31 176400 Bytes per second = (Sample Rate * BitsPerSample * Channels) / 8.
32-33 4 Block align = (BitsPerSample * Channels) / 8.1 - 8 bit mono2 - 8 bit stereo/16 bit mono4 - 16 bit stereo
34-35 16 Bits per sample
36-39 "data" "data" chunk header. Marks the beginning of the data section.
40-43 File size (data) Size of the data section.

For more info on the format of the .wav file have a look at these websites:

  • http://soundfile.sapp.org/doc/WaveFormat/
  • http://blogs.msdn.com/b/dawate/archive/2009/06/23/intro-to-audio-programming-part-2-demystifying-the-wav-format.aspx
  • http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html

The header provides metadata to do with the audio file. Some parts of the metadata are essential for us to know if we are to reverse the file.

After the header is the audio data, which is basically the actual values of the audio signal's amplitudes which make up the sound.

So now we know that a wav file has metadata, and audio data, how do we go about reversing the sound?

What exactly do we need to reverse?

We definitely don't want to reverse the header, since this contains the metadata to do with the file, so all we need to reverse is the audio data, which comes after the header.

Audio data and samples

Unfortunately, we can't simply create a new file with a copy of the header, and then a copy of the audio data in reverse.

This is because a wav file has things called "samples". If each sample was only one byte, then we could simply copy the header into a new byte array, then use a for loop to copy each element of the audio data, in reverse, into the new byte array, then create a new wav file based on this new byte array.

What we need to reverse are these "samples".

What makes up a sample?

If we want to reverse the samples, we need to know what exactly makes up a sample, ie. how many bytes are used to represent each sample?

According to the documentation for a wav file, the "bits per sample" is stored in the wav file's byte array at index 34 and 35.

With the wav file I was using for testing, I had a value of 16 in index 34, and 0 in 35.

According to http://soundfile.sapp.org/doc/WaveFormat/, if your first 4 bytes contain the string "RIFF", as opposed to "RIFX", then the file's byte order is little-endian.

This means the value in index 35 of my wav file's byte array is the most significant byte, and the byte in index 34 is the least significant byte.

So to get the bits per sample, we need to read the number as 0 followed by 16, which looks like this in binary, 0000000010000, which is 16 in decimal.

We now know that there are 16 bits (or 2 bytes), per sample.

The process

To reverse the sound of a wav file, we need to do the following:

  1. Get the byte array that makes up the audio file.
  2. Extract the bytes that make up the header.
  3. Extract the audio data samples and reverse them.
  4. Create a new byte array and copy in the header from step 2, and the reversed samples from step 3.
  5. Write this new byte array to a file.

The code

Step 1

First we need to read the file's file stream into a byte array, and then close the file stream:

string forwardsWavFilePath = @"C:\ForwardsWavFile.wav";

byte[] forwardsWavFileStreamByteArray = PopulateForwardsWavFileByteArray(forwardsWavFilePath);

private static byte[] PopulateForwardsWavFileByteArray(string forwardsWavFilePath)
{
    byte[] forwardsWavFileStreamByteArray;
    using (FileStream forwardsWavFileStream = new FileStream(forwardsWavFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
    {
        forwardsWavFileStreamByteArray = new byte[forwardsWavFileStream.Length];
        forwardsWavFileStream.Read(forwardsWavFileStreamByteArray, 0, (int) forwardsWavFileStream.Length);
    }
    return forwardsWavFileStreamByteArray;
}

Step 2

Now we need to extract the header from the byte array we read from the file stream in step 1.

Here I'm using a static variable from a class called Constants (Constants.StartIndexOfAudioDataChunk).

I'll show you this class in its entirety later on, for now you just need to know that its value is 44.

byte[] forwardsArrayWithOnlyHeaders = CreateForwardsArrayWithOnlyHeaders(forwardsWavFileStreamByteArray, Constants.StartIndexOfAudioDataChunk);

private static byte[] CreateForwardsArrayWithOnlyHeaders(byte[] forwardsWavFileStreamByteArray, int startIndexOfDataChunk)
{
    byte[] forwardsArrayWithOnlyHeaders = new byte[startIndexOfDataChunk];
    Array.Copy(forwardsWavFileStreamByteArray, 0, forwardsArrayWithOnlyHeaders, 0, startIndexOfDataChunk);
    return forwardsArrayWithOnlyHeaders;
}

Step 3

Now comes the hard part, reversing the samples.

As we mentioned earlier, because my audio file has 2 bytes per sample, we can't just use a for loop to iterate through the forwards byte array, in reverse, copying each element into the new byte array.

If we did this, we would be not only reversing the samples, but reversing each sample's bytes which would result in completely wrong amplitude values.

To work out how to design the for loop, let's begin with an easy example, a byte array with only 10 elements, and each sample has 2 bytes:

As you can see from the diagram above, our for loop needs to count in a very odd way.

To count like this, we need to start from the very end of the array.

We then need to work our way backwards through the array, but not per index, rather per sample, copying in each sample's bytes into our new byte array, preserving the sample's forwards order of bytes.

The variable called sampleIdentifier is used to help us go backwards, sample by sample, through the array.

The if statement: if (i != 0 && i % bytesPerSample == 0), helps determine if we are at an even, or an odd index.

If we're at an odd index, then we need to go one index forwards in our reverse for loop to get the last byte from the sample.

This is quite hard to explain, so the best way to understand what is going on is to examine the below code, and step through it in debug mode to see what exactly is happening.

byte[] reversedArrayWithOnlyAudioData = ReverseTheForwardsArrayWithOnlyAudioData(bytesPerSample, forwardsArrayWithOnlyAudioData);

private static byte[] ReverseTheForwardsArrayWithOnlyAudioData(int bytesPerSample, byte[] forwardsArrayWithOnlyAudioData)
{
    int length = forwardsArrayWithOnlyAudioData.Length;
    byte[] reversedArrayWithOnlyAudioData = new byte[length];
    int sampleIdentifier = 0;
    for (int i = 0; i < length; i++)
    {
        if (i != 0 && i % bytesPerSample == 0)
        {
            sampleIdentifier += 2 * bytesPerSample;
        }
        int index = length - bytesPerSample - sampleIdentifier + i;
        reversedArrayWithOnlyAudioData[i] = forwardsArrayWithOnlyAudioData[index];
    }
    return reversedArrayWithOnlyAudioData;
}

Step 4

The second last step is to create a new byte array to store our new values. We need to copy in the header bytes, and the reversed samples.

byte[] reversedWavFileStreamByteArray = CombineArrays(forwardsArrayWithOnlyHeaders, reversedArrayWithOnlyAudioData);

private static byte[] CombineArrays(byte[] forwardsArrayWithOnlyHeaders, byte[] reversedArrayWithOnlyAudioData)
{
    byte[] reversedWavFileStreamByteArray = new byte[forwardsArrayWithOnlyHeaders.Length + reversedArrayWithOnlyAudioData.Length];
    Array.Copy(forwardsArrayWithOnlyHeaders, reversedWavFileStreamByteArray, forwardsArrayWithOnlyHeaders.Length);
    Array.Copy(reversedArrayWithOnlyAudioData, 0, reversedWavFileStreamByteArray, forwardsArrayWithOnlyHeaders.Length, reversedArrayWithOnlyAudioData.Length);
    return reversedWavFileStreamByteArray;
}

Step 5

The final step is easy, just write the new byte array to a file, and you're done!

string reversedWavFilePath = @"C:\ReversedWavFile.wav";

WriteReversedWavFileByteArrayToFile(reversedWavFileStreamByteArray, reversedWavFilePath);

private static void WriteReversedWavFileByteArrayToFile(byte[] reversedWavFileStreamByteArray, string reversedWavFilePath)
{
    using (FileStream reversedFileStream = new FileStream(reversedWavFilePath, FileMode.Create, FileAccess.Write, FileShare.Write))
    {
        reversedFileStream.Write(reversedWavFileStreamByteArray, 0, reversedWavFileStreamByteArray.Length);
    }
}

The entire code

Here's all the code put together.

I've written the code so that you can run it from any project type. You just need to call Reverser.Start().

You can also download it off my GitHub page.

internal static class Reverser
{
    public static void Start()
    {            
        string forwardsWavFilePath = @"C:\Users\david.klempfner\Documents\Sound recordings\RecordingWav.wav";
        byte[] forwardsWavFileStreamByteArray = PopulateForwardsWavFileByteArray(forwardsWavFilePath);            

        byte[] forwardsArrayWithOnlyHeaders = CreateForwardsArrayWithOnlyHeaders(forwardsWavFileStreamByteArray, Constants.StartIndexOfAudioDataChunk);
        byte[] forwardsArrayWithOnlyAudioData = CreateForwardsArrayWithOnlyAudioData(forwardsWavFileStreamByteArray, Constants.StartIndexOfAudioDataChunk);

        int bytesPerSample = MetadataGatherer.GetBitsPerSample(forwardsWavFileStreamByteArray) / Constants.BitsPerByte;
        byte[] reversedArrayWithOnlyAudioData = ReverseTheForwardsArrayWithOnlyAudioData(bytesPerSample, forwardsArrayWithOnlyAudioData);
        byte[] reversedWavFileStreamByteArray = CombineArrays(forwardsArrayWithOnlyHeaders, reversedArrayWithOnlyAudioData);

        string reversedWavFilePath = @"C:\Users\david.klempfner\Documents\Sound recordings\reversedFile.wav";

        WriteReversedWavFileByteArrayToFile(reversedWavFileStreamByteArray, reversedWavFilePath);
    }

    private static void WriteReversedWavFileByteArrayToFile(byte[] reversedWavFileStreamByteArray, string reversedWavFilePath)
    {
        using (FileStream reversedFileStream = new FileStream(reversedWavFilePath, FileMode.Create, FileAccess.Write, FileShare.Write))
        {
            reversedFileStream.Write(reversedWavFileStreamByteArray, 0, reversedWavFileStreamByteArray.Length);
        }
    }

    private static byte[] CombineArrays(byte[] forwardsArrayWithOnlyHeaders, byte[] reversedArrayWithOnlyAudioData)
    {
        byte[] reversedWavFileStreamByteArray = new byte[forwardsArrayWithOnlyHeaders.Length + reversedArrayWithOnlyAudioData.Length];
        Array.Copy(forwardsArrayWithOnlyHeaders, reversedWavFileStreamByteArray, forwardsArrayWithOnlyHeaders.Length);
        Array.Copy(reversedArrayWithOnlyAudioData, 0, reversedWavFileStreamByteArray, forwardsArrayWithOnlyHeaders.Length, reversedArrayWithOnlyAudioData.Length);
        return reversedWavFileStreamByteArray;
    }

    private static byte[] ReverseTheForwardsArrayWithOnlyAudioData(int bytesPerSample, byte[] forwardsArrayWithOnlyAudioData)
    {
        int length = forwardsArrayWithOnlyAudioData.Length;
        byte[] reversedArrayWithOnlyAudioData = new byte[length];
        int sampleIdentifier = 0;
        for (int i = 0; i < length; i++)
        {
            if (i != 0 && i % bytesPerSample == 0)
            {
                sampleIdentifier += 2 * bytesPerSample;
            }
            int index = length - bytesPerSample - sampleIdentifier + i;
            reversedArrayWithOnlyAudioData[i] = forwardsArrayWithOnlyAudioData[index];
        }
        return reversedArrayWithOnlyAudioData;
    }

    private static byte[] CreateForwardsArrayWithOnlyAudioData(byte[] forwardsWavFileStreamByteArray, int startIndexOfDataChunk)
    {
        byte[] forwardsArrayWithOnlyAudioData = new byte[forwardsWavFileStreamByteArray.Length - startIndexOfDataChunk];
        Array.Copy(forwardsWavFileStreamByteArray, startIndexOfDataChunk, forwardsArrayWithOnlyAudioData, 0, forwardsWavFileStreamByteArray.Length - startIndexOfDataChunk);
        return forwardsArrayWithOnlyAudioData;
    }

    private static byte[] CreateForwardsArrayWithOnlyHeaders(byte[] forwardsWavFileStreamByteArray, int startIndexOfDataChunk)
    {
        byte[] forwardsArrayWithOnlyHeaders = new byte[startIndexOfDataChunk];
        Array.Copy(forwardsWavFileStreamByteArray, 0, forwardsArrayWithOnlyHeaders, 0, startIndexOfDataChunk);
        return forwardsArrayWithOnlyHeaders;
    }

    private static byte[] PopulateForwardsWavFileByteArray(string forwardsWavFilePath)
    {
        byte[] forwardsWavFileStreamByteArray;
        using (FileStream forwardsWavFileStream = new FileStream(forwardsWavFilePath, FileMode.Open, FileAccess.Read, FileShare.Read))
        {
            forwardsWavFileStreamByteArray = new byte[forwardsWavFileStream.Length];
            forwardsWavFileStream.Read(forwardsWavFileStreamByteArray, 0, (int) forwardsWavFileStream.Length);
        }
        return forwardsWavFileStreamByteArray;
    }
}

internal static class MetadataGatherer
{
    internal static ushort GetBitsPerSample(byte[] forwardsWavFileStreamByteArray)
    {
        byte[] bitsPerSampleByteArray = GetRelevantBytesIntoNewArray(forwardsWavFileStreamByteArray, Constants.BitsPerSampleStartIndex, Constants.BitsPerSampleEndIndex);
        ushort bitsPerSample = BitConverter.ToUInt16(bitsPerSampleByteArray, 0);
        Console.WriteLine("Bits Per Sample = {0}", bitsPerSample);
        return bitsPerSample;
    }
    private static byte[] GetRelevantBytesIntoNewArray(byte[] forwardsWavFileStreamByteArray, int startIndex, int endIndex)
    {
        int length = endIndex - startIndex + 1;
        byte[] relevantBytesArray = new byte[length];
        Array.Copy(forwardsWavFileStreamByteArray, startIndex, relevantBytesArray, 0, length);
        return relevantBytesArray;
    }
}

internal static class Constants
{        
    internal const int BitsPerSampleStartIndex = 34;
    internal const int BitsPerSampleEndIndex = 35;

    internal const int StartIndexOfAudioDataChunk = 44;

    internal const int BitsPerByte = 8;
}

Conclusion

So there you have it, an app that reverses wav files :)

I hope you found this interesting and see the value in creating things yourself.

If you're ever working on something which you're not getting paid to do, I encourage you to reinvent the wheel and write whatever it is yourself from scratch.

By doing so you'll build skills which can help you in other areas of programming, and you get more satisfaction when you look back at something and realise you wrote it yourself.