Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Mobile / WinMobile

Tutorial on reading Audio-CDs

4.79/5 (34 votes)
9 Oct 2006CPOL7 min read 1   1.9K  
This easy-to-get tutorial explains in detail everything you need to know about audio-CDs and how to rip the tracks.

Introduction

I really got mad trying to find tutorials about audio-CDs. I found a few with some documentation about the structure of an audio-CD, but nothing that I could use for my "Save-Down-Audio-Tracks-To-File"-class. So, as you see, at last I got it... On my way up to my code I basically relied on two articles. One of them, written by Idael Cardoso and published here at CodeProject, is unfortunately written in C# and the technique of audio-CD-ripping isn't explained at all, as well as the code is not commented that well... The other article, in fact a series of articles by Larry Osterman, didn't make it that easy to rebuild code. So I wrote my class with the basic functionality (and some of it stolen from Idael and Larry). Regarding the fact that I did not find any reliable C++ source about ripping audio-CDs for hours of searching, I decided to publish my class with some explaining around.

Audio Formats

In the whole article I talk about uncompressed wave-audio in the PCM-Format. So don't ask me anything about MP3s or the like. Wave-data has some attributes. The attributes which decide about a wave-data's appearance are:

  • Bits per sample:The bits per sample determine the accuracy and bandwidth of frequencies which a sound contains. Usual values are 8 bit and 16 bit. As you may suppose, a single 16 bit-frequency needs a 16-bit-word-variable to be contained.
  • Count of Channels: Specifies the count of channels the wave-data uses. Usual values are 1 (mono) and 2 (stereo). Wave-data in stereo needs twice the size as mono-data. The frequencies in stereo-waves are stored in blocks like [Frequ Left Channel] [Frequ Right Channel] [Frequ Left Channel] ...
  • Sampling-Rate: A single block contains the frequency-data of all channels for one single moment. So a block of a wave-sample using stereo 16 bit, would be 4 bytes of size (ChannelCount*(BitsPerSample/8)). The sampling-rate determines how many blocks are used to display 1 second. Usual values within waves are 44100Hz, 22050Hz, 11025Hz and 8000Hz.

With these three attributes given (bits/second, channels, sampling-rate) you are able to compute the needed size for wave-audio. As an example, the audio-CD format always is 44100Hz, 16 bit, stereo. For one second of audio 44100*2*2 bytes are required. If you need 176400 byte for one second, you'll need 846720000 bytes (807MB) for 80 minutes, which is the maximum size of data fitting on an audio-CD.

So it's no wonder the MP3-format became such popular! An audio-sample with CD-quality which is 4 minutes long needs about 40 MB on your hard disk. The MP3-file takes about 4 MB with almost equivalent quality!
BTW: The expression "frequeny" is not the real meaning of the audio-data. It represents something close to it, but I think it's OK to imagine it in that way.

About audio-CDs

The data stored on CDs is determined in sectors. A "normal" CD-sector takes 2048 bytes (2KB) of size. Something special about audio-CDs is, that their audio-data is stored in sectors of 2352 bytes of size. That is because one sector should store 1/75 of one second of audio-data. One second needs 176400 bytes, so 1/75 needs 2352 bytes.

Each audio-CD contains a table of contents (TOC). It holds information about the track-count and the address of every track on CD. Usually Windows loads the TOC when you insert the CD and is updated on CD-change. You will retrieve the TOC by a single call to the CD-ROM drive. Because Windows holds the TOC, the CD-drive is not spinned up to get the data, you'll get it directly from your OS. Here you see it's structure:

C++
typedef struct _TRACK_DATA
{
    UCHAR Reserved;
    UCHAR Control : 4;
    UCHAR Adr : 4;
    UCHAR TrackNumber;
    UCHAR Reserved1;
    UCHAR Address[4];
} TRACK_DATA;

typedef struct _CDROM_TOC
{
    UCHAR Length[2];
    UCHAR FirstTrack;
    UCHAR LastTrack;
    TRACK_DATA TrackData[100];
} CDROM_TOC;

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.
To specify the size in sectors of the wave-track, use the following function:

C++
ULONG AddressToSectors( UCHAR Addr[4] )
{
    ULONG Sectors = Addr[1]*75*60 + Addr[2]*75 + Addr[3];
    return Sectors - 150;
}

