Sunday, 22 June 2008

WaveBuffer - Casting Byte Arrays to Float Arrays

A while ago on my blog I wrote about a C# language feature that I wanted - reinterpret casts between arrays of structs. One reason this would be so useful to me is that I want to improve the design of my open source audio library NAudio, and create an IWaveProvider interface that allows people to output their audio data in whatever format is most convenient for them. Audio is sometimes in 16 bit integer format, sometimes in 32 bit floating point format, and sometimes in compressed blocks of bytes (other common scenarios include 24 bit integers and 64 bit double precision floating point audio).

In the world of C/C++, this isn't a problem. The Read method of IWaveProvider simply needs to take a void pointer that can be cast to a byte, short or float pointer as appropriate. In .NET things are not nearly so simple. True, there are 'unsafe' pointers in C#, but using them immediately excludes developers from other .NET languages such as VB.NET from using the framework. Also, when reading or writing data from files in .NET, you must work with the System.IO.Stream class that expects reads and writes to provide byte arrays, requiring a manual copy from the pointer to an array.

My initial idea was to simply provide a variety of Read functions for IWaveProvider, and provide helper functions in an abstract base class that would allow users just to implement the one Read method whose signature best fitted their needs:

interface IWaveProvider 
{
   int Read(byte[] buffer, int byteCount);
   int Read(short[] buffer, int sampleCount);
   int Read(float[] buffer, int sampleCount);
   ...

However, a new contributor to the NAudio project, Alexandre Mutel, has come up with an ingenious solution thanks to a brilliant piece of lateral thinking. Suppose we define a WaveBuffer class that uses an explicit structure layout:

[StructLayout(LayoutKind.Explicit, Pack=2)]
public class WaveBuffer : IWaveBuffer
{
   [FieldOffset(0)]
   public int numberOfBytes;
   [FieldOffset(4)]
   private byte[] byteBuffer;
   [FieldOffset(4)]
   private float[] floatBuffer;
   [FieldOffset(4)]
   private short[] shortBuffer;
   ...

This class has some interesting capabilities. You can set byteBuffer to point to a new byte array, but then access it using floatBuffer. Sounds dangerous? Well it compiles, and initial tests show that it works just fine. It is true that using the floatBuffer accessor will let you write beyond the end of available space, but so long as you never write more than the requested number of samples to the buffer, you are safe. This structure even survives garbage collections without any issues.

This allows us to simplify our IWaveProvider interface dramatically:

interface IWaveProvider
{
   int Read(IWaveBuffer buffer);
   ...

Implementers of the Read method then have a choice of which buffer they write into. If they simply want to write samples (whether 16 bit integers or 32 bit floats) that is fine, but equally if it is easier to provide their data as a byte array (for example when reading from a WAV file), then that can be done. The WaveBuffer trick effectively gives us the casting feature we need.

Sounds too good to be true? Well there are some potential concerns. This approach could be described as a bit of a "hack". Do we know for sure that in a future version of the .NET framework it will still work (or even compile)? Does it work with 64 bit Windows? Could there be a garbage collection scenario we have not yet encountered that would cause us pr oblems? Would people object to using a hack like this right at the core of the NAudio framework?

The solution to these concerns is fairly simple. We will use an interface, IWaveBuffer instead of using WaveBuffer itself. This allows us to create an alternative implementation if ever we find that WaveBuffer has any issues.

So the plan is that NAudio will be migrating to use IWaveProvider and IWaveBuffer in the future (not for version 1.2, but probably appearing in the following version), but if anyone can think of any problems with using the proposed WaveBuffer class, I would be interested to hear them.

Post a Comment