Larry Osterman's WebLog

Confessions of an Old Fogey
Blog - Title

Playing Audio CDs, part 8 - Simple DAE Playback

Playing Audio CDs, part 8 - Simple DAE Playback

  • Comments 11
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!

  • I'm sure you meant to use delete[] instead of delete. The latter will cause Heinous Memory Leaks(c). God Bless Scotty Meyers' books.

    ~CDRomReadData()
    {
    delete [] _CDRomData;
    }
  • Am I really the first to comment on this post?

    :-)

    Larry, oh Larry...

    delete [] _CDRomData;
  • Well, I guess you did qualify your code with a declaration of horrendousness, so shouldn't be complaining too much. I'll stopy complaining/correcting and ask simple questions instead.

    Is it a nop simply because you never delete readData, or is there something else that I'm missing? Is there a reason that you dynamically allocate readData instead of putting it on the stack? That should keep it from leaking memory:

    CDRomReadData readData(DEF_SECTORS_PER_READ);
  • Actually, since the point has been raised, I'm going to chime in with a question that has been bugging me.

    Since the type of _CDRomData is a char array, doesn't it not matter that its delete [] over delete? Since there are no constructors/destructors at work, my experience thus far is the compiler just uses the scalar new/delete. For instance:

    wchar_t* wszString = new wchar_t[SomeLength]; // This actually calls scalar new (sizeof(wchar_t)*SomeLength)

    delete [] wszString; // If you step into in the debugger, this will just be the scalar delete, as there is no reason to use the vector delete.

    Now my understanding is that using scalar instead of vector causes Bad Things, because of not calling the correct destructors. However in primitive types, there really is no reason to not use the scalar version.

    Anyone know the Correct Answer, as opposed to the Empirical Answer?
  • Dan,
    Absolutely - that's why I made the mistake in the first place - arrays of POD don't have the new/delete issue.

    But it's always better to be consistant - because otherwise, when you change to a non POD type, you're going to be messed up something fierce.
  • Ah, so it technically isn't a mistake in logic, it is a mistake in style, and thus won't make Heinous Memory Leaks(c).

    I assume then that is what you meant by calling it a nop.
  • It's quite bloody simple:
    match new with delete
    match new[] with delete[]

    I can't find them ATM, but Raymond has a couple of blog entries about why mismatching the types of new and delete is a Bad Thing.
  • Isn't delete vs []delete an implementation detail? IIRC the C++ standard is that the two are different, but in actual implementation they may well work out to be the same. Thus code compiled without the [] may work on Visual C++, but not on GCC or the Intel C++ compilers for example.
  • Dan> The Correct Answer is (I believe) that delete against new[] is undefined behavior (of course), so that it is plain wrong. The relevant verse (from ISO C++) should be 5.3.5, paragraph 2, ending "... If not, the behavior is undefined. [Note: this means that the syntax of the delete-expression must match the type of the object allocated by new..." Obviously, there is no reason why this shouldn't work just fine, but, you know, undefined is undefined.
  • From the standard:

    5.3.5.2

    In the first alternative (delete object), the value of the operand of delete shall be a pointer to a nonarray object or a pointer to a subobject (1.8) representing a base class of such an object (clause 10). If not, the behavior is undefined. In the second alternative (delete array), the value of the operand of delete shall be the pointer value which resulted from a previous array newexpression. If not, the behavior is undefined.

    Using delete on something allocated with a new [] is just wrong. Just because it works on MSVC doesn't mean it won't break later or break on another compiler. EVIL EVIL EVIL
  • Hi,

    this is pretty cool stuff and the one I've been looking for for quite a while. Thanks!

    But I have one question: What is the definition of CDROM_COOKED_BYTES_PER_SECTOR?

    I couldn't find anything here or in the Internet.

    Best
    Bernhard
Page 1 of 1 (11 items)