As you may have noticed, the hours-value of the address is not used. I can't see any sense in it, but if a CD-track exceeds 60 minutes, the hours-value stays unused and the minutes exceed the 60-mark. A value of 150 is subtracted because, as I said, the first accessible address is 2 seconds (150 frames) behind the CD-start.

To read out the track-data we need to have the address and the length of a track, both in sectors.

For my class I chose quite a tiny structure to hold the track-info.

C++
struct CDTRACK
{
    ULONG Address;
    ULONG Length;
};

To calculate the address, just pass the TRACK_DATA::Address-value to AddressToSectors. To calculate the length, subtract the sector's address from the next track's sector-address.

C++
CDROM_TOC Toc;
CDTRACK SmallData;

SmallData.Address = AddressToSectors( Toc.TrackData[x].Address );
SmallData.Length = AddressToSectors( Toc.TrackData[x+1].Address )
                   - SmallData.Address;

Accessing the disc-drive

Once you know, the access to the disc-drive is really simple. You create a handle using CreateFile, communicate with the CD-drive using DeviceIoControl and close that handle via CloseHandle.

Using "CreateFile"

Many of you will know the usage of CreateFile, so here's just a short line of code on how to create the handle.

C++
char Fn[8] = { '\\', '\\', '.', '\\', Drive, ':', '\0' };
HANDLE hCD = CreateFile( Fn, GENERIC_READ, FILE_SHARE_READ,
                         NULL, OPEN_EXISTING, 0, NULL );

Note the path-parameter for CreateFile. It must have the form \\.\F: (in case F is your CD-drive).

Using "DeviceIoControl"

Be aware: DeviceIoControl works well with Win2000/XP. To use it with Win95/98/Me click here.

C++
BOOL DeviceIoControl(
HANDLE hDevice,
DWORD dwIoControlCode,
LPVOID lpInBuffer,
DWORD nInBufferSize,
LPVOID lpOutBuffer,
DWORD nOutBufferSize,
LPDWORD lpBytesReturned,
LPOVERLAPPED lpOverlapped);

hDevice takes our handle to the CD. dwIoControlCode gets one of the IOCTL_... messages and the next parameters specify the input, output and their size. Additionally, there's a dummy-parameter (lpBytesReturned) to which we will always pass some ULONG. We won't use the lpOverlapped-param, so set it to NULL.
There are several IOCTL-messages we are interested in:

  • IOCTL_CDROM_READ_TOC: Reads out the TOC as described above. Set both input-parameters to 0, output-parameters to CDROM_TOC* and sizeof(CDROM_TOC)
  • IOCTL_CDROM_RAW_READ: Reads raw data from the CD-drive. You have to pass a RAW_READ_INFO* and sizeof(RAW_READ_INFO) as input and a valid buffer-pointer with the bytes it can contain as output.
    At this point my code failed for a long time. In RAW_READ_INFO you specify from which sectors and how many sectors you want to read. Just specifying the song's sector-data (e.g. address=4492, length=16110 sectors) in RAW_READ_INFO, the call to DeviceIoControl will fail with GetLastError set to 87, ERROR_INVALID_PARAMETER.
    There is a maximum of sectors to be read at once! I did not find the maximum-number and perhaps it's drive-dependant. But be sure a value <= 1000 should work and a value around 20 is really safe.

Here an example, how to use IOCTL_CDROM_RAW_READ. Regarding the fact that we are not allowed to read a wave-track at once, we need to read it out piece for piece. It's an excerpt from the code:

C++
CDTRACK Track; // Filled with valid info
char* pBuf = new char [Track.Length*2352];

RAW_READ_INFO ReadInfo;
ReadInfo.TrackMode = CDDA; // Always use CDDA (numerical: 2)
ReadInfo.SectorCount = 20; // We'll read 20 sectors with each operation.

// Read the track-data in a loop. Read 20*2352 bytes per pass.
for ( ULONG i=0; i<Track.Length/20; i++ )
{
    // Calculate the new offset from where to read.
    ReadInfo.DiskOffset.QuadPart = (Track.Address + i*20) * 2048;

    // Call DeviceIoControl and read the audio-data to out buffer.
    ULONG Dummy;
    if ( 0 == DeviceIoControl( hCD, IOCTL_CDROM_RAW_READ,
                               &ReadInfo, sizeof(ReadInfo),
                               pBuf+i*20*2352,
                               20*2352,
                               &Dummy, NULL ) )
    {
        delete [] pBuf;
        return FALSE;
    }
}

