In the previous posting for this series, I had code that was functional. But there was a blaring short-coming. I was putting the primary thread to sleep for a specific number of seconds while the system rendered the audio. If the audio was made shorter or longer, then this delay would be inappropriate. In this post, I will fix that. I also recycle my buffers in this update. This is a problem in both the Windows and Linux versions of the code. I am only showing the changes for Windows here. I’ll make the functionally equivalent changes to the Linux version later.
Receiving Updates About Playback
The XAudio2 API is able to make callbacks to provide information about the playback. You can get updates on when a buffer has finished playing, when a buffer is started, when the entire stream has finished, when there’s an error, so on. To get this information, the developer must implement an interface named IXAudio2VoiceCallback. Let’s look at the interface definition.
struct IXAudio2VoiceCallback {
virtual void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) = 0;
virtual void __stdcall OnVoiceProcessingPassEnd() = 0;
virtual void __stdcall OnStreamEnd() = 0;
virtual void __stdcall OnBufferStart(void* pBufferContext) = 0;
virtual void __stdcall OnBufferEnd(void* pBufferContext) = 0;
virtual void __stdcall OnLoopEnd(void* pBufferContext) = 0;
virtual void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) = 0;
};
The methods all only receive information and don’t require anything be returned. You can have empty implementations for messages that you don’t care about, and provide implementations on what you do care about. In my case, I do not care about the methods OnVoiceError(), OnLoopEnd(), OnVoiceProcessingPassEnd(), and OnVoiceProcessingPassStart(). I found it easier to wrap the variables that I needed for managing my dynamic audio into a class and have that class also implement this interface. I care the most about the OnBufferEnd() method. When I see that a buffer has ended, I can add more audio to the buffer and append it to the end of the list of buffers to be played.
In the previous post for this series, there was a parameter I called in creating my sound-playback resource that had been NULL. This time, I will populate that parameter with a reference to my implementation of the above interface. My implementation of this interface will be named BufferManager. In addition to these methods for monitoring playback status I will have methods to start and stop playing and a method that will block the caller until the end of play is reached. The IXAudio2SourceVoice instance is going to be needed by this class. But I cannot pass it in the constructor. The creation of the IXAudio2SourceVoice reference is dependent on a reference to the BufferManager instance being passed. I create a buffer manager, pass it over to the creation of IXAudio2SourceVoice, and then hand that reference over to the BufferManager instance through BufferManager::SetXAudio2Source().
std::shared_ptr<BufferManager> bufferManager = std::make_shared<BufferManager>();result = pxAudio->CreateSourceVoice(&pxSourceVoice, &wfx, 0, XAUDIO2_DEFAULT_FREQ_RATIO, bufferManager.get(), nullptr, nullptr);bufferManager->SetXAudio2Source(pxSourceVoice);
What follows is the interface for my BufferManager class.
class BufferManager : public IXAudio2VoiceCallback { void setIsPlaying(bool playing); inline bool getIsPlaying() const ;public: static const size_t BUFFER_SIZE = 44100 / 20; // 0.05 second of audio at 44.1kHz std::shared_ptr<VoiceBase>> voice; BufferManager(); ~BufferManager(); void SetXAudio2Source(IXAudio2SourceVoice* pxSourceVoice); void WaitForStreamEnd(); void Start(); void Stop(); void FillNextBuffer(bool final = false); void __stdcall OnVoiceProcessingPassStart(UINT32 BytesRequired) {} void __stdcall OnVoiceProcessingPassEnd() {} void __stdcall OnStreamEnd() override; void __stdcall OnBufferStart(void* pBufferContext) override; void __stdcall OnBufferEnd(void* pBufferContext) override; void __stdcall OnLoopEnd(void* pBufferContext) override {} void __stdcall OnVoiceError(void* pBufferContext, HRESULT Error) override {}};
Even though I have 4 buffers of 1 second each, On that note, let’s make the buffers smaller. Memory is at a premium these days and I intend to eventually run this on computers that have less resources. Another advantage of smaller buffers is higher responsiveness. If I wanted to interject something into the audio, the maximum amount of time that could pass before my new sound is heard is determined by the buffer size.
static const size_t BUFFER_SIZE = 44100/20; // 0.05 second of audio at 44.1kHz
0.05s should be responsive enough. There are other ways to accomplish this, such as by playing a second sound and leaving the main buffer alone. But for my project, I only wish to use one buffer attached to the sound hardware. A potential disadvantage to smaller buffers is that they can be more subject to interruption. If I am performing other work on the same thread in which I am processing sound buffers and the thread becomes busy with some other work, the sound could stuffer.
Before I start playing audio, I populate the buffers and submit them for play. After they are all filled and submitted, I start playing audio. In the BufferManager class, as I get notifications that a buffer has finished playing, I populate and queue up the buffer that just finished. With most of this work occurring in the BufferManager class, there are only a few lines remaining in the main class.
bufferManager->>Start(); <br>bufferManager->>WaitForStreamEnd();<br>bufferManager->>Stop();<br>bufferManager = nullptr;pxMasteringVoice->>DestroyVoice();CoUninitialize();r>return 0;
The code for populating the next buffer is similar to the code that was shown in the previous post, But unlike before, I now optionally set a flag in the structure used to submit a buffer called XAUDIO2_END_OF_STREAM. When this flag is set for a buffer, after the buffer with that flag set plays the IXAudio2VoiceCallback::OnStreamEnd() method will be called. This saves us the need to count buffers.
void BufferManager::FillNextBuffer(bool final) { if (!availableBufferList.empty()) { int bufferIndex = availableBufferList.back(); availableBufferList.pop_back(); // Fill the buffer with new audio data for (size_t sampleIndex = 0; sampleIndex < BUFFER_SIZE; ++sampleIndex) { float sampleValue = voice->>getSample(currentTime); // Get the sample from the voice audioBuffers[bufferIndex][sampleIndex] = static_cast<short>>(sampleValue * 32767); // Convert to 16-bit PCM currentTime += deltaTime; } XAUDIO2_BUFFER buf = {}; buf.AudioBytes = BUFFER_SIZE * sizeof(short); buf.pAudioData = (BYTE*)audioBuffers[bufferIndex].data(); if (final) { buf.Flags = XAUDIO2_END_OF_STREAM; } pxSourceVoice->>SubmitSourceBuffer(&buf); }}
I can now play audio continuously and indefinitely. Before I continue to use this new capability, I need to implement similar functionality for the Linux implementation of my code.
Posts may contain products with affiliate links. When you make purchases using these links, we receive a small commission at no extra cost to you. Thank you for your support.
Mastodon: @j2inet@masto.ai
Instagram: @j2inet
Facebook: @j2inet
YouTube: @j2inet
Telegram: j2inet
Twitter: @j2inet