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 accessibleIOCTL_STORAGE_LOAD_MEDIA
: Injects the CD-drive if openedIOCTL_CDROM_EJECT_MEDIA
: Ejects the CD-driveIOCTL_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