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:
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:
public class WaveBuffer : IWaveBuffer
public int numberOfBytes;
private byte byteBuffer;
private float floatBuffer;
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:
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 problems? 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
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.