Playing Audio CDs, part 7 - DAE Table of contents.
So now this series comes to the "fun" part, DAE.
DAE stands for "Digital Audio Extraction", it means reading the raw audio data from the CDROM.
Over the next couple of articles, this will turn into the most complicated code I've ever attempted to drop into the blog, so bear with me - this turns into a bit of a wild ride.
The first thing to know about DAE is that to be able to use it, you need the DDK. The code in this example depends on NTDDCDRM.H, which contains the definitions for the CDROM IOCTLs. One other HUGE caveat: The IOCTLs defined here are subject to change - they're based on preliminary documentation, so YMMV.
So, with that, here's the initialization and table of contents reading logic:
#define CD_BLOCKS_PER_SECOND 75 // A useful constant that's NOT in the DDK
DWORD MSFToBlocks( UCHAR msf[4] )
{
DWORD cBlock;
cBlock =
( msf[1] * ( CD_BLOCKS_PER_SECOND * 60 ) ) +
( msf[2] * CD_BLOCKS_PER_SECOND ) +
msf[3];
return( cBlock - 150);
}
MSFToBlocks converts from a MSF array (4 bytes, representing Hours, Minutes, Seconds and Frames, where a frame is a sample of audio data) into a block count. It assumes that there are always 0 hours in an MSF array (which apparently is true on CD audio tracks). Before anyone asks, I'm not sure where the 150 comes from.
HRESULT CDAESimplePlayer::OpenCDRomDrive(LPCTSTR CDRomDrive)
{
HRESULT hr = S_OK;
_CDRomHandle = CreateFile(CDRomDrive, GENERIC_READ, FILE_SHARE_READ|FILE_SHARE_WRITE, NULL, OPEN_EXISTING,
FILE_FLAG_OVERLAPPED|FILE_ATTRIBUTE_NORMAL, NULL);
if (_CDRomHandle == INVALID_HANDLE_VALUE)
{
hr = HRESULT_FROM_WIN32(GetLastError());
printf("Error %x opening CDROM drive %s", hr, CDRomDrive);
}
return hr;
}
OpenCDRomDrive opens the CDRom drive. Please note that we're opening the file for overlapped access, this is important later on in the series. The format of a CDRom drive string is "\\.\<drive letter>:".
HRESULT CDAESimplePlayer::CDRomIoctl(DWORD IOControlCode,
void *ioctlInputBuffer,
DWORD ioctlInputBufferSize,
void *ioctlOutputBuffer,
DWORD &ioctlOutputBufferSize)
{
HRESULT hr = S_OK;
OVERLAPPED overlapped = {0};
overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
if (overlapped.hEvent == NULL)
{
hr = HRESULT_FROM_WIN32(GetLastError());
printf("Error %d getting event handle\n", hr);
goto Exit;
}
if (!DeviceIoControl(_CDRomHandle, IOControlCode, ioctlInputBuffer, ioctlInputBufferSize, ioctlOutputBuffer, ioctlOutputBufferSize,
&ioctlOutputBufferSize, &overlapped))
{
hr = HRESULT_FROM_WIN32(GetLastError());
if (hr == HRESULT_FROM_WIN32(ERROR_IO_PENDING))
{
if (!GetOverlappedResult(_CDRomHandle, &overlapped, &ioctlOutputBufferSize, TRUE))
{
hr = HRESULT_FROM_WIN32(GetLastError());
printf("Error %d waiting for CDROM IOCTL\n", hr);
}
else
{
hr = S_OK;
}
}
else
{
printf("Error %d IOCTLING CDROM\n", hr);
}
}
Exit:
if (overlapped.hEvent)
{
CloseHandle(overlapped.hEvent);
}
return hr;
}
Please note the use of a static overlapped to turn the asynchronous IOCTL to a synchronous IOCTL. Otherwise this is just a wrapper around the DeviceIOControl API.
HRESULT CDAESimplePlayer::Initialize()
{
DWORD driveMap = GetLogicalDrives();
int i;
CString DriveName;
for (i = 0 ; i < 32 ; i += 1)
{
if (driveMap & 1 << i)
{
DriveName.Format(_T("%c:"), 'A' + i);
if (GetDriveType(DriveName) == DRIVE_CDROM)
{
break;
}
}
}
_CDRomDriveName= CString(_T("\\\\.\\")) + DriveName;
return S_OK;
}
Initialize is easy - just walk through the drives until you hit one that's a CD ROM drive, then remember the drive letter.
HRESULT CDAESimplePlayer::DumpTrackList()
{
HRESULT hr;
hr = OpenCDRomDrive(_CDRomDriveName);
if (hr != S_OK)
{
printf("Failed to open CDRom Drive %s: %x\n", _CDRomDriveName, hr);
goto Exit;
}
CDROM_TOC tableOfContents;
DWORD tocSize = sizeof(tableOfContents);
hr = CDRomIoctl(IOCTL_CDROM_READ_TOC, NULL, 0, (void *)&tableOfContents, tocSize);
if (hr != S_OK)
{
printf("Failed to read CDRom Table of contents: %x\n", _CDRomDriveName, hr);
goto Exit;
}
for (int i = tableOfContents.FirstTrack - 1 ; i < tableOfContents.LastTrack ; i += 1)
{
CString trackName;
DWORD trackLengthInBlocks = MSFToBlocks(tableOfContents.TrackData[i+1].Address) - MSFToBlocks(tableOfContents.TrackData[i].Address);
DWORD trackLengthInSeconds = trackLengthInBlocks / CD_BLOCKS_PER_SECOND;
DWORD trackLengthInMinutes = trackLengthInSeconds / 60;
DWORD trackLengthInHours = trackLengthInMinutes / 60;
DWORD trackLengthFrames = trackLengthInBlocks % CD_BLOCKS_PER_SECOND;
DWORD trackLengthMinutes = trackLengthInMinutes - trackLengthInHours*60;
DWORD trackLengthSeconds = trackLengthInSeconds - trackLengthMinutes*60;
trackName.Format(_T("Track %d, Starts at %02d:%02d:%02d:%02d, Length: %02d:%02d:%02d:%02d"), tableOfContents.TrackData[i].TrackNumber,
tableOfContents.TrackData[i].Address[0],
tableOfContents.TrackData[i].Address[1],
tableOfContents.TrackData[i].Address[2],
tableOfContents.TrackData[i].Address[3],
trackLengthInHours,
trackLengthMinutes,
trackLengthSeconds,
trackLengthFrames
);
printf("%s\n", trackName);
CDRomTrack track;
track._TrackStartAddress = MSFToBlocks(tableOfContents.TrackData[i].Address);
track._TrackNumber = tableOfContents.TrackData[i].TrackNumber;
track._TrackControl = tableOfContents.TrackData[i].Control;
track._TrackLength = MSFToBlocks(tableOfContents.TrackData[i+1].Address) - track._TrackStartAddress;
this->_TrackList.Add(track);
}
Exit:
return hr;
}
Ok, now the meat of the code - we issue the IOCTL_CDROM_READ_TOC IOCTL to the drive and retrieve a table of contents structure.
The TOC contains the basic information about the drive - the track number of the first and last track, plus an array of TRACK_DATA structures. The TRACK_DATA structure's where all the fun is. We only really care about the start address of each track - the rest isn't significant for this example.
One thing to note is that there's an implicit array overrun - while the TOC runs from the first track to the last track, the track data actually runs to one more than the last track - that's to allow the length calculation of the last track to work correctly.
The calculation of the track length is more tortuous than it needs to be, I was trying to set it up so that someone stepping through it in the debugger (me :)) would be able to see what was going on.
Tomorrow, I'll start with the playback logic. Today was easy, tomorrow, it starts getting complicated.
Comments
- Anonymous
May 02, 2005
Red Book requires a 150 frame pregap from the start of the CD. These are given negative indices and the first usable location is numbered 0. - Anonymous
May 02, 2005
Hmm,
Frustrating that the DDK is by mail-order-on-a-CD-only basis, and at a somewhat steep price, too.
Why not treat it as the platform SDK is, and make it available for free download via the web? - Anonymous
May 02, 2005
Thanks Nicholas, I knew there was a reason but hadn't seen it documented.
The DDK is included in the price for MSDN subscribers, it's just if you want it without MSDN that you have to pay. I can't explain why this is, I'm not on that team :) - Anonymous
May 02, 2005
But aren't administrative privileges necessary to open the device itself? I was playing around with this about a year ago and remember having read something like that then. Maybe I misunderstood the docs, but I thought that CreateFile would file for plain users... - Anonymous
May 02, 2005
Michael,
Apparently not for this purpose. I run as a non admin and I've not had any issues doing raw reads of the drive. - Anonymous
May 02, 2005
It didn't make sense to me then - I was just to lazy to try it out. At that time I didn't run as a non-admin user. - Anonymous
May 02, 2005
"The start of MSF addressing is biased 150 frames (2 seconds) into the cd. Thus an msf of 0:0:0 is an lba address of frame 149."
http://www.linux-sxs.org/bedtime/cdapi.html
- Aaron - Anonymous
May 02, 2005
"MSF does not limit minutes to 0-59?"
Apparently not. I don't have a CD with a track longer than one hour available unfortunately (I took all my audio CDs home except for one of them). - Anonymous
May 02, 2005
How come hours will always be zero? I have tracks that are longer than an hour, MSF does not limit minutes to 0-59? I.e. block 333,333 is at 0:74:2:33 instead of 1:14:2:33? - Anonymous
May 02, 2005
+1 to Mike's comment about the DDK. I'm a student and this is the 3rd time in as many days that I need the DDK for something and I'm not able to get my hands on it. - Anonymous
May 03, 2005
It's not much, but if you're a student, the academic version of the MSDN Operating Systems library may not be that expensive (I can't find direct pricing on it). - Anonymous
May 03, 2005
The comment has been removed - Anonymous
July 28, 2005
I've been searching the web for an answer, and came across your articles. (Which unfortunately don't give me the answer I need either, so I thought I'd ask.)
Is there any way to query the lengths of the tracks? I know you're subtracting one tracks offset from the next to get the length, but in the case of at least some audio CD's with DATA sessions, the data session starts way after the end of the last audio track, so the last audio track looks like its quite a bit longer then it actually is.
I know its possible to do this, as iTunes does it, but I'm not sure how. - Anonymous
July 28, 2005
John, I'm not sure. I don't have any mixed content (and didn't have mixed content) to test.
I'm not sure how this works..