Handmade Penguin Chapter 7: Initialising SDL Audio

This chapter covers roughly the content in the Initializing DirectSound part of the Handmade Hero course, under the Linux operating system.

<-- Chapter 6 | Back to Index | Chapter 8 -->

Alt+F4

Unlike in windows, handling SDL_KEYDOWN and SDL_KEYUP doesn't disable the window manager shortcuts. This means that Alt+F4 is already working if it's configured in your window manager. However, if our window manager doesn't support it, we can add it in ourselves.

Be warned! Depending on the exact window manager and version of SDL you're using, grabbing input (in particular grabbing the keyboard), will break all of your window manager's keyboard shortucts, including things like Alt+Tab. Also be careful: if you don't have the XRandR library configured and you use fullscreen mode, sometimes your keyboard will be grabbed for you. Eek!

If we look up the SDL_Keysym struct that lies in our SDL_KeyboardEvent, we see that it has a mod member. This is a bitfield of SDL_Keymod values which represent the current state of all of the modifier keys on the keyboard. So, to check if Alt is pressed, we can use:

bool AltKeyWasDown = (Event->key.keysym.mod & KMOD_ALT);

We can then just check if the AltKeyWasDown and F4 was pressed, we quit:

if (KeyCode == SDLK_F4 && AltKeyWasDown)
{
    ShouldQuit = true;
}

Be warned! If your window manager does have its own Alt+F4 handling, then it's likely that we'll get our SDL_QUIT message before we even get the SDL_KEYDOWN message for the Alt+F4. Because we quit when we see SDL_QUIT, we may never see our Alt+F4 SDL_KEYDOWN event.

A Sound Idea

The first thing we need to to do get sound working in our game is to initialise SDL's audio subsystem. We can do this with the familiar SDL_Init() function — we just need to add the SDL_INIT_AUDIO flag:

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_GAMECONTROLLER | SDL_INIT_HAPTIC | SDL_INIT_AUDIO);

Check this out! If you want to specify which driver SDL uses for audio, you can use the SDL_AudioInit() function instead. It's usually best to simply use the defaults, though: they'll be tailored to fit the user's system.

Once the audio subsystem is initialised, we need to open the audio device. We do this with the SDL_OpenAudio() function. This function accepts two pointers to an instance of the SDL_AudioSpec struct. SDL_AudioSpec describes the properties of an audio device: SDL_OpenAudio() accepts one as input (desired), which contains the properties you want, and one as output (obtained), which contains the properties of the device the system was able to provide. If we no-longer need our desired SDL_AudioSpec, we can pass a null pointer to obtained, and desired will be modified instead.

Now let's look at the SDL_AudioSpec struct to see what we have to fill in:

Clearly, we need to make a callback function before we can do anything:

internal void
SDLAudioCallback(void *UserData, Uint8 *AudioData, int Length)
{
    // Clear our audio buffer to silence.
    memset(AudioData, 0, Length);
}
Not as horrible as one might think, is it? The parameters we get are: We use the memset() function to clear the buffer to silence. It takes a pointer, a byte value and the number of bytes, staring at the pointer, to set to the byte. By setting everything to zero, we make it silence.

Now let's open our audio device:

SDL_AudioSpec AudioSettings = {0};

AudioSettings.freq = SamplesPerSecond;
AudioSettings.format = AUDIO_S16LE;
AudioSettings.channels = 2;
AudioSettings.samples = BufferSize;
AudioSettings.callback = &SDLAudioCallback;

SDL_OpenAudio(&AudioSettings, 0);
Note that, unlike DirectSound, SDL handles all of the ring buffer stuff for us. This means that we don't want a huge, two second buffer: that'd give us two seconds of latency. We generally don't care exactly how big our buffer is: it needs to be big enough that we can update it in time, but small enough to not have huge amounts of latency. Note that the underlying API might not give us what we want (things like ALSA and HDMI output tend to do this) anyway. We do want to check that we got the right format though:
if (AudioSettings.format != AUDIO_S16LE)
{
    // TODO: Complain if we can't get an S16LE buffer.
}

Check this out! There's a slightly more advanced function for opening the audio device, SDL_OpenAudioDevice(). This lets you open multiple audio devices, open capture devices for recording audio, and tell SDL which parts of your SDL_AudioSpec you must get back exactly and which you don't really care about.

We can close the device with SDL_CloseAudio(). SDL_Quit() will also do this for us, but doing it by hand can't hurt. You need to be careful on Linux, as if the user is not using PulseAudio or ALSA's dmix feature, then only one audio device can be open across the entire system at a time. Fortunately this is increasingly rare (and many games don't support this system at all), but leaving the audio device open doesn't make you many friends in that situation.

Queueing and Pausing Audio

SDL doesn't actually start playing any audio when you open the device, as it starts off paused. To actually start playing audio, we need to call SDL_PauseAudio. To pause audio, pass 1 as its argument, to unpause, 0. So, to start playing our silence, we'll need to unpause it:

SDL_PauseAudio(0);

You might have noticed how different the idea of having an audio callback is to just having a single buffer we write new samples to (like DirectSound). Fortunately, SDL provides another way of passing audio data to the device: queueing. If we don't provide a callback (leaving the callback field of our SDL_AudioSpec structure null), then instead of having SDL call us when it wants new audio, we can queue up new samples to be played with SDL_QueueAudio(). If we haven't queued up enough audio, we'll simply get silence.

Be warned! The SDL_QueueAudio() function is only available in SDL 2.0.4 or newer, which means that most Linux distros do not have it yet. Whether or not we end up using it depends on how the future audio code in Handmade Hero is put together. For now, treat this as some bonus information. The source code download does not yet use it.

SDL_QueueAudio takes three parameters:

If your system supports SDL_QueueAudio(), give it a try! It may end up being much easier than using a callback!

End of Lesson!

It might not seem that impressive, but we've got a lot done behind the scenes! We are actually playing sound (even if it happens to be silence). If you don't believe me, open your system's mixer/volume control while our game is running: you should see it listed as one of the programs producing audio! We'll see you tomorrow for even more sound-releated fun!

Be warned! Timezones are conspiring against me: I won't be doing these (even slightly) live for the next few days. I will be trying to get each chapter done before the next stream comes out, though.

If you've bought handmade hero, the source for the linux version can be downloaded here.


<-- Chapter 6 | Back to Index | Chapter 8 -->