Search code examples
c#wavcda

How to verify that a cda file on Audio CD is the result of a wav file in C#


I have written a code segment to burn wav files into an Audio CD. It works fine, however, at the last step when performing verification, it fails on some DVD drives. The files are actually burnt into CD and they can be played with no issues. But the verification seems to be failing for no reason. I can turn off verification. However, I prefer to write another function that manually checks the burnt files and verifies that they are the actual result of wav files. I was able to do that for burning data CD. But for Audio CD, as it converts them into cda files on disc, I can't compare them. Any suggestion on how to verify them using C#? Basically let's assume that I have an Audio CD with a few .cda files in it, and I want to make sure they are the actual converted files from the original wav files. I do know that cda files are just placeholders, I just don't know how to get the wav files (if it is possible) out of them to compare with the original wav files.


Solution

  • Converting a cda file to wav

    It's not that easy converting a cda to wav file.

    You have to use some unmanaged memory and pointer to read data from the cd.


    ReadCD method:

    • The readcd procedure verifies that the drive is a cdrom drive

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

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

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

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

    • 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.

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

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

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

    • 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.

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

    • Now we iterate through all sectors in this track.

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

    • 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.)

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

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

    • Repeat for all sectors in the track.

    • Repeat for all tracks on the CD.

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

       // 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();
      
      }  
      

    ConvertToWav method:

    • Initialize the four ChunkIds that are required for a .wav header.
    • 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.

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

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

    • Build the Header.

    • Add the "Music" data.
    • Write the file using WriteFile from Kernel32.

    • Proceed if that was successful.

    • We flush all buffers used in WriteFile.

    • And we close the file using CloseHandle.

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

    • And now we go play the wav files and gloat over how cooked we are

          // 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;
      
          }
      
      }
      

    File compare

    This method has a validation before all bytes are checked for equality.

    • When the length of the two files are not the same it is not possible they are the same file and false will be returned
    • After that the method compares all bytes and returns the success if all of them equals

      private bool FileCompare(string file1, string file2)
      {
       int file1byte;
       int file2byte;
       FileStream fs1;
       FileStream fs2;
      
       // Open the two files.
       fs1 = new FileStream(file1, FileMode.Open);
       fs2 = new FileStream(file2, FileMode.Open);
      
       // Check the file sizes. If they are not the same, the files
          // are not the same.
       if (fs1.Length != fs2.Length)
       {
            // Close the file
            fs1.Close();
            fs2.Close();
      
            // Return false to indicate files are different
            return false;
       }
      
       // Read and compare a byte from each file until either a
       // non-matching set of bytes is found or until the end of
       // file1 is reached.
       do
       {
            // Read one byte from each file.
            file1byte = fs1.ReadByte();
            file2byte = fs2.ReadByte();
       }
       while ((file1byte == file2byte) && (file1byte != -1));
      
       // Close the files.
       fs1.Close();
       fs2.Close();
      
       // Return the success of the comparison. "file1byte" is
       // equal to "file2byte" at this point only if the files are
          // the same.
       return ((file1byte - file2byte) == 0);
       }
      

    Links: