Converting CDA to WAV

This article explains the procedure for converting a CDA to a WAV.

Converting .CDA to .WAV (the hard way)

I have recently been delving into some of the low-level file IO operations available in the Kernel32 API. My end game would ideally be to learn how to to write audio CD's using low level Kernel32 calls. But the first step in that game is to better understand the current standard. This can be at least partially accomplished through low-level reading of the binary data in an existing CD.

Before diving into the details, I want to explain that I have intentionally kept this program as simple as possible. I could have "Classified" (ha ha ha; nerd humor) the whole thing but I did not want you to have to dig through ten different documents to trace the flow of the reads and writes. I also did no error handling (I use this technique to aid in trouble; really I'm just lazy). You could really build this up breaking this into classes with events and even adding an internet lookup for the CD data, but, I'm lazy. ;}
 
Code Snippet: readcd      

        void readcd()

        {

            bool TocValid = false;

            IntPtr cdHandle = IntPtr.Zero;

            CDROM_TOC Toc = null;

            int track, StartSector, EndSector;

            BinaryWriter bw;

            bool CDReady;

            uint uiTrackCount, uiTrackSize, uiDataSize;

            int i;

            uint BytesRead, Dummy;

            char Drive = (char)cmbDrives.Text[0];

            TRACK_DATA td;

            int sector;

            byte[] SectorData;

            IntPtr pnt;

            Int64 Offset;

 

            btnStart.Enabled = false;

 

            Dummy = 0;

            BytesRead = 0;

            CDReady = false;

 

            Toc = new CDROM_TOC();

            IntPtr ip = Marshal.AllocHGlobal((IntPtr)(Marshal.SizeOf(Toc)));

            Marshal.StructureToPtr(Toc, ip, false);

            // is it a cdrom drive

            DriveTypes dt = GetDriveType(Drive + ":\\");

            if (dt == DriveTypes.DRIVE_CDROM)

            {

                // get a Handle to control the drive with

                cdHandle = CreateFile("\\\\.\\" + Drive + ':', GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);

                CDReady = DeviceIoControl(cdHandle, IOCTL_STORAGE_CHECK_VERIFY, IntPtr.Zero, 0, IntPtr.Zero, 0, ref Dummy, IntPtr.Zero) == 1;

                if (!CDReady)

                {

                    MessageBox.Show("Drive Not Ready", "Drive Not Ready", MessageBoxButtons.OK);

 

                }

                else

                {

                    uiTrackCount = 0;

                    // is the Table of Content valid?

                    TocValid = DeviceIoControl(cdHandle, IOCTL_CDROM_READ_TOC, IntPtr.Zero, 0, ip, (uint)Marshal.SizeOf(Toc), ref BytesRead, IntPtr.Zero) != 0;

                    //fetch the data from the unmanaged pointer back to the managed structure

                    Marshal.PtrToStructure(ip, Toc);

                    if (!TocValid)

                    {

                        MessageBox.Show("Invalid Table of Content ", "Invalid Table of Content ", MessageBoxButtons.OK);

                    }

                    else

                    {

                        // really only nescary if there are un-useable tracks

                        uiTrackCount = Toc.LastTrack;

                        //for (i = Toc.FirstTrack - 1; i < Toc.LastTrack; i++)

                        //{

                        //    if (Toc.TrackData[i].Control == 0)

                        //        uiTrackCount++;

                        //}

                        // create a jagged array to store the track data

                        TrackData = new byte[uiTrackCount][];

                       

 

                        // read all the tracks

                        for (track = 1; track <= uiTrackCount; track++)//uiTrackCount; track++)

                        {

                            Offset = 0;// used to store Sectordata into trackdata

                            label1.Text = "Reading Track" + track.ToString() + " of " + uiTrackCount.ToString(); ;

                            Application.DoEvents();

                            // create a binary writer to write the track data

                            bw = new BinaryWriter(File.Open(Application.StartupPath + "\\Track" + track.ToString (), FileMode.Create));

 

                            //The CDROM_TOC-structure contains the FirstTrack (1) and the LastTrack (max. track nr). CDROM_TOC::TrackData[0] contains info of the

                            //first track on the CD. Each track has an address. It represents the track's play-time using individual members for the hour, minute,

                            //second and frame. The "frame"-value (Address[3]) is given in 1/75-parts of a second -> Remember: 75 frames form one second and one

                            //frame occupies one sector.

 

                            //Find the first and last sector of the track

                            td = Toc.TrackData[track - 1];

                            //              minutes                   Seconds       fractional seconds     150 bytes is the 2 second lead in to track 1

                            StartSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 150;

                            td = Toc.TrackData[track];

                            EndSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 151;

                            progressBar1.Minimum = StartSector;

                            progressBar1.Maximum = EndSector;

                            uiTrackSize = (uint)(EndSector - StartSector) * CB_AUDIO;//CB_AUDIO==2352

                            // how big is the track

                            uiDataSize = (uint)uiTrackSize;

                            //Allocate for the track

                            TrackData[track - 1] = new byte[uiDataSize];

                            SectorData = new byte[CB_AUDIO * NSECTORS];

 

                            // read all the sectors for this track

                            for (sector = StartSector; (sector < EndSector); sector += NSECTORS)

                            {

                                Debug.Print(sector.ToString("X2"));

                                RAW_READ_INFO rri = new RAW_READ_INFO();// contains info about the sector to be read

                                rri.TrackMode = TRACK_MODE_TYPE.CDDA;

                                rri.SectorCount = (uint)1;

                                rri.DiskOffset = sector * CB_CDROMSECTOR;

                                //get a pointer to the structure

                                Marshal.StructureToPtr(rri, ip, false);

                                // allocate an unmanged pointer to hold the data read from the disc

                                int size = Marshal.SizeOf(SectorData[0]) * SectorData.Length;

                                pnt = Marshal.AllocHGlobal(size);

 

                                //Sector data is a byte array to hold data from each sector data

                                // initiallize it to all zeros

                                SectorData.Initialize();

 

 

                                // read the sector

                                i = DeviceIoControl(cdHandle, IOCTL_CDROM_RAW_READ, ip, (uint)Marshal.SizeOf(rri), pnt, (uint)NSECTORS * CB_AUDIO, ref BytesRead, IntPtr.Zero);

                                if (i == 0)

                                {

                                    MessageBox.Show("Bad Sector Read", "Bad Sector Read from sector " + sector.ToString("X2"), MessageBoxButtons.OK);

                                    break;

                                }

                                progressBar1.Value = sector;                             // return the pointers to their respective managed data sources

                                Marshal.PtrToStructure(ip, rri);

                                Marshal.Copy(pnt, SectorData, 0, SectorData.Length);

 

                                Marshal.FreeHGlobal(pnt);

                                Array.Copy(SectorData, 0, TrackData[track - 1], Offset, BytesRead);

                                Offset += BytesRead;

                            }

 

                            // write the binary data nad then close it

                            bw.Write(TrackData[track - 1]);

                            bw.Close();

                        }

                        //unlock

                        PREVENT_MEDIA_REMOVAL pmr = new PREVENT_MEDIA_REMOVAL();

                        pmr.PreventMediaRemoval = 0;

                        ip = Marshal.AllocHGlobal((IntPtr)(Marshal.SizeOf(pmr)));

                        Marshal.StructureToPtr(pmr, ip, false);

                        DeviceIoControl(cdHandle, IOCTL_STORAGE_MEDIA_REMOVAL, ip, (uint)Marshal.SizeOf(pmr), IntPtr.Zero, 0, ref Dummy, IntPtr.Zero);

                        Marshal.PtrToStructure(ip, pmr);

                        Marshal.FreeHGlobal(ip);

                    }

                }

            }

            //Close the CD Handle

            CloseHandle(cdHandle);

            ConvertToWav();

        }

        // this functions reads binary audio data from a cd and stores it in a jagged array called TrackData

        // it uses only low level file io calls to open and read the Table of Content and then the binary 'music' data sector by sector

        // as discovered from the table of content

        // it also writes it to a binary file called tracks with not extension

        // this file can be read by any decent hex editor

        void readcd()

        {

            bool TocValid = false;

            IntPtr cdHandle = IntPtr.Zero;

            CDROM_TOC Toc = null;

            int track, StartSector, EndSector;

            BinaryWriter bw;

            bool CDReady;

            uint uiTrackCount, uiTrackSize, uiDataSize;

            int i;

            uint BytesRead, Dummy;

            char Drive = (char)cmbDrives.Text[0];

            TRACK_DATA td;

            int sector;

            byte[] SectorData;

            IntPtr pnt;

            Int64 Offset;

 

            btnStart.Enabled = false;

 

            Dummy = 0;

            BytesRead = 0;

            CDReady = false;

 

            Toc = new CDROM_TOC();

            IntPtr ip = Marshal.AllocHGlobal((IntPtr)(Marshal.SizeOf(Toc)));

            Marshal.StructureToPtr(Toc, ip, false);

            // is it a cdrom drive

            DriveTypes dt = GetDriveType(Drive + ":\\");

            if (dt == DriveTypes.DRIVE_CDROM)

            {

                // get a Handle to control the drive with

                cdHandle = CreateFile("\\\\.\\" + Drive + ':', GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);

                CDReady = DeviceIoControl(cdHandle, IOCTL_STORAGE_CHECK_VERIFY, IntPtr.Zero, 0, IntPtr.Zero, 0, ref Dummy, IntPtr.Zero) == 1;

                if (!CDReady)

                {

                    MessageBox.Show("Drive Not Ready", "Drive Not Ready", MessageBoxButtons.OK);

 

                }

                else

                {

                    uiTrackCount = 0;

                    // is the Table of Content valid?

                    TocValid = DeviceIoControl(cdHandle, IOCTL_CDROM_READ_TOC, IntPtr.Zero, 0, ip, (uint)Marshal.SizeOf(Toc), ref BytesRead, IntPtr.Zero) != 0;

                    //fetch the data from the unmanaged pointer back to the managed structure

                    Marshal.PtrToStructure(ip, Toc);

                    if (!TocValid)

                    {

                        MessageBox.Show("Invalid Table of Content ", "Invalid Table of Content ", MessageBoxButtons.OK);

                    }

                    else

                    {

                        // really only nescary if there are un-useable tracks

                        uiTrackCount = Toc.LastTrack;

                        //for (i = Toc.FirstTrack - 1; i < Toc.LastTrack; i++)

                        //{

                        //    if (Toc.TrackData[i].Control == 0)

                        //        uiTrackCount++;

                        //}

                        // create a jagged array to store the track data

                        TrackData = new byte[uiTrackCount][];

                       

 

                        // read all the tracks

                        for (track = 1; track <= uiTrackCount; track++)//uiTrackCount; track++)

                        {

                            Offset = 0;// used to store Sectordata into trackdata

                            label1.Text = "Reading Track" + track.ToString() + " of " + uiTrackCount.ToString(); ;

                            Application.DoEvents();

                            // create a binary writer to write the track data

                            bw = new BinaryWriter(File.Open(Application.StartupPath + "\\Track" + track.ToString (), FileMode.Create));

 

                            //The CDROM_TOC-structure contains the FirstTrack (1) and the LastTrack (max. track nr). CDROM_TOC::TrackData[0] contains info of the

                            //first track on the CD. Each track has an address. It represents the track's play-time using individual members for the hour, minute,

                            //second and frame. The "frame"-value (Address[3]) is given in 1/75-parts of a second -> Remember: 75 frames form one second and one

                            //frame occupies one sector.

 

                            //Find the first and last sector of the track

                            td = Toc.TrackData[track - 1];

                            //              minutes                   Seconds       fractional seconds     150 bytes is the 2 second lead in to track 1

                            StartSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 150;

                            td = Toc.TrackData[track];

                            EndSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 151;

                            progressBar1.Minimum = StartSector;

                            progressBar1.Maximum = EndSector;

                            uiTrackSize = (uint)(EndSector - StartSector) * CB_AUDIO;//CB_AUDIO==2352

                            // how big is the track

                            uiDataSize = (uint)uiTrackSize;

                            //Allocate for the track

                            TrackData[track - 1] = new byte[uiDataSize];

                            SectorData = new byte[CB_AUDIO * NSECTORS];

 

                            // read all the sectors for this track

                            for (sector = StartSector; (sector < EndSector); sector += NSECTORS)

                            {

                                Debug.Print(sector.ToString("X2"));

                                RAW_READ_INFO rri = new RAW_READ_INFO();// contains info about the sector to be read

                                rri.TrackMode = TRACK_MODE_TYPE.CDDA;

                                rri.SectorCount = (uint)1;

                                rri.DiskOffset = sector * CB_CDROMSECTOR;

                                //get a pointer to the structure

                                Marshal.StructureToPtr(rri, ip, false);

                                // allocate an unmanged pointer to hold the data read from the disc

                                int size = Marshal.SizeOf(SectorData[0]) * SectorData.Length;

                                pnt = Marshal.AllocHGlobal(size);

 

                                //Sector data is a byte array to hold data from each sector data

                                // initiallize it to all zeros

                                SectorData.Initialize();

 

 

                                // read the sector

                                i = DeviceIoControl(cdHandle, IOCTL_CDROM_RAW_READ, ip, (uint)Marshal.SizeOf(rri), pnt, (uint)NSECTORS * CB_AUDIO, ref BytesRead, IntPtr.Zero);

                                if (i == 0)

                                {

                                    MessageBox.Show("Bad Sector Read", "Bad Sector Read from sector " + sector.ToString("X2"), MessageBoxButtons.OK);

                                    break;

                                }

                                progressBar1.Value = sector;                             // return the pointers to their respective managed data sources

                                Marshal.PtrToStructure(ip, rri);

                                Marshal.Copy(pnt, SectorData, 0, SectorData.Length);

 

                                Marshal.FreeHGlobal(pnt);

                                Array.Copy(SectorData, 0, TrackData[track - 1], Offset, BytesRead);

                                Offset += BytesRead;

                            }

 

                            // write the binary data nad then close it

                            bw.Write(TrackData[track - 1]);

                            bw.Close();

                        }

                        //unlock

                        PREVENT_MEDIA_REMOVAL pmr = new PREVENT_MEDIA_REMOVAL();

                        pmr.PreventMediaRemoval = 0;

                        ip = Marshal.AllocHGlobal((IntPtr)(Marshal.SizeOf(pmr)));

                        Marshal.StructureToPtr(pmr, ip, false);

                        DeviceIoControl(cdHandle, IOCTL_STORAGE_MEDIA_REMOVAL, ip, (uint)Marshal.SizeOf(pmr), IntPtr.Zero, 0, ref Dummy, IntPtr.Zero);

                        Marshal.PtrToStructure(ip, pmr);

                        Marshal.FreeHGlobal(ip);

                    }

                }

            }

            //Close the CD Handle

            CloseHandle(cdHandle);

            ConvertToWav();

        } 

