Rip Audio CD Programmatically

By | April 8, 2021

More than 10 years ago I started XEdit project which is binary editor for files and disks contents. XEdit is available there. I am still using it sometime, however I do not support the project since 2011 mainly because this MFC application was written on Visual C++ 6 and I have no time to do migration to MS VS 20xx. The edit has a lot of useful tools such as search for binary sequences in files and disk sectors (to get access to disk level XEdit should be started as administrator).
XEdit also has audio CD ripping tool which I want to show how it works from inside. There are a lot of free and commercial software which does the same and ripping by it self is very simple and consists of 3 following steps:

  1. Find binary sequence which corresponds to appropriate sound track (song)
  2. Read this binary sequence
  3. Save them to sound file

The source code from XEdit which is doing all 3 steps may be downloaded using link below:


Ripping source code
In the first step you need to read CD TOC (table of content) which shows location of all sound tracks on the CD, for example this one:
Audio CD
The raw view TOC from this CD is:
CD TOC Raw Data
Then the raw TOC data are casted to _CDROM_TOC structure:


typedef struct _TRACK_DATA {
  UCHAR nbsp;    Reserved;
  UCHAR nbsp;    Control : 4;
  UCHAR nbsp;    Adr : 4;
  UCHAR nbsp;    TrackNumber;
  UCHAR nbsp;    Reserved1;
  UCHAR nbsp;    Address[4];
} TRACK_DATA, *PTRACK_DATA;

typedef struct _CDROM_TOC {
  UCHAR nbsp;    Length[2];
  UCHAR nbsp;    FirstTrack;
  UCHAR nbsp;    LastTrack;
  TRACK_DATA TrackData[MAXIMUM_NUMBER_TRACKS];
} CDROM_TOC, *PCDROM_TOC;

The first 2 bytes is length of tracks data, then one byte is the first track number and next one is the last track number. The total number of tracks is 13 the same as song number of this CD.
Now the code which reads CD TOC:


void RippingCDDlg::OnRead()
{
  CString csBuffer;
  DWORD dwLength;
  m_nResult = -1;
  CDROM_DISK_DATA  cdd;
  DWORD dwData;
  m_pExeditView->m_nDiskType = -1;
  //read it again, may floppy was reamoved or changed
  if(GetDriveGeometry((LPCTSTR)m_pExeditView->m_csDefaultCD,&m_dg))
  {
    UpdateDialog();
    csBuffer = "\\\\.\\";
    csBuffer += m_pExeditView->m_csDefaultCD;
    //read from disc
    m_hDrive = CreateFile((LPCTSTR)csBuffer, GENERIC_READ,
      FILE_SHARE_READ | FILE_SHARE_WRITE,
      NULL, OPEN_EXISTING,
      /*FILE_FLAG_RANDOM_ACCESS,*/
      FILE_FLAG_NO_BUFFERING,
      NULL);
    if(m_hDrive != INVALID_HANDLE_VALUE)
    {
      if(DeviceIoControl(m_hDrive, IOCTL_CDROM_DISK_TYPE, NULL, 0, &cdd, sizeof(cdd), &dwData, 0))
      {
        switch (cdd.DiskData & 0x03)
        {
        case CDROM_DISK_DATA_TRACK:
          m_pExeditView->m_nDiskType = CDROM_DISK_DATA_TRACK;
          csBuffer = "Data";
          break;
        case CDROM_DISK_AUDIO_TRACK:
          m_pExeditView->m_nDiskType = CDROM_DISK_AUDIO_TRACK;
          csBuffer = "Audio";
          break;
        case CDROM_DISK_DATA_TRACK|CDROM_DISK_AUDIO_TRACK:
          m_pExeditView->m_nDiskType = CDROM_DISK_DATA_TRACK|CDROM_DISK_AUDIO_TRACK;
          csBuffer = "Audio + Data";
        default:
          m_pExeditView->m_nDiskType = 0;
          csBuffer = "Unknow type";
          break;
        }
      }
      if (DeviceIoControl(m_hDrive, IOCTL_CDROM_READ_TOC, NULL, 0, &m_pExeditView->m_toc,
        sizeof(m_pExeditView->m_toc), &dwData, 0))
      {
        if(dwData!=0)
        {
          m_nResult = IDOK;
          m_pExeditView->m_int64Length = dwData;
          if(*m_ppbyFileBuffer)
          {
            //    delete[] m_pbyFileBuffer;
            free(*m_ppbyFileBuffer);
            *m_ppbyFileBuffer = NULL;
          }
          *m_ppbyFileBuffer = new BYTE[dwData];
          memcpy(*m_ppbyFileBuffer,(const void*)&m_pExeditView->m_toc,dwData);
          int nAttribLen = dwData/4;
          if(dwData % 4) nAttribLen++;
          if(*m_ppbyFileBufferAttribute)
          {
            free(*m_ppbyFileBufferAttribute);
            *m_ppbyFileBufferAttribute = NULL;
          }
          *m_ppbyFileBufferAttribute = (BYTE*)malloc(nAttribLen);
          if(*m_ppbyFileBufferAttribute) memset(*m_ppbyFileBufferAttribute,0,nAttribLen);
          m_pExeditView->m_csFileName.Format("Raw TOC of %s (%s)",            (LPCTSTR)m_pExeditView->m_csDefaultCD,
            (LPCTSTR)csBuffer);
        } else {
          m_nResult = -1;
        }
      }
      CloseHandle(m_hDrive);
    }
  }
  if(m_nResult != IDOK)
  {
    csBuffer.Format("Cannot read Table of Contents of %s device or no such device",
      m_pExeditView->m_csDefaultCD);
    AfxMessageBox((LPCTSTR)csBuffer);
    m_nResult = -1;
  }
  EndDialog(m_nResult);
}

