C# Audio CD Writer for Windows XP
As many of you who have looked into CD writing using the .NET runtime may have found,
there is a beautiful
MSDN example written by Anson Horton on utilising the IMAPI (Image Mastering
API) service which ships with Windows XP and 2003 Server operating systems.
While the original example component provides a fantastic starting point (certainly
got me on the right path), you will quickly find an issue with the sample —
an inability to write Audio CD’s.
The crux of Audio CD writing in IMAPI is based around the IRedbookDiscMaster interface. Our C# COM declaration goes something like this (this is actually in the original MSDN sample):
[ComImport]
[Guid("E3BC42CD-4E5C-11D3-9144-00104BA11C5E")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
internal interface IRedbookMaster
{
void GetTotalAudioTracks( out int pnTracks);
void GetTotalAudioBlocks( out int pnBlocks);
void GetUsedAudioBlocks( out int pnBlocks);
void GetAvailableAudioTrackBlocks( out int pnBlocks);
void GetAudioBlockSize( out int pnBlockBytes);
void CreateAudioTrack( int nBlocks);
void AddAudioTrackBlocks( ref byte pby, int cb);
void CloseAudioTrack();
}
MSDN's IMAPI platform SDK tells us that in order to burn an API CD we should perform the following operations for each track that we wish to burn:
- Use CreateAudioTrack(int nBlocks) to indicate that we're starting a new track.
- Call AddAudioTrackBlocks(out int pnBlocks) to add the actual raw audio block data to the track.
- Close the audio track using CloseAudioTrack()
Well that's great, but what exactly is an audio block? An audio block is a byte array of no more than 2352. So when we're feeding IRedbookMaster.AddAudioTrackBlocks data, we send raw audio data in multiples of 2352.
So now you're probably asking, how do I read raw audio data? And what audio data can I send through the IRedbookMaster.AddAudioTrackBlocks method?
IMAPI's IRedbookMaster is only capable of accepting 44.1 KHz 16 bit raw audio. This is essentially a typical PCM wav file. Googleing around the web a little bit, we can quickly learn the structure of a wav file. It looks something like this:

So to read our favourite wav file into usable data we'll need the following data structures.
[StructLayout( LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
struct RIFFChunk
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst=4)]
public char [] riff;
public int length;
[MarshalAs(UnmanagedType.ByValArray, SizeConst=4)]
public char [] wave;
}
[StructLayout( LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
struct FormatHeader
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst=8)]
public char [] formatHeader;
// Format Fields
short audioFormat;
short numOfChannels;
int sampleRate;
int avgBytesPerSecond;
short bytesPerSample;
short bitsPerSample;
}
[StructLayout( LayoutKind.Sequential, Pack=1, CharSet=CharSet.Ansi)]
struct DataChunk
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst=4)]
public char [] data;
public int length;
}
Now that we have the 3 components of a wav file described, each having fixed-sizes and correct layouts, it's time to get our fingers dirty in some FileStream and Marshalling code. I've encapsulated the entire audio track writing procedure in a single method, I'll explain as we go:
private void CreateAudioTracks()
{
foreach ( string fileName in this .fFiles.Keys )
{
FileStream fileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read);
At this stage we've simply begun the iteration of wav files that we'd like to burn onto the cd. We create a System.IO.FileStream class to do all the reading for us.
byte [] b = new byte [Marshal.SizeOf( typeof (RIFFChunk))];
fileStream.Read(b, 0, b.Length);
// Load riffchunk of wav file
RIFFChunk riffChunk = (RIFFChunk) RawDeserializeEx(b, typeof (RIFFChunk));
Scary stuff! Not really. Here we need to calculate the actual byte size of a RIFFChunk data structure. To do this you just call Marshal.SizeOf(System.Type t), and that will give you the raw byte size of the object that you've defined.
So we define a simple byte[] array called b, our fileStream will fill the byte[] array with data in the way file. At this stage it's a chunk of useless data which is unusable. To convert the bytes into an actual usable RIFFChunk, we call a method which I find myself using quite frequently these days, called RawDeserializeEx(byte[] rawdata, System.Type t).
Essentially all this method does is perform a Marshal.PtrToStructure() call based on the byte array that we provide and the object type. It then returns the object.
// check for valid file
if ( new string (riffChunk.riff) != "RIFF"
&& new string (riffChunk.wave) != "WAVE")
{
throw new Exception("Not a valid wav file!");
}
So now that we've successfully read the wav's RIFFChunk, we should find that the riffChunk.riff and riffChunk.wave properties, each contain a char[] array which strings up into “RIFF” and “WAVE” respectively. If you place a breakpoint after loading your RIFFChunk and you don't have tidy characters in the array, then either your file is something other than a wave or your wav file is corrupt.
b = new byte [Marshal.SizeOf( typeof (FormatHeader))];
fileStream.Read(b, 0, b.Length);
FormatHeader formatHeader = (FormatHeader)RawDeserializeEx(b, typeof (FormatHeader));
Here we do a similar thing to before, except this time we're loading the FormatHeader from the file. This will return all sorts of information about our wav file. Sample rate, approximate length, stuff like that.
DataChunk dataChunk;
b = new byte [Marshal.SizeOf( typeof (DataChunk))];
// Locate the the .wav data chunk. I think a whole
// bunch of meta data is stored in the front of the file
// and the raw audio begins where we first encounter a
// valid DataChunk.
while ( true )
{
// Load DataChunks in the wav file
int nRead = fileStream.Read(b, 0, b.Length);
dataChunk = (DataChunk) RawDeserializeEx(b, typeof (DataChunk));
if ( nRead == 0 )
{
throw new Exception("Error while reading wav file!");
}
if ( new string (dataChunk.data).ToLower()=="data" )
{
break ;
}
Debug.WriteLine(String.Format("Skipping chunk: '{0}{1}{2}{3}'",
dataChunk.data[0], dataChunk.data[1],
dataChunk.data[2], dataChunk.data[3]));
int i = 0;
while ( i < dataChunk.length )
{
char [] buffer = new char [16];
int nToRead = 16;
if ( nToRead > ( dataChunk.length - i ) )
nToRead = ( dataChunk.length - i );
b = new byte [16];
nRead = fileStream.Read(b, 0, ( int )nToRead);
for ( int x=0;x<b.Length;x++)
{
buffer[x] = ( char )b[x];
}
if ( nRead != nToRead )
{
throw new Exception( "Error while reading wav file!");
}
i += nRead;
}
}
Here what we're doing is skipping through the Data chunks of the wave file until we end up with one which contains the text “data”. If we find it, this would indicate the starting position of the wav's actual raw audio data.
ulong nBlocksCount = ( ulong )dataChunk.length / 2352;
if ( dataChunk.length % 2352 != 0 )
nBlocksCount++;
fMusicDiscWriter.CreateAudioTrack( ( int )nBlocksCount );
Now we're calculating the number of blocks that the wav file will use on our CD. Remember, a block is 2352 bytes of data. Obviously its pretty unlikely that our file is exactly divisble by 2352, so we increment the number of blocks by 1 if there's a remainder.
Once we have our number, we just pass it to IRedbookDiscMaster as we indicate that we would like to start a new track.
for ( ulong k = 0; k < nBlocksCount; k++ )
{
byte [] blocks = new byte [2352];
ulong nToRead = 2352;
if ( k == ( nBlocksCount - 1 ) )
nToRead = ( ulong )dataChunk.length % 2352;
int nRead = fileStream.Read( blocks, 0, ( int )nToRead );
if ( nRead != ( int )nToRead )
{
throw new Exception( "Error while reading wav file!");
}
fMusicDiscWriter.AddAudioTrackBlocks( ref blocks[0], 2352 );
}
Here we're just passing in audio blocks to IRedbookDiscMaster. Fairly simple
fMusicDiscWriter.CloseAudioTrack();
fileStream.Close();
}
}
And finally, we can close our audio track and filestream.