The program flows like this:

  1. Select the CD to read and press start, calling the readcd procedure.

  2. The readcd procedure verifies that the drive is a cdrom drive

  3. It then gets a handle to the drive via a call to CreateFile in Kernel32.

  4. Next we use that handle to see if the drive is ready to read using DeviceIoControl in kerenl32.

  5. If the drive is ready then we see if it has a valid Table of Contents (hereafter refereed to as TOC) again using DeviceIoControl.

  6. If the TOC is valid then we next read the TOC using DeviceIoControl.

  7. Using the TOC we determine how may tracks there are on the CD (no file IO here; we already have the TOC). Then in an iteration though all the tracks we proceed.

  8. We create a binary writer for use in writing the binary files.

  9. We cast the TOC track data into a kernel32 structure called TRACK_DATA.

  10. Using that structure we are able to determine which sector holds the beginning of that track.

  11. And the l sector will be one sector less than the start sector of the next track.
    Side note: There are many pointers to structures and byte arrays so there is also a lot of converting back and forth between them.

  12. Track size is expressed in number of sectors by subtracting start from end.

  13. Now we iterate through all sectors in this track.

  14. We create a  kernel32 RAW_READ_INFO structure to be used in the DeviceIoControl call to read the sector.

  15. The structure informs the DeviceIoControl call that we are reading a CD and that we are reading one sector where he sector is on the disc.
    (Remember CD sectors are a little different than HD sectors; more on that latter.)

  16. Now we read that sector  via DeviceIoControl. If it was successful then we retrieve the sector data just read.

  17. Put the sector data into the appropriat4e place in the TrackData jagged array.

  18. Repeat for all sectors in the track.

  19. Repeat for all tracks on the CD.

  20. Close the handle to the drive using CloseHandle in kerenl32.

  21. Call ConvertToWav.