Raw data may be converted to user friendly view, see TOCDialog::OnInitDialog function using “Ripping source code” link:
TOC Dialog
Now reading sound track data per song:


int TOCDialog::ReadCDTrack(BYTE ** ppBuffer, BOOL bFineReading)
{
  DWORD dwStartFrame;
  DWORD dwNumOfFrame;
  DWORD dwBytesRdWr;
  RAW_READ_INFO rawRead;
  int nIndex = 0;
  int nPos = 0;
  BOOL bRet;
  DISK_GEOMETRY dg;
  BOOL bDataCD;
  int int64Len = 0;
  CDROM_DISK_DATA  cdd;
  DWORD dwData;
  DWORD dwRetVal = (DWORD)-1;
  LARGE_INTEGER n64Pos;
  DWORD dwLength;
  CString csBuffer;
  CListCtrl * pListCtrl = (CListCtrl*)GetDlgItem(IDC_LIST_TOC);

  HCURSOR hWaitCursor = LoadCursor( NULL, IDC_WAIT);
  HCURSOR hOldCursor = SetCursor( hWaitCursor);
  BYTE * pSelectedBuffer;
  CProgressCtrl * pProgressCtrl = (CProgressCtrl*)GetDlgItem(IDC_PROGRESS_TOC);
  pProgressCtrl->ShowWindow(SW_NORMAL);
  GetDriveGeometry((LPCTSTR)m_csDrive,&dg);
  m_hDrive = CreateFile((LPCTSTR)m_csDrive,
    GENERIC_READ,FILE_SHARE_READ,NULL,
    OPEN_EXISTING,NULL,NULL);
  if(m_hDrive == INVALID_HANDLE_VALUE)
  {
    csBuffer.Format("No read access to device (%s)",
      m_pExeditView->m_csDefaultCD);
    AfxMessageBox((LPCTSTR)csBuffer);
  } else {
    pListCtrl->EnableWindow(FALSE);
    dwStartFrame = (m_pExeditView->m_toc.TrackData[m_nSelectedItem].Address[0]*3600+ //hours
      m_pExeditView->m_toc.TrackData[m_nSelectedItem].Address[1]*60 + //minutes
      m_pExeditView->m_toc.TrackData[m_nSelectedItem].Address[2]/*seconds*/-2)*CD_FRAMES_PER_SECOND +
      m_pExeditView->m_toc.TrackData[m_nSelectedItem].Address[3];
    dwNumOfFrame = pListCtrl->GetItemData(m_nSelectedItem);
    if(*ppBuffer!=NULL)
    {
      if(m_pExeditView->m_pbyFileBuffer)
      {
        //    delete[] m_pbyFileBuffer;
        free(m_pExeditView->m_pbyFileBuffer);
        m_pExeditView->m_pbyFileBuffer = NULL;
      }
      m_pExeditView->m_pbyFileBuffer = new BYTE[dwNumOfFrame*CDROM_SECTOR_SIZE];
      pSelectedBuffer = m_pExeditView->m_pbyFileBuffer;
    } else {
      *ppBuffer = new BYTE[dwNumOfFrame*CDROM_SECTOR_SIZE];
      pSelectedBuffer = *ppBuffer;
    }
    int64Len = dwNumOfFrame*CDROM_SECTOR_SIZE;

    pProgressCtrl->SetRange(0, dwNumOfFrame);
    while(dwNumOfFrame)
    {
      pProgressCtrl->SetPos(nPos);
      rawRead.TrackMode = CDDA;
      rawRead.DiskOffset.QuadPart = dwStartFrame*CDROM_DATA_TO_SEC;
      if(bFineReading)
      {
        rawRead.SectorCount = 1;
      } else {
        rawRead.SectorCount = (rawRead.DiskOffset.QuadPart % (dg.SectorsPerTrack * CDROM_DATA_TO_SEC));
        rawRead.SectorCount = dg.SectorsPerTrack – rawRead.SectorCount/CDROM_DATA_TO_SEC;
        if(rawRead.SectorCount>dwNumOfFrame)
        {
          rawRead.SectorCount = dwNumOfFrame;
        }
      }
      bRet = DeviceIoControl(m_hDrive, IOCTL_CDROM_RAW_READ,
        &rawRead, sizeof(RAW_READ_INFO),
        &pSelectedBuffer[nIndex],
        CDROM_SECTOR_SIZE * rawRead.SectorCount,
        &dwBytesRdWr, (LPOVERLAPPED) NULL);
      if(bFineReading)
      {
        nIndex += CDROM_SECTOR_SIZE;
        dwStartFrame += 1;
        dwNumOfFrame-=1;
        nPos += 1;
      } else {
        nIndex += (rawRead.SectorCount * CDROM_SECTOR_SIZE);
        dwStartFrame += rawRead.SectorCount;
        dwNumOfFrame-=rawRead.SectorCount;
        nPos += rawRead.SectorCount;
      }
      dwRetVal &= (DWORD)bRet;
    }
    CloseHandle(m_hDrive);
    m_hDrive = INVALID_HANDLE_VALUE;
    if(dwRetVal == 0)
    {
      csBuffer.Format("Reading error(s) from %s",
        m_pExeditView->m_csDefaultCD);
      AfxMessageBox((LPCTSTR)csBuffer);
    }
    pListCtrl->EnableWindow(TRUE);
  }
  SetCursor( hOldCursor);
  pProgressCtrl->ShowWindow(SW_HIDE);
  return int64Len;
}

