Share via


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.