Code Snippet -  ConvertToWav

       

        // this procedure tacks the biary data stored in the jagged array called TraackData

        // and, using low level file io functions) writes it out as a .wav file called trackx.wav

        private void ConvertToWav()

        {

            int i, j, k, track, tracks;

            byte[] b;

            char[] riffchunk ={ 'R', 'I', 'F', 'F' };

            char[] wavechunk ={ 'W', 'A', 'V', 'E' };

            char[] datachunk ={ 'd', 'a', 't', 'a' };

            char[] fmtchunk ={ 'f', 'm', 't', ' ' };

            Int32 riffsize, datasize, fmtsize, extrabits;

            Int32 DI, SampleRate, ByteRate;

            uint BytesWritten;

            Int16 BlockAlign, Format, NumChannels, BitsPerSample;

            Byte[] Image;

            IntPtr FileHandle;

 

            Format = 1; // PCM

            NumChannels = 2;// Stereo

            SampleRate = 44100;// 44100 Samples per secon

            BitsPerSample = 16; // 16 bits per sample

            ByteRate = SampleRate * NumChannels * BitsPerSample / 8;

            BlockAlign = 4;

            fmtsize = 0x12;// size of the 'fmt ' chunk is 18 bytes

            // get the number of tarcks stoerd in track data

            tracks = TrackData.GetUpperBound(0);

            // setup the progressbar

            progressBar1.Maximum = tracks;

            progressBar1.Minimum = 0;

            // do all the tracks

            for (track = 0; track <= tracks; track++)

            {

                DI = 0;//IDI is an index into the Image array where the next chunk of data will be stored

                progressBar1.Value = track;

                label1.Text = "Writeing Track " + (track + 1).ToString() + ".wav";

                Application.DoEvents();

                // Create a File called trackx.wav and return a handle to it

                FileHandle=CreateFile(Application.StartupPath + "\\Track" + (track + 1).ToString() + ".wav",GENERIC_WRITE,0,IntPtr.Zero ,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero );

                // Wav file format is notthe subject of this project .. .

                // suffice it to say that at minimum there is a Header which is followed by the PCM, Stereo , 44100 Hz Sample rate binary data

                // for more info on Wav format plese visit:

                //http://www.sonicspot.com/guide/wavefiles.html

 

                //Start prepareing the RIFF header

 

                // how big the the 'music' binary data

                datasize = TrackData[track].Length;

                //build the header

                riffsize = datasize;

                riffsize += 4;//RIFFSize

                riffsize += 4;//WAVE

                riffsize += 4;//fmt

                riffsize += fmtsize;

                riffsize += 4;// DATA

                riffsize += 4;//datasize

                extrabits = 0;

                // build the image

                Image = new Byte[riffsize + 8];// riffchunk + riffsize

                b = Encoding.ASCII.GetBytes(riffchunk);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

                b = BitConverter.GetBytes(riffsize);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

                b = Encoding.ASCII.GetBytes(wavechunk);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = Encoding.ASCII.GetBytes(fmtchunk);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(fmtsize);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(Format);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(NumChannels);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(SampleRate);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(ByteRate);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(BlockAlign);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(BitsPerSample);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(extrabits);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = Encoding.ASCII.GetBytes(datachunk);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(datasize);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                // add the digital 'music' data retrieved earler

                Array.Copy(TrackData[track], 0, Image, DI, TrackData[track].Length);

                // write the binary file - trackx.wav

                i = WriteFile(FileHandle, Image, (uint)Image.Length, out BytesWritten, IntPtr.Zero);

                //if successful then

                // flush all buffers used in the low level write operation

                // then close the file

                if(i!= 0)

                {

                    //Flush the file buffers to force writing of the data.

                    i = FlushFileBuffers(FileHandle);

                    //Close the file.

                    i = CloseHandle(FileHandle);

                }

                // the wave file now exists (created by reading the CD and can be playedby most wav players

                Image = null;

                progressBar1.Value = track;

            }

        }

