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.
Comments
- Anonymous
May 05, 2005
This approach for playing audio seems to be quite different from what is described and recomended in the documentation for the waveOut-API. Are there any issues on using this method compared to using a callback proc or handle? - Anonymous
May 05, 2005
Trygve, do you have specifics? I'll add this to my queue of things to write about (in particular, the differences between the three types of notification).
For this example, I didn't use the event I created in the waveOutOpen (you'll remember I used that in the first DAE example). That's because the event fires when ANY wave write completes - there's no way of scoping the event to a particular write - if I could, I'd have done that.
I could have associated the request with a window message, that would allow me to find out which write completed, but this example is a console app - it doesn't have a message pump.
And I could have used a callback function - but that would have defeated one of the design goals of this function, which is to be single threaded (thus avoiding concurrency issues). You'll notice that all the other examples are also single threaded.
For the purposes of this - Anonymous
May 05, 2005
The comment has been removed - Anonymous
May 05, 2005
Trygve,
I believe that sndrec32 uses mciwave to play back its files. But I may be wrong (I've never worked on that code).
How do you receive your notification? Are you using wave callbacks? If so, check the limitations on the waveProc function - you can do VERY little in one of those callbacks - calling any wave functions is absolutely not allowed. - Anonymous
May 05, 2005
I recieve notification using a windows handle. (I use the flag CALLBACK_WINDOW when calling waveOutOpen.) Are there limitations on what you can do when handling these messages?
I tried checking what functions are imported by sndrec32.exe, and as far as I can see waveOutOpen and waveOutWrite are amongs those. (No mci-functions seem to be imported) - Anonymous
May 05, 2005
mschaef, that's tomorrows post. - Anonymous
May 06, 2005
As I mentioned in my previous post, the code I've provided will play back audio CDs.&nbsp; But it's not... - Anonymous
May 14, 2005
RePost:
http://www.yeyan.cn/Programming/PlayingAudioCDsGlitchFreeLowMemory.aspx - Anonymous
January 08, 2007
As I mentioned in my previous post , the code I've provided will play back audio CDs. But it's not ready