Previously we saw how to get sound to work on the Raspberry Pi with SDL2 and the SDL2_mixer library. Although this works for simple situations, you’d like your game to have a more sophisticated sound system as we often have many different sound effects playing at once, as well as background music. We could load and play these every time we need them, but when things get busy on the screen this might lead to quite the performace drop.

So we’ll design a class which handles all sound in our application, and we want it to have the following features:

  1. Handle sound initialization with separate initialization of sound modules to ease cross-platform development
  2. Handle background music and switching it
  3. Handling sound effects and repeated sound effects
  4. Clean everything up (duh…)

So, we’ll implement the SoundHandler class, with the following interface:

#ifndef ARCADE_AUDIO_HPP
#define ARCADE_AUDIO_HPP

#include <string>
#include <map>
#include <vector>

#include <SDL2/SDL_mixer.h>

class AudioHandler {
public:
  //Constructor and Destructor
  AudioHandler();
  ~AudioHandler();

  //Separate module initialization (in this case just Ogg Vorbis)
  void InitOgg();
  /*
    For example, additional modules could be implemented
    void InitMP3();
    void InitFLAC();
    void InitMOD();
  */

  //Change background music
  void ChangeMusic(const std::string&);

  //Load effects
  void LoadEffects(const std::vector<std::string>&);

  //Play effect
  void PlayEffect(const std::string&);

  //Play/stop looped effect
  int StartLoopedEffect(const std::string&);
  void StopLoopedEffect(int);
private:
  Mix_Music* music;
  std::map<std::string, Mix_Chunk*> sfx;
};

#endif

Note that I’m not using Mix_Music for the sound effects, but Mix_Chunk. This is because SDL mixer treats sound effects differently from music. You can only have one music file playing at a time, while you can have multiple sound effects playing at once.

Initialization and loading

In the last post on programming with SDL2 mixer we saw how to initialize the system, and this is quite straightforward. Initializing Ogg Vorbis is put in a different file as I like to have all my platform-specific code in 1 file:

AudioHandler::AudioHandler() {
  //Initialize SDL mixer
  if(Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
    std::cerr << "Mix_OpenAudio Error: " << Mix_GetError() << std::endl;
    return;
  }
}

void AudioHandler::InitOgg() {
  //Initialize Ogg Vorbis module
  int initted = Mix_Init(MIX_INIT_OGG);
  if(initted != MIX_INIT_OGG) {
    std::cerr << "Mix_Init Error: " << Mix_GetError() << std::endl;
  }
}

Note that if your sound files use a different bitrate, you have to change the 44100 in the code

Handling background music

We also saw how play music in the last post, however we didn’t yet loop it. In order to do this, we need to look at the second parameter to Mix_PlayMusic, which represents the number of times the music should be looped, so a value of 0 would play the music once, 1 plays it twice etc. A value of -1 will loop it infinitely, which is what we want.

Before loading a new music file, we need to check whether a file is already loaded. This is pretty easy, as when no music is loaded, the music variable will be equal to NULL.

void AudioHandler::ChangeMusic(const std::string& file) {
  //If old music is loaded (!= NULL), free it
  if(music) {
    Mix_FreeMusic(music);
  }

  //Load new music and play it infinitely
  music = Mix_LoadMUS(file.c_str());
  if(!music) {
    std::cerr << "Mix_LoadMUS Error: " << Mix_GetError() << std::endl;
    return;
  }
  Mix_PlayMusic(music, -1);
}

Handing sound effects

Any game will probably have quite a number of sound effects. Some will be used everywhere, some may be specific to a certain level. As we probably have a lot of sound effects, we don’t want all of those in our memory at the same time, but we also don’t want to load every sound effect everytime we go to a new level. To avoid these problems, we’ll use a sound effects pool, and everytime we go to a different level or screen, we’ll pass the entire list of sound effects we want loaded for that screen to AudioHandler. AudioHandler will then free redundant sound effects, and load the effects that weren’t loaded yet. This minimizes the number of disk reads and the amount of memory used at the same time:

void AudioHandler::LoadEffects(const std::vector<std::string>& effects) {
  /*
    Check whether there are redundant effects in the effects pool, and if so, remove them:
      Iterate over all effects in the pool
  */
  for(std::map<std::string, Mix_Chunk*>::const_iterator it = sfx.begin(); 
    it != sfx.end(); ++it) {
    //If the list of effects that need to be loaded doesn't contain the effect, remove it from the pool
    if(std::find(effects.begin(), effects.end(), it->first) == effects.end()) {
      sfx.erase(it->first);
    }
  }

  /*
    Check whether an effect is already loaded, otherwise, load it
      Iterate over all effects that need to be loaded
  */
  for(std::vector<std::string>::const_iterator it = effects.begin(); 
    it != effects.end(); ++it) {
    //If the pool doesn't contain the effect, load it
    if(sfx.find(*it) == sfx.end()) {
      sfx[*it] = Mix_LoadWAV((*it).c_str());
      if(!sfx[*it]) {
        std::cerr << "Mix_LoadWAV Error: " << Mix_GetError() << std::endl;
      }
    }
  }
}

Note that you’ll need to include the algorithm header in order to use std::find.

Playing a sound is almost just as straightforward as playing music, except its function Mix_PlayChannel requires 1 more parameter, which is the channel we want to play the effect on (as the function name suggests). We don’t really have to care about this however, as we can just pass -1 to it, and SDL mixer will find a free channel to play the effect on. The resulting function then is:

void AudioHandler::PlayEffect(const std::string& effect) {
  //Play sound (first parameter is the channel, the last one the amount of loops)
  Mix_PlayChannel(-1, sfx[effect], 0);
}

Looping sound effects

Sometimes you want to loop a sound effects, for example when you have a driving car, an elevator or a flamethrower. We can do this using the loop parameter of Mix_PlayChannel, however in order to stop the sound effect from playing we need to know what channel it is playing on. Luckily, Mix_PlayChannel returns the channel the sound is played on, and we can later call Mix_HaltChannel to stop a sound effect playing on a specific channel.

int AudioHandler::StartLoopedEffect(const std::string& effect) {
  //Play sound forever, and return it's channel
  return Mix_PlayChannel(-1, sfx[effect], -1);
}

void AudioHandler::StopLoopedEffect(int chan) {
  //Stop the channel the looped effect is playing on
  Mix_HaltChannel(chan);
}

And there you have it, a nice, compact class for handling all sound in a game using SDL. Later on, I’ll probably add support for volume and fading in and out. The entire class can be found on this GitHub gist