The procedure ConvertToWav takes the binary data from TrackData (read in readcd). It flows as follows:

  1. Initialize the four ChunkIds that are required for a .wav header.

  2. Then we initialize the various parts of the three main chunks to represent PCM, Stereo, 44100 Samples per second, and other aspects that represent true CD data.

  3. Next, we iterate through all tracks as represented in the jagged array TrackData.

  4. Create a file called "Track(x).wav" and return a handle to it using CreateFile in Kernel32.

  5. Build the Header.

  6. Add the "Music" data.

  7. Write the file using WriteFile from Kernel32.

  8. Proceed if that was successful.

  9. We flush all buffers used in WriteFile.

  10. And we close the file using CloseHandle.

  11. Return and do the next track until they are all done.

  12. And now we go play the wav files and gloat over how cooked we are. :}

Audio CD Format

An audio CD has (essentially) two parts; a Table of Contents and data.

The ToC contains information about how many tracks are on the CD and the address of each track. Each address is given in four bytes representing the time slot in which the track begins (hours, minutes, seconds and fractional seconds). Hours is never used because minutes can represent 4 1/4 hours. The first track starts 2 seconds after the ToC so subtract 150 bytes (75 bytes per second * 2 seconds= 150 bytes). The start sector for each track can be read as follows: The minutes byte times 60 seconds times 75 bytes per second + seconds byte time 75 bytes per second plus fractional seconds. The subtract 150 byte for the 2 second lead in.

