Let’s Make Honey! Version 0.09 – Sounds Good


 

Let’s Make Honey!

 

 

Today we add squishy squishy sound.

 

Standard caveat: I’m opening up my learning process in the spirit of the CRAPL license. I’ll be making C++ mistakes and learning to correct them in public! For the first ten or twenty posts, don’t trust me as a C++ teacher!

 

 

Prepping the SDL

 

We’ll be using the SDL, and like when we worked with images, what this really means is we’ll be using another external library some nice people made to work with the SDL. This time it’s SDL_mixer, which will allow us to load mp3s and play multi-channel sound. Multi-channel sound just means sounds that stack on top of each other. It’s something we’ll see in passing today but we really won’t think much about it.

 

Go to the SDL_mixer download site, and I CANNOT STRESS THIS ENOUGH, get the version 2.0.0 dmg file, not the version 2.0.2. Per a comment from an obscure gist snippet, version 2.0.2 has an error which will prevent it from loading mp3s, whereas version 2.0.0 works fine, and I can confirm that this is true.

 

Seriously, download version 2.0.0, not version 2.0.2.

 

Open it and install the SDL2_mixer framework in /Library/Frameworks, same as you did with SDL2 and SDL2_image.

 

Now set the version to 0_09 in both Engine’s SConstruct and BearsLoveHoney’s SConstruct files. Additionally, modify line 10 of BearsLoveHoney’s SConstruct to read:

 


env.AppendUnique(FRAMEWORKS = Split("OpenGL SDL2 SDL2_image SDL2_mixer"))

 

We’re just telling our builds to use the SDL2_mixer framework along with all those others.

 

Before we really get going, we’re also going to make some small changes to the logic and effects class, to seed our random numbers earlier and to add a random integer generator:

 

 

As always, that diff screenshot is also a link to the actual diff on github.

 

Now, before we write an actual sound class, we’re going to have to modify our very early init work to turn sound on. Make these modifications to window.h and window.cpp:

 

 

We’re adding SDL2_mixer to the window class, and we’re making sure SDL initializes with audio as well as video and game controllers. On lines 56-61 of window.cpp, we use the SDL_mixer Mix_Init function to tell the mixer to initialize mp3 functionality. Later, when we quit the window, we also quit the mixer.

 

Build the library (Go to Engine and run scons) and the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney. It should still work as before.

 

Classy Audio

 

 

Now we’ll write the audio wrapper class. In Honey, we’re going to have sounds (which are all wave files) and music (which are all mp3 files). From the user perspective, these won’t be very different. We may even combine them later if this structure proves too unwieldy.

 

But for now, since the SDL thinks of them as two different things under the hood, in Honey we’ll treat them as two different things for code simplicity.

 

Much like images, we want to be able to add, play and destroy sounds, and also add, play and destroy music. We also want volume control. For now, we’re going to settle on just this fairly simple functionality.

 

Sooooo, add empty sound.h and sound.cpp files to Engine/Source, and add this to sound.h:

 


/*

  Honey
  Copyright 2018 - Matthew Carlin

  Sound class wraps SDL sound functions.
*/

#ifndef HONEY_SOUND_H_
#define HONEY_SOUND_H_

#include <string>
#include <stdlib.h>
#include <unordered_map>

#include <SDL2/SDL.h>
#include "SDL2_mixer/SDL_mixer.h"

namespace Honey {
  class Sound {
   public:

      std::unordered_map<std::string, Mix_Chunk*> sounds;
      std::unordered_map<std::string, Mix_Music*> music;

      Sound();

      void initialize();

      // From 0 silent to 1 full volume
      void setSoundVolume(float fraction);
      void setMusicVolume(float fraction);

      void addSound(std::string label, std::string path);
      //-1 loops forever
      void playSound(std::string label, int loops);
      void destroySound(std::string label);

      void addMusic(std::string label, std::string path);
      //-1 loops forever
      void playMusic(std::string label, int loops);
      void destroyMusic(std::string label);
  };

  extern Sound* sound;
}
#endif

 

Just what I said. We have methods for adding, playing and destroying sound, and adding, playing, and destroying music. We can choose how many times the sound or the music loops. We also have methods for controlling sound and music volume independently of each other.

 

Under the hood, sound is stored in Mix_Chunk objects, and music is stored in Mix_Music objects. They’re not too different from our casual perspective.

 

Sound.cpp isn’t actually tooooo complicated, so let’s just punch it out all in one go:

 


/*

  Honey
  Copyright 2018 - Matthew Carlin

*/

#include "sound.h"

namespace Honey {
  Sound* sound = new Sound();

  Sound::Sound() {
    sounds = {};
    music = {};
  }

  void Sound::initialize() {
    Mix_OpenAudio(22050, AUDIO_S16SYS, 2, 640);
  }

  void Sound::setSoundVolume(float fraction) {
    Mix_Volume(-1, (int) (MIX_MAX_VOLUME * fraction));
  }