// Read the remaining sectors.
ReadInfo.SectorCount = Track.Length % 20;
ReadInfo.DiskOffset.QuadPart = (Track.Address + i*20) * 2048;
ULONG Dummy;
if ( 0 == DeviceIoControl( hCD, IOCTL_CDROM_RAW_READ,
                           &ReadInfo, sizeof(ReadInfo),
                           pBuf+i*20*2352,
                           ReadInfo.SectorCount*2352,
                           &Dummy, NULL ) )
{
    delete [] pBuf;
    return FALSE;
}

delete [] pBuf;

Looks quite simple, huh? The only thing I tumble over is the number 2048. This is the only thing about the whole CD-ripping that I really do not understand! It would make sense if you'd replace the number 2048 with 2352! It's pretty weird... But that's the only way it works and was a huge hurdle (if you do not have great tutorials).

The rest of the code should be self-explanatory. At first, the data is read in a loop, 20 sectors (20*2352 bytes) per pass. Then the remaining sectors are read. During the read-process, the correct cd-offset and buf-offset are calculated and the audio-data is stored to that computed buffer-offset.

Additional, there are some more IOCTL_...-messages of interest:

  • IOCTL_STORAGE_CHECK_VERIFY: Checks whether your CD-drive is accessible
  • IOCTL_STORAGE_LOAD_MEDIA: Injects the CD-drive if opened
  • IOCTL_CDROM_EJECT_MEDIA: Ejects the CD-drive
  • IOCTL_CDROM_GET_CONFIGURATION: Retrieves the type of disk (CD-ROM/CD-R/CD-RW/DVD-ROM/DVD-R/...)
  • IOCTL_CDROM_PLAY_AUDIO_MSF, IOCTL_CDROM_PAUSE_AUDIO, IOCTL_CDROM_RESUME_AUDIO, IOCTL_CDROM_STOP_AUDIO: Plays audio data. Pretty simple to control!

Using the code

The class CAudioCD was written to extract audio-tracks from a CD onto your hard-disc. That's the reason why the class is not able to do much more than that. It is able to:

  • Get some info about the count of tracks and each track's play length
  • Read tracks into memory
  • Read tracks from CD directly to a wave-file
  • Do the reading in an extra-thread
  • Inform you about the current progress (callback)
  • Inject & eject the CD, my favourite :D

Using the code should be really simple. The main class is CAudioCD. Here's an example on how to use the class:

C++
#include "CAudioCD.h"
#include <stdio.h>

#define MY_CDROM_DRIVE 'F'

void OnAudioCDProgress( ULONG Track, ULONG Percentage, VOID* Param )
{
    printf( "Ripping track nr. %i\n", Track );
    printf( ": Progress at %i%%\n", Percentage );
}

int main( ... )
{
    CAudioCD AudioCD;
    if ( ! AudioCD.Open( MY_CDROM_DRIVE ) )
    {
        printf( "Cannot open cd-drive!\n" );
        return 0;
    }

    ULONG TrackCount = AudioCD.GetTrackCount();
    printf( "Track-Count: %i\n", TrackCount );

    for ( ULONG i=0; i<TrackCount; i++ )
    {
        ULONG Time = AudioCD.GetTrackTime( i );
        printf( "Track %i: %i:%.2i;  %i bytes of size\n", i+1,
                Time/60, Time%60, AudioCD.GetTrackSize(i) );
    }

    // Prepare param for reading...
    AUDIOCD_READTRACK ReadInfo;
    ReadInfo.Track = 7;
    ReadInfo.SaveToFile = "C:\\Song.wav" );
    ReadInfo.ProgressCb = OnAudioCDProgress;

    if ( ! AudioCD.ReadTrack( &ReadInfo ) )
        printf( "Cannot start reading track: %i\n", GetLastError() );

    return 0;
}

I won't explain anything about this code, read it and you will understand.

Finally...

I hope the article was interesting for you and I hope you'll forgive my bad English, I'm a native German and did write my last English essay in school 2 years ago. So, contact me for any grammatical or spelling mistakes or if I wrote something totally wrong about some audio-stuff.

Greetings, Michel

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)