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:

  1. 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.
  2. 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:

  1. We start the thread using CreateThread.
  2. The main thread enters the mainloop.
  3. The audio thread enters its own loop - right now neither of them do anything.
  4. If we press a key, the main thread exits its own loop while the audio is still running its own.
  5. The main thread signals, through shouldExit = true;, to the audio thread that it should finish up. It then busywaits until inThread == false
  6. The audio thread, seeing shouldExit == true, leaves its own loop, stops audio, sets inThread to false and exits cleanly.
  7. 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:

  1. 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.
  2. If we subtract this value from our full buffer size, we get a value of how much the cursor effectively advanced.
  3. If this value is non-zero, we then fill the buffer for the appropriate amount of frames.

This, by the way, is absolutely by the book and how Microsoft themselves suggest doing it.

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.