td = Toc.TrackData[track - 1];

//minutes Seconds fractional seconds 150 bytes is the 2 second lead in to track

StartSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 150;
                         
The end sector address is the sart sector address of the next track minus 1 byte.

 td = Toc.TrackData[track];
EndSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 151;
                           
Normally a sector is 2048 bytes in length. In audio CDs the sector is shorter (2352 bytes) because each sector holds 1/75 of a second worth of audio data. The arithmetic works as such: 44100 samples per second, 2 bytes per sample and 2 channels equals 176400 bytes per second and 176400 bytes per sector times 1/75 secunds (per sector) = 2352 bytes per sector (44100*2*2=176400; 176400 / 75=2352.

And so we can read the ToC and then, sector by sector read the binary music data in each track and do with it as we will, however I'm lazy. ;}

References

Full Code
 

using System;

using System.Collections.Generic;

using System.IO;

using System.Runtime.InteropServices;

using System.Text;

using System.Windows.Forms;

using System.Diagnostics;

 

namespace cda_to_wav

{

    public partial class Form1 : Form

    {

#region Kernel32 stuff

        [System.Runtime.InteropServices.DllImport("Kernel32.dll")]

        public extern static int FlushFileBuffers(IntPtr FileHandle);

        [System.Runtime.InteropServices.DllImport("Kernel32.dll")]

        public extern static DriveTypes GetDriveType(string drive);

        [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]

        public extern static IntPtr CreateFile(string FileName, uint DesiredAccess,

          uint ShareMode, IntPtr lpSecurityAttributes,

          uint CreationDisposition, uint dwFlagsAndAttributes,

          IntPtr hTemplateFile);

        [DllImport("kernel32.dll")]

        public static extern int WriteFile(IntPtr hFile,

        [MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer, // also tried this.

        uint nNumberOfBytesToWrite,

        out uint lpNumberOfBytesWritten,

        IntPtr lpOverlapped);

 

 

        [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]

        public extern static int DeviceIoControl(IntPtr hDevice, uint IoControlCode,

          IntPtr lpInBuffer, uint InBufferSize,

          IntPtr lpOutBuffer, uint nOutBufferSize,

          ref uint lpBytesReturned,

          IntPtr lpOverlapped);

        [System.Runtime.InteropServices.DllImport("Kernel32.dll", SetLastError = true)]

        public extern static int CloseHandle(IntPtr hObject);

        public enum TRACK_MODE_TYPE { YellowMode2, XAForm2, CDDA }

        [StructLayout(LayoutKind.Sequential)]

        public class RAW_READ_INFO

        {

            public long DiskOffset = 0;

            public uint SectorCount = 0;

            public TRACK_MODE_TYPE TrackMode = TRACK_MODE_TYPE.CDDA;

        }

        public enum DriveTypes : uint

        {

            DRIVE_UNKNOWN = 0,

            DRIVE_NO_ROOT_DIR,

            DRIVE_REMOVABLE,

            DRIVE_FIXED,

            DRIVE_REMOTE,

            DRIVE_CDROM,

            DRIVE_RAMDISK

        };

        [StructLayout(LayoutKind.Sequential)]

        public class PREVENT_MEDIA_REMOVAL

        {

            public byte PreventMediaRemoval = 0;

        }

 

        public struct TRACK_DATA

        {

            public byte Reserved;

            private byte BitMapped;

            public byte Control

            {

                get

                {

                    return (byte)(BitMapped & 0x0F);

                }

                set

                {

                    BitMapped = (byte)((BitMapped & 0xF0) | (value & (byte)0x0F));

                }

            }

            public byte Adr

            {

                get

                {

                    return (byte)((BitMapped & (byte)0xF0) >> 4);

                }

                set

                {

                    BitMapped = (byte)((BitMapped & (byte)0x0F) | (value << 4));

                }

            }

            public byte TrackNumber;

            public byte Reserved1;

            /// <summary>

            /// Don't use array to avoid array creation

            /// </summary>

            public byte Address_0;

            public byte Address_1;

            public byte Address_2;

            public byte Address_3;

        };

        [StructLayout(LayoutKind.Sequential)]

        public class TrackDataList

        {

            [MarshalAs(UnmanagedType.ByValArray, SizeConst = MAXIMUM_NUMBER_TRACKS * 8)]

            private byte[] Data;

            public TRACK_DATA this[int Index]

            {

                get

                {

                    if ((Index < 0) | (Index >= MAXIMUM_NUMBER_TRACKS))

                    {

                        throw new IndexOutOfRangeException();

                    }

                    TRACK_DATA res;

                    GCHandle handle = GCHandle.Alloc(Data, GCHandleType.Pinned);

                    try

                    {

                        IntPtr buffer = handle.AddrOfPinnedObject();

                        buffer = (IntPtr)(buffer.ToInt32() + (Index * Marshal.SizeOf(typeof(TRACK_DATA))));

                        res = (TRACK_DATA)Marshal.PtrToStructure(buffer, typeof(TRACK_DATA));

                    }

                    finally

                    {

                        handle.Free();

                    }

                    return res;

                }

            }

            public TrackDataList()

            {

                Data = new byte[MAXIMUM_NUMBER_TRACKS * Marshal.SizeOf(typeof(TRACK_DATA))];

            }

        }

 

        [StructLayout(LayoutKind.Sequential)]

        public class CDROM_TOC

        {

            public ushort Length;

            public byte FirstTrack = 0;

            public byte LastTrack = 0;

 

            public TrackDataList TrackData;

 

            public CDROM_TOC()

            {

                TrackData = new TrackDataList();

                Length = (ushort)Marshal.SizeOf(this);

            }

        }

 

        protected const int NSECTORS = 1;// 13;

        protected const int UNDERSAMPLING = 1;

        protected const int CB_CDDASECTOR = 2368;

        protected const int CB_QSUBCHANNEL = 16;

        protected const int CB_CDROMSECTOR = 2048;

        protected const int CB_AUDIO = (CB_CDDASECTOR - CB_QSUBCHANNEL);

 

        public const uint IOCTL_CDROM_READ_TOC = 0x00024000;

        public const uint IOCTL_STORAGE_CHECK_VERIFY = 0x002D4800;

        public const uint IOCTL_CDROM_RAW_READ = 0x0002403E;

        public const uint IOCTL_STORAGE_MEDIA_REMOVAL = 0x002D4804;

        public const uint IOCTL_STORAGE_EJECT_MEDIA = 0x002D4808;

        public const uint IOCTL_STORAGE_LOAD_MEDIA = 0x002D480C;

        public const uint GENERIC_READ = 0x80000000;

        public const uint GENERIC_WRITE = 0x40000000;

        public const uint GENERIC_EXECUTE = 0x20000000;

        public const uint GENERIC_ALL = 0x10000000;

 

        //Share constants

        public const uint FILE_SHARE_READ = 0x00000001;

        public const uint FILE_SHARE_WRITE = 0x00000002;

        public const uint FILE_SHARE_DELETE = 0x00000004;

 

        //CreationDisposition constants

        public const uint CREATE_NEW = 1;

        public const uint CREATE_ALWAYS = 2;

        public const uint OPEN_EXISTING = 3;

        public const uint OPEN_ALWAYS = 4;

        public const uint TRUNCATE_EXISTING = 5;

 

        public const uint FILE_ATTRIBUTE_NORMAL = 0x80;

 

        public const int MAXIMUM_NUMBER_TRACKS = 100;

 

#endregion

       

        byte[][] TrackData;

 

        public Form1()

        {

            InitializeComponent();

        }

 

        private void Form1_Load(object sender, EventArgs e)

        {

            String[] Drives = Directory.GetLogicalDrives();

 

            foreach (String Drive in Drives)

            {

                DriveInfo di = new DriveInfo(Drive);

                if (di.DriveType == DriveType.CDRom)

                    cmbDrives.Items.Add(Drive);

            }

            if (cmbDrives.Items.Count > 0)

                cmbDrives.SelectedIndex = 0;

        }

 

        // used as a short cut for debuging the convert to wav procedure

        // it simply reads the binary files written by readcd into the jagged array called trackData

        //assuming you have already call readcd in a previous run and the binary data still exists

        private void readdata()

        {

            uint uiDataSize,tracks=0;

            DirectoryInfo di = new DirectoryInfo(Application.StartupPath);

            FileInfo[] fi1 = di.GetFiles();

            FileInfo fi;

            foreach(FileInfo FI in fi1)

            {

                if (FI.Extension.ToLower() == "")

                    tracks++;

            }

            TrackData = new byte[tracks][];

            for (int i = 0; i < tracks; i++)

            {

                fi = new FileInfo(Application.StartupPath + "\\Track" + Convert.ToString(i + 1));

                uiDataSize = (uint)fi.Length;

                TrackData[i] = new byte[uiDataSize];

                TrackData[i] = File.ReadAllBytes(Application.StartupPath + "\\Track" + Convert.ToString(i + 1));

            }

            ConvertToWav();

 

        }

 

        private void btnStart_Click(object sender, EventArgs e)

        {

            btnStart.Enabled = false;

            label1.Text = "";

            readcd();

            //readdata();

            label1.Text = "Done";

            btnStart.Enabled = true;

 

        }

 

        // this functions reads binary audio data from a cd and stores it in a jagged array called TrackData

        // it uses only low level file io calls to open and read the Table of Content and then the binary 'music' data sector by sector

        // as discovered from the table of content

        // it also writes it to a binary file called tracks with not extension

        // this file can be read by any decent hex editor

        void readcd()

        {

            bool TocValid = false;

            IntPtr cdHandle = IntPtr.Zero;

            CDROM_TOC Toc = null;

            int track, StartSector, EndSector;

            BinaryWriter bw;

            bool CDReady;

            uint uiTrackCount, uiTrackSize, uiDataSize;

            int i;

            uint BytesRead, Dummy;

            char Drive = (char)cmbDrives.Text[0];

            TRACK_DATA td;

            int sector;

            byte[] SectorData;

            IntPtr pnt;

            Int64 Offset;

 

            btnStart.Enabled = false;

 

            Dummy = 0;

            BytesRead = 0;

            CDReady = false;

 

            Toc = new CDROM_TOC();

            IntPtr ip = Marshal.AllocHGlobal((IntPtr)(Marshal.SizeOf(Toc)));

            Marshal.StructureToPtr(Toc, ip, false);

            // is it a cdrom drive

            DriveTypes dt = GetDriveType(Drive + ":\\");

            if (dt == DriveTypes.DRIVE_CDROM)

            {

                // get a Handle to control the drive with

                cdHandle = CreateFile("\\\\.\\" + Drive + ':', GENERIC_READ, FILE_SHARE_READ, IntPtr.Zero, OPEN_EXISTING, 0, IntPtr.Zero);

                CDReady = DeviceIoControl(cdHandle, IOCTL_STORAGE_CHECK_VERIFY, IntPtr.Zero, 0, IntPtr.Zero, 0, ref Dummy, IntPtr.Zero) == 1;

                if (!CDReady)

                {

                    MessageBox.Show("Drive Not Ready", "Drive Not Ready", MessageBoxButtons.OK);

 

                }

                else

                {

                    uiTrackCount = 0;

                    // is the Table of Content valid?

                    TocValid = DeviceIoControl(cdHandle, IOCTL_CDROM_READ_TOC, IntPtr.Zero, 0, ip, (uint)Marshal.SizeOf(Toc), ref BytesRead, IntPtr.Zero) != 0;

                    //fetch the data from the unmanaged pointer back to the managed structure

                    Marshal.PtrToStructure(ip, Toc);

                    if (!TocValid)

                    {

                        MessageBox.Show("Invalid Table of Content ", "Invalid Table of Content ", MessageBoxButtons.OK);

                    }

                    else

                    {

                        // really only nescary if there are un-useable tracks

                        uiTrackCount = Toc.LastTrack;

                        //for (i = Toc.FirstTrack - 1; i < Toc.LastTrack; i++)

                        //{

                        //    if (Toc.TrackData[i].Control == 0)

                        //        uiTrackCount++;

                        //}

                        // create a jagged array to store the track data

                        TrackData = new byte[uiTrackCount][];

                       

 

                        // read all the tracks

                        for (track = 1; track <= uiTrackCount; track++)//uiTrackCount; track++)

                        {

                            Offset = 0;// used to store Sectordata into trackdata

                            label1.Text = "Reading Track" + track.ToString() + " of " + uiTrackCount.ToString(); ;

                            Application.DoEvents();

                            // create a binary writer to write the track data

                            bw = new BinaryWriter(File.Open(Application.StartupPath + "\\Track" + track.ToString (), FileMode.Create));

 

                            //The CDROM_TOC-structure contains the FirstTrack (1) and the LastTrack (max. track nr). CDROM_TOC::TrackData[0] contains info of the

                            //first track on the CD. Each track has an address. It represents the track's play-time using individual members for the hour, minute,

                            //second and frame. The "frame"-value (Address[3]) is given in 1/75-parts of a second -> Remember: 75 frames form one second and one

                            //frame occupies one sector.

 

                            //Find the first and last sector of the track

                            td = Toc.TrackData[track - 1];

                            //              minutes                   Seconds       fractional seconds     150 bytes is the 2 second lead in to track 1

                            StartSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 150;

                            td = Toc.TrackData[track];

                            EndSector = (td.Address_1 * 60 * 75 + td.Address_2 * 75 + td.Address_3) - 151;

                            progressBar1.Minimum = StartSector;

                            progressBar1.Maximum = EndSector;

                            uiTrackSize = (uint)(EndSector - StartSector) * CB_AUDIO;//CB_AUDIO==2352

                            // how big is the track

                            uiDataSize = (uint)uiTrackSize;

                            //Allocate for the track

                            TrackData[track - 1] = new byte[uiDataSize];

                            SectorData = new byte[CB_AUDIO * NSECTORS];

 

                            // read all the sectors for this track

                            for (sector = StartSector; (sector < EndSector); sector += NSECTORS)

                            {

                                Debug.Print(sector.ToString("X2"));

                                RAW_READ_INFO rri = new RAW_READ_INFO();// contains info about the sector to be read

                                rri.TrackMode = TRACK_MODE_TYPE.CDDA;

                                rri.SectorCount = (uint)1;

                                rri.DiskOffset = sector * CB_CDROMSECTOR;

                                //get a pointer to the structure

                                Marshal.StructureToPtr(rri, ip, false);

                                // allocate an unmanged pointer to hold the data read from the disc

                                int size = Marshal.SizeOf(SectorData[0]) * SectorData.Length;

                                pnt = Marshal.AllocHGlobal(size);

 

                                //Sector data is a byte array to hold data from each sector data

                                // initiallize it to all zeros

                                SectorData.Initialize();

 

 

                                // read the sector

                                i = DeviceIoControl(cdHandle, IOCTL_CDROM_RAW_READ, ip, (uint)Marshal.SizeOf(rri), pnt, (uint)NSECTORS * CB_AUDIO, ref BytesRead, IntPtr.Zero);

                                if (i == 0)

                                {

                                    MessageBox.Show("Bad Sector Read", "Bad Sector Read from sector " + sector.ToString("X2"), MessageBoxButtons.OK);

                                    break;

                                }

                                progressBar1.Value = sector;                             // return the pointers to their respective managed data sources

                                Marshal.PtrToStructure(ip, rri);

                                Marshal.Copy(pnt, SectorData, 0, SectorData.Length);

 

                                Marshal.FreeHGlobal(pnt);

                                Array.Copy(SectorData, 0, TrackData[track - 1], Offset, BytesRead);

                                Offset += BytesRead;

                            }

 

                            // write the binary data nad then close it

                            bw.Write(TrackData[track - 1]);

                            bw.Close();

                        }

                        //unlock

                        PREVENT_MEDIA_REMOVAL pmr = new PREVENT_MEDIA_REMOVAL();

                        pmr.PreventMediaRemoval = 0;

                        ip = Marshal.AllocHGlobal((IntPtr)(Marshal.SizeOf(pmr)));

                        Marshal.StructureToPtr(pmr, ip, false);

                        DeviceIoControl(cdHandle, IOCTL_STORAGE_MEDIA_REMOVAL, ip, (uint)Marshal.SizeOf(pmr), IntPtr.Zero, 0, ref Dummy, IntPtr.Zero);

                        Marshal.PtrToStructure(ip, pmr);

                        Marshal.FreeHGlobal(ip);

                    }

                }

            }

            //Close the CD Handle

            CloseHandle(cdHandle);

            ConvertToWav();

        }

 

        // this procedure tacks the biary data stored in the jagged array called TraackData

        // and, using low level file io functions) writes it out as a .wav file called trackx.wav

        private void ConvertToWav()

        {

            int i, j, k, track, tracks;

            byte[] b;

            char[] riffchunk ={ 'R', 'I', 'F', 'F' };

            char[] wavechunk ={ 'W', 'A', 'V', 'E' };

            char[] datachunk ={ 'd', 'a', 't', 'a' };

            char[] fmtchunk ={ 'f', 'm', 't', ' ' };

            Int32 riffsize, datasize, fmtsize, extrabits;

            Int32 DI, SampleRate, ByteRate;

            uint BytesWritten;

            Int16 BlockAlign, Format, NumChannels, BitsPerSample;

            Byte[] Image;

            IntPtr FileHandle;

 

            Format = 1; // PCM

            NumChannels = 2;// Stereo

            SampleRate = 44100;// 44100 Samples per secon

            BitsPerSample = 16; // 16 bits per sample

            ByteRate = SampleRate * NumChannels * BitsPerSample / 8;

            BlockAlign = 4;

            fmtsize = 0x12;// size of the 'fmt ' chunk is 18 bytes

            // get the number of tarcks stoerd in track data

            tracks = TrackData.GetUpperBound(0);

            // setup the progressbar

            progressBar1.Maximum = tracks;

            progressBar1.Minimum = 0;

            // do all the tracks

            for (track = 0; track <= tracks; track++)

            {

                DI = 0;//IDI is an index into the Image array where the next chunk of data will be stored

                progressBar1.Value = track;

                label1.Text = "Writeing Track " + (track + 1).ToString() + ".wav";

                Application.DoEvents();

                // Create a File called trackx.wav and return a handle to it

                FileHandle=CreateFile(Application.StartupPath + "\\Track" + (track + 1).ToString() + ".wav",GENERIC_WRITE,0,IntPtr.Zero ,OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, IntPtr.Zero );

                // Wav file format is notthe subject of this project .. .

                // suffice it to say that at minimum there is a Header which is followed by the PCM, Stereo , 44100 Hz Sample rate binary data

                // for more info on Wav format plese visit:

                //http://www.sonicspot.com/guide/wavefiles.html

 

                //Start prepareing the RIFF header

 

                // how big the the 'music' binary data

                datasize = TrackData[track].Length;

                //build the header

                riffsize = datasize;

                riffsize += 4;//RIFFSize

                riffsize += 4;//WAVE

                riffsize += 4;//fmt

                riffsize += fmtsize;

                riffsize += 4;// DATA

                riffsize += 4;//datasize

                extrabits = 0;

                // build the image

                Image = new Byte[riffsize + 8];// riffchunk + riffsize

                b = Encoding.ASCII.GetBytes(riffchunk);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

                b = BitConverter.GetBytes(riffsize);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

                b = Encoding.ASCII.GetBytes(wavechunk);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = Encoding.ASCII.GetBytes(fmtchunk);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(fmtsize);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(Format);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(NumChannels);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(SampleRate);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(ByteRate);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(BlockAlign);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(BitsPerSample);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = BitConverter.GetBytes(extrabits);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 2);

                DI += 2;

 

                b = Encoding.ASCII.GetBytes(datachunk);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                b = BitConverter.GetBytes(datasize);

                if (!BitConverter.IsLittleEndian)

                    Array.Reverse(b);

                Array.Copy(b, 0, Image, DI, 4);

                DI += 4;

 

                // add the digital 'music' data retrieved earler

                Array.Copy(TrackData[track], 0, Image, DI, TrackData[track].Length);

                // write the binary file - trackx.wav

                i = WriteFile(FileHandle, Image, (uint)Image.Length, out BytesWritten, IntPtr.Zero);

                //if successful then

                // flush all buffers used in the low level write operation

                // then close the file

                if(i!= 0)

                {

                    //Flush the file buffers to force writing of the data.

                    i = FlushFileBuffers(FileHandle);

                    //Close the file.

                    i = CloseHandle(FileHandle);

                }

                // the wave file now exists (created by reading the CD and can be playedby most wav players

                Image = null;

                progressBar1.Value = track;

            }

        }

 

        private void label1_Click(object sender, EventArgs e)

        {

            if (label1.Text == "Done")

                this.Dispose();

        }

    }

}