Handmade Penguin Chapter 7: Initialising SDL Audio

<-- 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:

freq : This is the sampling frequency of our sound: the number of samples per second we need to provide.

: This is the sampling frequency of our sound: the number of samples per second we need to provide. format : This is the format of each individual sample. To have signed, 16-bit, little-endian integer samples, we'll use AUDIO_S16LE .

: This is the format of each individual sample. To have signed, 16-bit, little-endian integer samples, we'll use . channels : The number of channels of sound we're providing: 1 for mono, 2 for stereo, more for surround sound setups.

: The number of channels of sound we're providing: 1 for mono, 2 for stereo, more for surround sound setups. silence : The recommended value for silence. This is calculated by SDL, we don't need to provide it, and we can work this out from format anyway. (It's usually 0).

: The recommended value for silence. This is calculated by SDL, we don't need to provide it, and we can work this out from anyway. (It's usually 0). samples : The number of samples we should provide in one go. Higher values will have greater latency, but result in our callback being executed less frequently.

: The number of samples we should provide in one go. Higher values will have greater latency, but result in our callback being executed less frequently. size : The size in bytes. SDL calculates this for us, we don't need to provide it. It's equal to (the size of a sample in format )*( channels )*( samples ).

: The size in bytes. SDL calculates this for us, we don't need to provide it. It's equal to (the size of a sample in )*( )*( ). callback : A callback function. This function will be called by SDL whenever more audio data is required. It is often called on a different thread to the main game.

: A callback function. This function will be called by SDL whenever more audio data is required. It is often called on a different thread to the main game. userdata : A pointer of our choice that gets passed to callback . This is a way for us to assosciate data with our sound system without using global variables.

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); }

UserData : The pointer we passed in our SDL_AudioSpec . We're not going to use it yet.

: The pointer we passed in our . We're not going to use it yet. AudioData : A pointer to a buffer where we can put all of our sample data. We can cast this to our sample size.

: A pointer to a buffer where we can put all of our sample data. We can cast this to our sample size. Length : The length of the buffer pointed to by AudioData in bytes.

memset()

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);

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:

dev : This is the id number of our audio device. If we used SDL_OpenAudioDevice() it would return this. Since we used SDL_OpenAudio() , it's always 1.

: This is the id number of our audio device. If we used it would return this. Since we used , it's always 1. data : This is the raw sample data we want to queue, in the format of the obtained SDL_AudioSpec from SDL_OpenAudio() .

: This is the raw sample data we want to queue, in the format of the from . len : The length, in bytes, of data .

SDL_QueueAudio()

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 -->