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:
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:
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.
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.
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.
Many of you will know the usage of CreateFile, so here's just a short line of code on how to create the handle.
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).
Be aware: DeviceIoControl works well with Win2000/XP. To use it with Win95/98/Me click here.
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:
CDTRACK Track; char* pBuf = new char [Track.Length*2352];
RAW_READ_INFO ReadInfo;
ReadInfo.TrackMode = CDDA; ReadInfo.SectorCount = 20;
for ( ULONG i=0; i<Track.Length/20; i++ )
{
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,
20*2352,
&Dummy, NULL ) )
{
delete [] pBuf;
return FALSE;
}
}
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:
#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) );
}
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