Saturday, March 28, 2009

NAudio Tutorial 4 - Sample Reversing

Welcome to the next edition of the NAudio Tutorials series. In this tutorial we will be looking at how a sample can be reversed and played back.

This tutorial builds upon the previous tutorials, if you haven't had a chance to review them I suggest that you read them first before attempting this tutorial:

A bit of a disclaimer for this approach; we will be overriding the Read method to achieve the playback of the reversed sample. Please don't misinterpret this approach to be the most suitable for implementation or the approach which is close to an appropriate design pattern. Marks suggestion for implementing this feature was to create a WaveStreamReverse stream derived from WaveStream. I've taken this approach because I am looking to demonstrate how wave data stored in a byte array can be manipulated and passed back to the mixer for playback.

Also note that during the writing of this tutorial I have uncovered what looks to be a minor bug that was preventing this function for working on samples longer than a second. A complete post of the details is available here:
Due to this I have packaged a modified version of the NAudio DLL with this Tutorial as per the reference of suggested modification in the NAudio thread.

Hopefully that is suitable for the readership in the crowd out there but if you want to see it setup in a derived WaveStreamReverse class then drop me a line and let me know.

You can download a complete copy of all of the source files and this documentation in AbiWord format from here.

Reversing The Sample

Lets open the floor to how we can actually reverse a wave file. Basically a wave file is made up of samples, for argument sake we will consider a wave file with a two channels (stereo) and the data for both channels, for the same position is considered a sample. A simple example below:

Sine Wave Points

Here we have a single channel sine wave. The 6 boxes highlight six points in this sine wave, these are samples. Now consider an exact copy of this image for our second channel of audio and we would have two points for each sample. Conceptually thats certainly simple enough; now we need to discuss how this data is stored.

