Playing Audio CDs, part 10 - Glitch Free, Low Memory

So yesterday I wrote an example that removed the glitching from my DAE CD playback example.

But it had some major drawbacks - for example, it consumed huge amounts of system memory, and had absolutely horrendous latency problems - if you wanted to pause playback, you would have to wait for all 10 minutes worth of queued audio samples had played before the pause would take effect.

Is it possible to rewrite the example to save memory and improve latency?

Of course there is (otherwise why would I be writing this?).  The key is to notice that by the time a block has finished playing, the player has had time to read the next block - you don't need a block for every read, you can instead recycle the read blocks.

And that brings us to the next version of the PlayTrack method.

HRESULT CDAENoWaitLowMemPlayer::PlayTrack(int TrackNumber){    HRESULT hr;    HANDLE waveWriteEvent = CreateEvent(NULL, FALSE, FALSE, NULL);    MMRESULT waveResult;    CDRomReadData *readData = NULL;    HWAVEOUT waveHandle = OpenWaveForCDAudio(waveWriteEvent);    if (waveHandle == NULL)    {        return E_FAIL;    }    TrackNumber -= 1; // Bias the track number by 1 - the track array is )ORIGIN 0.    CAtlList<CDRomReadData *> readDataList;    for (DWORD i = 0 ; i < CDROM_READAHEAD_DEPTH ; i += 1)    {        readData = new CDRomReadData(DEF_SECTORS_PER_READ);        if (readData == NULL)        {            printf("Failed to allocate a block\n");            return E_FAIL;        }        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);        }        readData->_WaveHdr.dwFlags |= WHDR_DONE;        readDataList.AddTail(readData);    }    for (DWORD i = 0 ; i < (_TrackList[TrackNumber]._TrackLength / DEF_SECTORS_PER_READ); i += 1)    {        //        // Get a free block from the read queue. Since WAVE writes complete in order, the queue is sorted by wave write completion status.        // If the head of the queue isn't done, spin waiting until it IS done.         //        while (true)        {            if (!readDataList.IsEmpty() && readDataList.GetHead()->_WaveHdr.dwFlags & WHDR_DONE)            {                readData = readDataList.RemoveHead();                break;            }            else            {                Sleep(10); // Sleep for a bit to release the CPU.            }        };        //        //  Read the data from the disk.        //        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;        }        //        // Write it to the audio device.        //        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);        }        //        // And add this buffer to the end of the read queue.        //        readDataList.AddTail(readData);    }    //    // We're done playing, drain the requests in the queue.    //    while (!readDataList.IsEmpty())    {        if (readDataList.GetHead()->_WaveHdr.dwFlags & WHDR_DONE)        {            CDRomReadData *completedBlock;            completedBlock = readDataList.RemoveHead();            waveOutUnprepareHeader(waveHandle, &completedBlock->_WaveHdr, sizeof(readData->_WaveHdr));            delete completedBlock;        }        else        {            Sleep(100);        }    };    return S_OK;}

This version uses significantly less memory - in fact, it's pretty glitch free with CDROM_READAHEAD_DEPTH set to 2 (I thought I'd need 3 buffers for this example, but two seems to work (but there may be glitches on startup)).  It also improves the latency problem - at no time are more than CDROM_READAHEAD_DEPTH blocks worth of data are queued to the wave writer.  So if you pause playback, the playback will stop quickly.

I've also done a bit of restructuring the code to clarify the relationship between the buffer and the waveOutPrepareHeader/waveOutUnprepareBuffer API.  The actual inner loop simply grabs a buffer from the queue of ready buffers (the readDataList), reads the audio data, calls waveOutWrite on the data and adds the block back to the queue.

I took a small liberty of overusing the WHDR_DONE flag in the code that prepares the loop - I turn the bit on on newly allocated buffers to pretend that they've been played - this makes the loop that pulls the blocks from the queue easier.

I was taken to task in the previous version for not calling waveOutUnprepareBuffer, the commenters were right, even though the waveOutUnprepareBuffer is functionally a NOP on every supported version of Windows, it's more complete to include it in the code.

I do want to stress that this is NOT production code though.  Tomorrow, I'll write a bit about what it would take to change this simple example into something that could be used in a production system.