  void Sound::setMusicVolume(float fraction) {
    Mix_VolumeMusic((int) (MIX_MAX_VOLUME * fraction));
  }  

  void Sound::addSound(std::string label, std::string path) {
    Mix_Chunk *sound;
    sound = Mix_LoadWAV(path.c_str());
    sounds[label] = sound;
  }

  void Sound::playSound(std::string label, int loops) {
    if (sounds.count(label) == 0) {
      printf("Failed to find %s in sounds.\n", label.c_str());
      return;
    }

    if (loops > 0) {
      // In PlayChannel, 0 is 1 loop, 1 is two loops, etc.
      loops--;
    }

    Mix_PlayChannel(-1, sounds[label], loops);
  }

  void Sound::destroySound(std::string label) {
    if (sounds.count(label) == 0) {
      printf("Failed to delete %s because it wasn't in sounds.\n", label.c_str());
      return;
    }

    Mix_FreeChunk(sounds[label]);
    sounds.erase(label);
  }

  void Sound::addMusic(std::string label, std::string path) {
    Mix_Music *musica;
    musica = Mix_LoadMUS(path.c_str());
    music[label] = musica;
  }

  void Sound::playMusic(std::string label, int loops) {
    if (music.count(label) == 0) {
      printf("Failed to find %s in music.\n", label.c_str());
      return;
    }

    Mix_PlayMusic(music[label], loops);
  }

  void Sound::destroyMusic(std::string label) {
    if (music.count(label) == 0) {
      printf("Failed to delete %s because it wasn't in sounds.\n", label.c_str());
      return;
    }

    Mix_FreeMusic(music[label]);
    music.erase(label);
  }
}

 

The initialize method uses Mix_OpenAudio to turn on very standard mid-quality stereo sound. You can read about it in depth if you want. I didn’t.

 

setSoundVolume uses Mix_Volume, and setMusicVolume uses Mix_VolumeMusic, both of which take an integer between 0 and MIX_MAX_VOLUME (which I think is 128). We want to set volume in fractions, so we just multiply by MIX_MAX_VOLUME and cast to an int.

 

addSound uses the Mix_LoadWAV function to load a wave file given by the path. I’m feeling lazy today, so we haven’t done checking to make sure this load works. We’ll add that in later. The file is loaded into a Mix_Chunk object, which we store in our map using the label as a key.

 

Similarly addMusic uses the Mix_LoadMUS function to load an mp3 file given by the path. We’re not checking success here either; again, we’ll add it later. The file is loaded into a Mix_Music object, which we store in our other map using the label as a key.

 

destroyChunk uses Mix_FreeChunk and destroyMusic uses Mix_FreeMusic, each to actually free the object from memory, after which we delete the label.

 

playMusic uses Mix_PlayMusic to play the Mix_Music object. It just needs this object and a number of loops. If we pass -1, it will loop forever. If we pass 0, it will treat the request as a No-op. Otherwise, if we pass N, it will play N times.

 

Similarly but not quite the same, playSound uses Mix_PlayChannel to play the Mix_Chunk sound object. In this case we have to specify a channel, the object, and a number of loops. Giving -1 for the channel means “just use the next available channel”, which is what we want, so we always do that. For the number of loops, -1 will again loop forever, but in this case, 0 means “play once”, 1 means “play twice”, etc. So if it’s not -1, we actually subtract 1 (to keep consistency with our other method). So for Honey, 1 always means “play once”, 2 means “play twice”, etc.

 

And that’s it! Build the library (Go to Engine and run scons) and the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney. It should still still still work as before. Only now we can put sounds in it.

 

 

Mr. Soundgood Goodsounds

 

 

To add to BearsLoveHoney today, we have some adorable sound by Friends On Mountain’s Trace Ronning. He’s given me a cute piece of music called “Nothing to Fear”. We also went a sound site called splice.com and bought some royalty free sounds to use in this and other games. You can find all of these on the Honey github page at https://github.com/bugsbycarlin/Honey/tree/master/BearsLoveHoney/Sound. Go there and grab those files. Make a directory in BearsLoveHoney called Sound and put all these sweet files there.

 

Modify config.txt to add some volume settings, and, heck, we’re going to need to shorten the animations to match the sound speeds, so modify those too:

 

 

And finally, modify BearsLoveHoney’s main.cpp like so:

 

 

Build the library (Go to Engine and run scons) and the game (Go to BearsLoveHoney and run scons BearsLoveHoney), and then run ./BearsLoveHoney. You should now have some candy sweet background music and some juicy action sounds when you move the carousel. Try changing the volume in the config file and see if it updates in game.

 

As for the code in main.cpp, I’m not even going to explain myself. We’re pretty literally just adding sounds, playing sounds, and destroying sounds, using the class we just created.

 

And it sounds good.

 

Leave a Reply

Your email address will not be published. Required fields are marked *