In a wave file the beginning of the file has a set of data explaining what the format of the file is, Frequency, Bit Size, Number of Channels etc. Once we go past this header information the main wave file starts. Based on the presiding information we can determine how to read the wave file. NAudio completes that read and load operation for us (thankfully because if you don't have to look at it, don't, it's not lots of fun) and provides us with a byte array of the actual wave form data. Now depending on the preceding information we need to adjust the way we consider and utalise this data; if we have only a single channel the byte array needs to be read in accordance with that, if we have a greater precision of samples (16 bit vs. 8 bit) then we need to also take that in to account. So specifically for reversing we are interested in the number of bytes per sample, which is calculated as such:

bytesPerSample = (channelStream.WaveFormat.BitsPerSample / 8) * channelStream.WaveFormat.Channels;

Taking the number of bits per sample and dividing by 8 gives us the number of bytes per sample (which is very important considering that the wave file is stored in a byte array) and then we multiple this by the number of channels of data we need to handle. So in effect, only considering the number of bytes per sample and then reversing the order that the complete sample appears in a byte array allows us to reverse the complete sample.

Lets have a look at how that works, in a new class called NAudioBufferReverse, which takes in the sampleToReverse as a byte array, the length of the source file in bytes and the number of bytes per sample, notice for this class as long as we have pre-calculated the number of bytes per sample we don't actually need to know the details of why there are that many bytes per sample, only that there is and that each sample needs to be moved as a whole, in the reverse order, to another byte array.

class NAudioBufferReverse
   // Length of the buffer
   private int numOfBytes;

   // The byte array to store the reversed sample
   byte[] reversedSample;

   public byte[] reverseSample(byte[] sampleToReverse, int SourceLengthBytes, int bytesPerSample)

       numOfBytes = SourceLengthBytes;

       // Set the byte array to the length of the source sample
       reversedSample = new byte[SourceLengthBytes];
       // The alternatve location; starts at the end and works to the begining
       int b = 0;

       //Prime the loop by 'reducing' the numOfBytes by the first increment for the first sample
       numOfBytes = numOfBytes - bytesPerSample;

       // Used for the imbeded loop to move the complete sample
       int q = 0;

       // Moves through the stream based on each sample
       for (int i = 0; i < numOfBytes - bytesPerSample; i = i + bytesPerSample)
           // Effectively a mirroing process; b will equal i (or be out by one if its an equal buffer)
           // when the middle of the buffer is reached.
           b = numOfBytes - bytesPerSample - i;

           // Copies the 'sample' in whole to the opposite end of the reversedSample
           for (q = 0; q <= bytesPerSample; q++)
               reversedSample[b + q] = sampleToReverse[i + q];

       // Sends back the reversed stream
       return reversedSample;

Yes, over commented if anything but remember this is a tutorial so you can learn whats going on right? After this class has been implemented we now have an available function to help us reverse a byte array by a sample.

Setup the Sample

So now we have to call this and setup our sample class, which we have introduced in previous tutorials, to access this function. Enter stage right:

// SampleArray to be store the reveresed array
byte[] reversedSample;

bool _sampleReversed = false;

public AudioSample(string fileName)
   _fileName = fileName;
   WaveFileReader reader = new WaveFileReader(fileName);
   channelStream = new WaveChannel32(reader);
   muted = false;
   volume = 1.0f;
   // Reverse the sample
   NAudioBufferReverse nbr = new NAudioBufferReverse();
   // Setup a byte array which will store the reversed sample, ready for playback
   reversedSample = new byte[(int)channelStream.Length];

   // Read the channelStream sample in to the reversedSample byte array
   channelStream.Read(reversedSample, 0, (int)channelStream.Length);
   // Calculate how many bytes are used per sample, whole samples are swaped in
   // positioning by the reverse class
   bytesPerSample = (channelStream.WaveFormat.BitsPerSample / 8) * channelStream.WaveFormat.Channels;
   // Pass in the byte array storing a copy of the sample, and save back to the
   // reversedSample byte array
   reversedSample = nbr.reverseSample(reversedSample, (int)channelStream.Length, bytesPerSample);

So the main difference here is that we have an additional byte array in our sample class, which will be used to store the reversedSample. We cheat a bit by doing this because we don't have to setup the wave format of the reversed sample, as it will be in exactly the same format as the sample which also means there is no requirement to setup any header information, the land of the byte array bliss.

Read What?

Thats all well and good but now we need to be able to use this reversed sample during wave play back and how do you suppose that some additional wave bytes are going to help us achieve this, well like the good little supper hero NAudio is, it fly's in stage left and flapping under it's giant red cape is a Read method which has the override directive and ties us back to the actual stream reading function. Those astute people in the audience who have read the previous tutorials and committed every word to memory should be nodding like the good bobble heads they are, recalling that we used a similar approach in the previous tutorial to provide looping functionality to our samples. An in rides our override:

public override int Read(byte[] buffer, int offset, int count)
   if (_sampleReversed)
       //need to understand why this is a more reliable offset
       offset = (int)channelStream.Position;

       // Have to work out our own number. The only time this number should be
       // different is when we hit the end of the stream but we always need to
       // report that we read the same amount. Missing data is filled in with
       // silence
       int outCount = count;

       // Find out if we are trying to read more data than is available in the buffer
       if (offset + count > reversedSample.Length)
           // If we are then reduce the read amount
           count = count - ((offset + count) - reversedSample.Length);

       for (int i = 0; i < count; i++)
           // Individually copy the samples into the buffer for reading by the overriden method
           buffer[i] = reversedSample[i + offset];

       // Setting this position lets us keep track of how much has been played back.
       // There is no other offset used to track this information
       channelStream.Position = channelStream.Position + count;

       // Regardless of how much is read the count expected by the calling method is
       // the same number as was origionaly provided to the Read method
       return outCount;
       // Normal read code, sample has not been set to loop
       return channelStream.Read(buffer, offset, count);

Whats going on again? Well we check if the sample has been requested to be played in reverse (every time the read method is called) and then work out from where we should be playing back the reversed sample - at the moment this code is just assuming you play it in one direction from start-to-end or end-to-start; it's not handling the transposing of position for the stream when a reversed flag is set mid playback - that's a small addition for another day.

Now the actual trick here comes by way of us not using the channelStream.Read method when we are using the reverse flag for playback, instead we just write directly to the byte buffer, the samples which are ready for playback. Notice if the stream is not reversed then there is no need to do this, we let it go on using the channelStream.Read method as it always has. So why does this work, well instead of us relying on a standard stream method to read back the data we just copy in data we deem necessary and because this data is in the same format (remember all the reversal occurs after the channelStream has been created) we don't need to do any conversion on the byte array. There was a few little oddities this deals with, like always saying we read as much as was requested even if we didn't but if we don't we throw an exception somewhere else, Nice.

Thats actually about it for this tutorial. I haven't linked this back to the form inside the text here but you can download the sample project and have a look at how it all hangs together (there is really nothing new going on there except for a reverse check box and I don't want to insult any ones intelligence by explaining how that works here). There are two little omissions from this tutorial which I will leave you as homework (that I'll undoubtedly have to do for OpenSebJ at some point in time)
1. Looping of the reversed sample for playback
2. Transposing playback position so a dynamic switching between reversed and non-reversed sample playback can be handled.


Thats a wrap for this Tutorial on how to Reverse a Wave Sample in C# using the NAudio Framework. All of this content and the example project is available for download from here. Let me know how you go and where you use it, always interested in hearing about Audio C# Development.

Until next time, when we look at recording wave files, direct to disk.


Anonymous said...

I get a 404 on this link above:

I'm getting the same "Offset and length were out of bounds..." exception when using NAudio and would love to have your patched version. Could you re-post?


Eliot Stock.

OpenSebJ said...

Hi Eliot,

Could you please try again, the file is certainly there now.

If you can't access it for some other reason though, send me an email and I'll forward it to you.