How to start writing a music replayer for your demo in <200 lines
Okay, so we're back again after the previous
tutorial that showed us how to build a basic 3D renderer.
After I published that one, I've come to recognize two problems with
Miniaudio, the library I've been using in that tutorial:
At the time of writing, it is clocking at 3.92MB, which for a single
header file is a bit ridiculous, so I'm not sure it qualifies for the
prefix "mini-" anymore, and most of it is stuff you'll never need.
In terms of synchronization, it has a pretty bad clock resolution problem,
so in effect you'd be limited to about 100 fps - not too bad,
but certainly not ideal.
So let's fix that, and write our own basic music player from scratch;
we're going to be using WASAPI
as our audio backend, solely because it's shockingly simple and at this
point DirectSound is just a wrapper on top of it anyway.
The basics
Again, if you're having problems with this, off to
Codecademy
with you.
Initializing WASAPI
First off, we need to initialize our audio device.
We include mmdeviceapi.h which is where WASAPI lives;
we only need conio.h for _kbhit() to quit on keypress
just for the sake of this example - you will probably replace that later
with your own demo's mainloop.
First off, we'll need a device enumerator, and the device itself;
the enumerator is basically a manager object that takes care of all
the audio devices in the system.
Why? Because remember: nowadays, your headphones,
your VR helmet, your phone, your video game controller, they all have
speakers and microphones, and usually act as separate audio devices,
so you'll need some sort of selection system to send the audio through
the correct one.
We also need to keep track of how big our buffer is - we'll get back
to that later.
This is the basic initialization flow:
First we initialize the COM interface;
you can read more about it here this later.
Next, we create an instance of MMDeviceEnumerator using the
class UUID,
and then use that instance to create our audio device; there are a number
of parameters to GetDefaultAudioEndpoint
that you can read all about later.
We also create a basic mainloop that just busywaits - our music will be
playing in the background of whatever we're doing, so for this example
we don't need to do anything here (yet).
And then we clean up, because we're civilized people.
Right now, this doesn't do much, but as long as we can run this without
an error we're on the right track.
Creating an audio client
The next step is creating what WASAPI calls an "audio client"; this is
effectively what the operating system considers the interface between
itself and an application that plays audio.
First we include the header.
This will be the instance of our audio client.
When we create the audio client, we will need to specify the format
of audio we're sending out; typically even now in 2026, the most common
format is what's usually referred to is "CD quality" audio -
44100Hz, 16-bit,
stereo - so we set ourselves up accordingly.
Of particular importance is the WAVEFORMATEX struct;
this is a very common struct when dealing with audio in Windows,
so much so that it's used in the header of a .WAV
file to describe its contents. You can read all about the details
of how to fill this struct out
here.
One sidenote worth making is that since most audio operations tend
to be in floating point, you can also specify your audio to be in 32-bit
floats instead of 16-bit shorts and skip the conversion step - this is
up to you.
Then we create our client object, and initialize it.
One interesting note is the bufferDuration variable:
the REFERENCE_TIME type in Windows is
defined
as a unit that counts in 100-nanosecond increments. This means that 10
such units will constitute as one microsecond, so 10 * 1000 * 1000 units
will constitute a second. Thus, specifying this value will give us a second long buffer.
We also query the buffer size we got; however, note that the value returned here
is in frames; a frame is generally what we call any number of audio
samples that are played simultaneously, so a stereo 16-bit 44100 Hz signal will have 44100
frames per second, but 88200 samples, totalling at 176400 bytes.
(Be mindful that in common parlance, the term "sample" is sometimes used
interchangably with "frame", which can be dangerous.)
And then we clean up like the wonderful people we are.
Again, not a whole lot of things happening, but at least we're still
rolling without errors.
Firing up a thread
Audio by its nature is a background operation, so we'll need to somehow
run it in the background - in a modern operating system, this is what
threads are for;
if you're not familiar with them, just think about a thread as a function
call that runs in the background. As you can imagine, inter-thread
communication can get super complicated, but luckily for our use case
we don't need to worry about that.
This is our thread function. Right now it does nothing and quits
immediately, ending the thread. This, for now, is fine.
Here we start the thread; this is all simple stuff that's
thoroughly documented
so we won't spend a lot of time on it here; all you need to know is that
when you call CreateThread, your main thread will continue
execution further, while simultaneously ThreadProc will also
start running concurrently.
Because we don't want audio to interrupt, we prefer putting our thread
to have the highest possible priority.
And we clean up.
Next, let's fill up the thread.
Setting up the thread logic
So we have a thread running that quits immediately, what do we do next?
Well, what we need is our thread to continually feed the music data we wants to
play into the WASAPI buffer, but also stop doing that and quit when the main
thread tells it to stop.
So let's do that.
First, we need two flags:
shouldExit will be written into by the main thread to tell the
thread to quit when it's true, while inThread
will be written into by the audio renderer thread, and will signal to
the main thread when it has finally finished; this latter is needed
in order to be sure that the thread has quit so that we don't cause a
crash trying to deallocate resources while it's still running even
though we told it to stop.
First, in the thread, we tell the audio client to start playing. This is
what actually begins playing the audio.
Next, we reset the two flags in our audio thread, and start rendering
until we're told to stop; we'll leave this empty for now and come back
to it in a moment. (The
Sleep()
call is merely to yield the CPU when
we're not doing anything.)
If we're told to stop, tell the client to stop playing, signal we're done,
and exit the thread cleanly.
Inside the main thread, since now our audio thread loops, once we're
finished with our replay, tell the thread to exit, then wait for it to
exit.
To recap here, this is our expected sequence of events:
We start the thread using CreateThread.
The main thread enters the mainloop.
The audio thread enters its own loop - right now neither of them do
anything.
If we press a key, the main thread exits its own loop while the
audio is still running its own.
The main thread signals, through shouldExit = true;,
to the audio thread that it should finish up. It then busywaits until
inThread == false
The audio thread, seeing shouldExit == true,
leaves its own loop, stops audio, sets inThread to
false and exits cleanly.
The main thread exits out of the busy wait loop and begins
deallocating. Tidy!
One thing to mention here is that using simple bool flags
and no things like
critical sections
or locks
will work in this use case but not in more complex situations;
feel free to play The
Deadlock Empire if you want to learn more about why.
Setting up a renderer
Now that we have an audio thread and the two loops, we'll need an audio
renderer that feeds the audio data from our music through a buffer to the
actual sound device.
This will be the actual renderer object.
Next, we create it through the audio client.
This function will serve as the buffer filler; it requests a number of
frames from the render client buffer, decodes our music into it (we'll
do that later), and then sends it back to the render client.
We'll use a separate function here because we're going to use it in two
places.
First, before we play the buffer, we fill it in full so that we're not
playing an empty buffer.
And then, after playing, we start rendering to the thread - but first
we need to understand how modern audio works:
Audio buffers are ring buffers
and have a play cursor continually moving forwards in them and wrapping
around when the end is reached; think about it as a vinyl record that,
instead of a spiral groove, has a circular locked groove that it keeps
replaying over and over. What we can do, however, is simply write to
the buffer somewhere ahead of the play cursor (our "needle") so that
by the time the cursor gets there, the buffer is already filled.
There are two schools of thought here: an interactive application like
a video game will try to keep this gap as low as possible, since the
sound needs to play as soon as the main thread requests it. In our case,
however, since the music never changes, we can effectively be ahead of
the cursor almost a full buffer's length (i.e. we're almost
"behind" the cursor) and have no perceptible difference. So we'll do that:
First, we get the
"padding" of the current audio replay; this effectively means how
much of the buffer (again, in frames) we shouldn't touch.
If we subtract this value from our full buffer size, we get a value of
how much the cursor effectively advanced.
If this value is non-zero, we then fill the buffer for the appropriate
amount of frames.
And of course we clean up the render client as well.
So now we have most things in place, except we're not putting anything
into our buffer so we're playing back silence (although feel free to
put a sine wave or white noise in there for giggles) - let's fix that.
Loading an MP3
For music decoding, we're going to use dr_mp3,
which is a simple single-file MP3 decoding library; there are a ton of
similar libraries for various other formats of course
(stb_vorbis
is a popular example) so feel free to substitute this, but we'll just stick
for this for now.
We start by including the header.
We then declare the actual MP3 object.
We then load the file. (As usual, supplying the actual MP3 is an exercise
left up to the reader.)
In the thread, we tell the MP3 decoder to decode the amount of frames
into signed 16-bit format into our buffer.
And then we deallocate.
Practically speaking, we're done! We have an MP3 playing! Whee!
...Except we (probably) need one more thing.
Reading the timer
See, if we're writing a demo, we will probably need a way to tell where
the audio replay is at so that we can synchronize our visuals to it - we
could just use a system timer, but remember, part of the reason for this
exercise is that we're trying to have the most accurate timer we can, so
let's go.
For this, we're doing to use a WASAPI feature called the "audio clock".
This isn't strictly needed, it's just so that we can use printf.
We declare the clock object.
We create the clock object, and retrieve the clock frequency; this is
needed because the position retrieval in WASAPI is based on a hardware
clock.
We then retrieve the position, and divide it with the clock frequency;
this will give you the current audio replay position measured in seconds.
And we then clean up after ourselves.
And that's it.
We have an MP3 playing and a highly accurate timer telling us where it is.
So where can you go from here? Well, first you'll probably want to
turn all this into a library so that you can plug it into your engine;
you might also want to support more than one format, and you could also
use this to turn this into a softsynth for a 64k intro.