Playing Audio CDs, part 8 - Simple DAE Playback

Ok, time to get down and dirty in the "CD Playback" series.

Up until now, we've just been reading metadata from the CD.  Now it's time to read the actual audio data and play it back.

First, a bit about playback.  To do the playback, we'll be using the waveOutXxx APIs.  There are a boatload of multimedia APIs available, but the reality is that for a task this simple, the wave APIs are probably the best suited for the work.

There are four wave APIs that we care about here: waveOutOpen, waveOutWrite, waveOutPrepareHeader and waveOutUnprepareHeader.  We won't use waveOutUnprepareHeader in this example because we never free the buffer in question - we always use the same buffer for wave writes.  waveOutOpen opens the wave device for rendering with a specified audio format (in this case, 44.1kHz, stereo, 16 bits/sample), waveOutPrepareHeader sets a buffer up for writing, and waveOutWrite queues the buffer to the internal wave playback queue (all wave buffers are queued when you call waveOutWrite, and are each rendered in turn).

So on with the code.

First off, we've got to add a new class to hold the data read from the CDROM, the CDRomReadData.

struct CDRomReadData {    CDRomReadData(DWORD SectorsPerRead)    {        _CDRomDataLength = SectorsPerRead*CDROM_RAW_BYTES_PER_SECTOR;        _CDRomAudioLength = SectorsPerRead*CDROM_RAW_BYTES_PER_SECTOR;        _CDRomData = new BYTE[_CDRomDataLength];        ZeroMemory(&_RawReadInfo, sizeof(_RawReadInfo));        ZeroMemory(&_WaveHdr, sizeof(_WaveHdr));        _WaveHdr.dwBufferLength = _CDRomDataLength;        _WaveHdr.lpData = (LPSTR)_CDRomData;        _WaveHdr.dwLoops = 0;    }    ~CDRomReadData()    {        delete []_CDRomData;    }    WAVEHDR _WaveHdr;    RAW_READ_INFO _RawReadInfo;    DWORD _CDRomDataLength;    DWORD _CDRomAudioLength;    BYTE * _CDRomData;};The WAVEHDR structure is used to hold state data for the waveOutWrite API.  The RAW_READ_INFO structure is used by the CD ROM driver to hold information about CD reads.

Next, we need a function to open the wave device:

HWAVEOUT CDAESimplePlayer::OpenWaveForCDAudio(HANDLE EventHandle){    WAVEFORMATEX waveFormat;    waveFormat.cbSize = 0;    waveFormat.nChannels = 2;    waveFormat.nSamplesPerSec = 44100;    waveFormat.wBitsPerSample = 16;    waveFormat.nBlockAlign = waveFormat.nChannels * waveFormat.wBitsPerSample;    waveFormat.nAvgBytesPerSec = waveFormat.nSamplesPerSec*waveFormat.nBlockAlign/8;    waveFormat.wFormatTag = WAVE_FORMAT_PCM;    HWAVEOUT waveHandle;    MMRESULT waveResult = waveOutOpen(&waveHandle, WAVE_MAPPER, &waveFormat, (DWORD_PTR)EventHandle, NULL,                                         CALLBACK_EVENT | WAVE_ALLOWSYNC | WAVE_FORMAT_DIRECT);    if (waveResult != MMSYSERR_NOERROR)    {        printf(_T("Failed to open wave device: %d\n"), waveResult);        return NULL;    }    //    // Swallow the "open" event.    //    WaitForSingleObject(EventHandle, INFINITE);    return waveHandle;}

There's at least one "tricky" bit here.  The function takes a pointer to an auto-reset event that's used to signal when the wave operation completes - this gets used later on in the process.  We also hard code the CD audio format - 44,100 samples per second, 16 bits per sample, stereo.  The "tricky" bit comes with the call to WaitForSingleObject - the Wave APIs will set the event to the signalled state whenever there is a "wave message" that occurs.  Since one of the messages (WOM_OPEN) is generated on any wave opens, we have to swallow that event before we return - otherwise the caller would be out of step with the wave driver.

And now, finally, what we've all been waiting for: CD Audio Playback.

HRESULT CDAESimplePlayer::PlayTrack(int TrackNumber)
{
    HRESULT hr;
    HANDLE waveWriteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);
    HWAVEOUT waveHandle = OpenWaveForCDAudio(waveWriteEvent);
    if (waveHandle == NULL)
    {
        return E_FAIL;
    }

    CDRomReadData *readData = new CDRomReadData(DEF_SECTORS_PER_READ);
    for (DWORD i = 0 ; i < (_TrackList[TrackNumber]._TrackLength / DEF_SECTORS_PER_READ); i += 1)
    {
        readData->_RawReadInfo.DiskOffset.QuadPart = ((i * DEF_SECTORS_PER_READ) + _TrackList[TrackNumber]._TrackStartAddress)*
                        CDROM_COOKED_BYTES_PER_SECTOR;
        readData->_RawReadInfo.TrackMode = CDDA;
        readData->_RawReadInfo.SectorCount = DEF_SECTORS_PER_READ;
        hr = CDRomIoctl(IOCTL_CDROM_RAW_READ, &readData->_RawReadInfo, sizeof(readData->_RawReadInfo), readData->_CDRomData,
                                            readData->_CDRomDataLength);
        if (hr != S_OK)
        {
            printf("Failed to read CD Data: %d", hr);
            return hr;
        }
        MMRESULT waveResult;
        readData->_WaveHdr.dwBufferLength = readData->_CDRomAudioLength;
        readData->_WaveHdr.lpData = (LPSTR)readData->_CDRomData;
        readData->_WaveHdr.dwLoops = 0;

        waveResult = waveOutPrepareHeader(waveHandle, &readData->_WaveHdr, sizeof(readData->_WaveHdr));
        if (waveResult != MMSYSERR_NOERROR)
        {
            printf("Failed to prepare wave header: %d", waveResult);
            return HRESULT_FROM_WIN32(waveResult);
        }
        waveResult = waveOutWrite(waveHandle, &readData->_WaveHdr, sizeof(readData->_WaveHdr));
        if (waveResult != MMSYSERR_NOERROR)
        {
            printf("Failed to write wave header: %d", waveResult);
            return HRESULT_FROM_WIN32(waveResult);
        }
        //
        // Wait until the wave write completes.
        //
        WaitForSingleObject(waveWriteEvent, INFINITE);
    }

    return S_OK;
}

Some things to note: First off, the error checking in this code is horrendous.  It leaks memory, and doesn't check for memory allocation failures.  But the loop is extremely straighforward - it simply opens a wave device, then loops for the number of blocks in the track reading each block, and handing it to the wave APIs to play back.  Note the call to WaitForSingleObject at the bottom of the loop, that's waiting on the WOM_DONE message that's generated whenever the wave write completes, we need to ensure that the wave write completes before we read the next block into the buffer.

The code is gross, but it DOES play the data on the CD.  However, you compile the code, you'll notice that it glitches like crazy.  The reason for that is really simple: We're doing everything synchronously, and that means that we don't have any opportunity to overlap the wave writes with the CD reads.  And that stinks.

Tomorrow, we'll start to fix the problem.

Edit: Fixed typo in destructor (it's a nop, but people are complaining...).

Edit2: Fixed waveOutOpen WAVEFORMATEX structure nAvgBytesPerSecond calculation.  The original version was bits/second, not bytes/second.  Thanks Elliot!