When track data have been read they are save to wav file as is after WaveFileHeader info:


int nFileHandler = _open((LPCTSTR)csFileName, _O_WRONLY | _O_BINARY | _O_TRUNC);
if( nFileHandler != -1)
{
  memcpy(WaveFileHeader.szRIFF,”RIFF”,4);
  memcpy(WaveFileHeader.szWAVE, “WAVE”, 4);
  memcpy(WaveFileHeader.szfmt,”fmt “,4);
  WaveFileHeader.dwLenOfChunk = 0x10;
  WaveFileHeader.wCode = 1;
  WaveFileHeader.wChannelNumbers = 0x02; //Stereo
  WaveFileHeader.dwSampleRate = 44100;
  WaveFileHeader.wBytesPerSample = 4;
  WaveFileHeader.dwBytesPerSecond = WaveFileHeader.dwSampleRate*
  WaveFileHeader.wBytesPerSample;
  WaveFileHeader.wBitsPerSample = 0x10;
  WaveFileHeader.dwFileLengthMinus8 = sizeof(WaveFileHeader) + int64Len – 8;
  WaveFileHeader.dwLengthOfSoundDataToFollow = int64Len;
  _write(nFileHandler, (void*)&WaveFileHeader, sizeof(WaveFileHeader));
  _write(nFileHandler, (void*)pbyBuffer, int64Len);
  _close(nFileHandler);
  delete[] pbyBuffer;
} else {
  csBuffer.Format(“File %s cannot be saved!”,(LPCTSTR)csFileName);
  AfxMessageBox(csBuffer);
}

Leave a Reply

Your email address will not be published. Required fields are marked *