Lazy Foo' Productions

SDL Forums external link SDL Tutorials 🐰SDL3 Tutorials🥚 Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Sound Effects and Music

Sound Effects and Music screenshot

Last Updated: Oct 19th, 2024

SDL's base audio is complex to say the least. The SDL_mixer extension library makes it much easier to play music and sound effects.
//Using SDL, SDL_image, SDL_mixer, and STL string
#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_mixer/SDL_mixer.h>
#include <string>
We're going to be using the SDL_mixer extension library so make sure it's set up in your project.
//Channel constants
enum eEffectChannel
{
    eEffectChannelScratch = 0,
    eEffectChannelHigh = 1,
    eEffectChannelMedium = 2,
    eEffectChannelLow = 3,
    kEffectChannelTotal = 4
};
In order to play a sound effect you need to play it on a channel. We have 4 different sound effects (a scratch and high/medium/low tap effects) and we'll have a channel for each sound effect.

In a real game, you don't need to allocate a channel for each sound effect (in fact you probably shouldn't), we're just doing it to demonstrate how channels work.
//The window we'll be rendering to
SDL_Window* gWindow{ nullptr };

//The renderer used to draw to the window
SDL_Renderer* gRenderer{ nullptr };

//Instruction texture
LTexture gPromptTexture;

//Playback audio device
SDL_AudioDeviceID gAudioDeviceId{ 0 };

//Allocated channel count
int gChannelCount = 0;

//The music that will be played
Mix_Music* gMusic{ nullptr };

//The sound effects that will be used
Mix_Chunk* gScratch{ nullptr };
Mix_Chunk* gHigh{ nullptr };
Mix_Chunk* gMedium{ nullptr };
Mix_Chunk* gLow{ nullptr };
In terms of global variables, we're using a SDL_AudioDeviceID to keep track of the audio device we're going to play to, gChannelCount to keep track of how many effect channels we have, a Mix_Music for our music and Mix_Chunks for our sound effects.
bool init()
{
    //Initialization flag
    bool success{ true };

    //Initialize SDL with audio
    if( !SDL_Init( SDL_INIT_VIDEO | SDL_INIT_AUDIO ) )
    {
        SDL_Log( "SDL could not initialize! SDL error: %s\n", SDL_GetError() );
        success = false;
    }
When using SDL_mixer or SDL audio in general, make sure to pass SDL_INIT_AUDIO to SDL_Init.
            //Initialize PNG loading
            int imgFlags = IMG_INIT_PNG;
            if( !( IMG_Init( imgFlags ) & imgFlags ) )
            {
                SDL_Log( "SDL_image could not initialize! SDL_image error: %s\n", SDL_GetError() );
                success = false;
            }

            //Set audio spec
            SDL_AudioSpec audioSpec;
            SDL_zero( audioSpec );
            audioSpec.format = SDL_AUDIO_F32;
            audioSpec.channels = 2;
            audioSpec.freq = 44100;

            //Open audio device
            gAudioDeviceId = SDL_OpenAudioDevice( SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &audioSpec );
            if( gAudioDeviceId == 0 )
            {
                SDL_Log( "Unable to open audio! SDL error: %s\n", SDL_GetError() );
                success = false;
            }
            else
            {
                //Initialize SDL_mixer
                if( !Mix_OpenAudio( gAudioDeviceId, nullptr ) )
                {
                    printf( "SDL_mixer could not initialize! SDL_mixer error: %s\n", SDL_GetError() );
                    success = false;
                }
            }
Much like how we had to initialize SDL_image, we need to initialize SDL_mixer.

First we're specify a SDL_AudioSpec with 32 bit stereo audio at 44.1 Khz. Then we call SDL_OpenAudioDevice to open up our default playback device. Once we have our playback device, we initialize SDL_mixer with it using Mix_OpenAudio.
bool loadMedia()
{
    //File loading flag
    bool success{ true };

    //Load scene image
    if( success &= gPromptTexture.loadFromFile( "15-sound-effects-and-music/prompt.png" ); !success )
    {
        SDL_Log( "Unable to load prompt image!\n" );
    }

    //Load audio
    if( gMusic = Mix_LoadMUS( "15-sound-effects-and-music/beat.wav" ); gMusic == nullptr )
    {
        SDL_Log( "Unable to load music! SDL_mixer error: %s\n", SDL_GetError() );
        success = false;
    }
    if( gScratch = Mix_LoadWAV( "15-sound-effects-and-music/scratch.wav" ); gScratch == nullptr )
    {
        SDL_Log( "Unable to load scratch sound! SDL_mixer error: %s\n", SDL_GetError() );
        success = false;
    }
    if( gHigh = Mix_LoadWAV( "15-sound-effects-and-music/high.wav" ); gHigh == nullptr )
    {
        SDL_Log( "Unable to load high sound! SDL_mixer error: %s\n", SDL_GetError() );
        success = false;
    }
    if( gMedium = Mix_LoadWAV( "15-sound-effects-and-music/medium.wav" ); gMedium == nullptr )
    {
        SDL_Log( "Unable to load medium sound! SDL_mixer error: %s\n", SDL_GetError() );
        success = false;
    }
    if( gLow = Mix_LoadWAV( "15-sound-effects-and-music/low.wav" ); gLow == nullptr )
    {
        SDL_Log( "Unable to load low sound! SDL_mixer error: %s\n", SDL_GetError() );
        success = false;
    }

    //Allocate channels
    if( success )
    {
        if( gChannelCount = Mix_AllocateChannels( kEffectChannelTotal ); gChannelCount != kEffectChannelTotal )
        {
            SDL_Log( "Unable to allocate channels! SDL_mixer error: %s\n", SDL_GetError() );
            success = false;
        }
    }

    return success;
}
To load music, we call Mix_LoadMUS and to load our sound effects we call Mix_LoadWAV.

After loading our audio files, we allocate audio channels using Mix_AllocateChannels.
void close()
{
    //Free music
    Mix_FreeMusic( gMusic );
    gMusic = nullptr;

    //Free sound effects
    Mix_FreeChunk( gScratch );
    gScratch =  nullptr;
    Mix_FreeChunk( gHigh );
    gHigh =  nullptr;
    Mix_FreeChunk( gMedium );
    gMedium =  nullptr;
    Mix_FreeChunk( gLow );
    gLow =  nullptr;


    //Clean up textures
    gPromptTexture.destroy();

    //Close mixer audio
    Mix_CloseAudio();

    //Close audio device
    SDL_CloseAudioDevice( gAudioDeviceId );
    gAudioDeviceId = 0;

    //Destroy window
    SDL_DestroyRenderer( gRenderer );
    gRenderer = nullptr;
    SDL_DestroyWindow( gWindow );
    gWindow = nullptr;

    //Quit SDL subsystems
    Mix_Quit();
    IMG_Quit();
    SDL_Quit();
}
When we're done with music or a sound effect we call Mix_FreeMusic/Mix_FreeChunk to free them. To close SDL_mixer audio, we call Mix_CloseAudio and to close the SDL audio device we call SDL_CloseAudioDevice. When we're done with the SDL_mixer library, we call Mix_Quit
                        switch( e.key.key )
                        {
                            //Play high sound effect
                            case SDLK_1:
                            Mix_PlayChannel( eEffectChannelHigh, gHigh, 0 );
                            break;
                            
                            //Play medium sound effect
                            case SDLK_2:
                            Mix_PlayChannel( eEffectChannelMedium, gMedium, 0 );
                            break;
                            
                            //Play low sound effect
                            case SDLK_3:
                            Mix_PlayChannel( eEffectChannelLow, gLow, 0 );
                            break;
                            
                            //Play scratch sound effect
                            case SDLK_4:
                            Mix_PlayChannel( eEffectChannelScratch, gScratch, 0 );
                            break;
To play a sound effect, call Mix_PlayChannel with the channel you want to play on and the effect you want to play (and if you want to loop it or not and since we don't we set the looping to 0). One thing you should notice if you press the button rapidly (this is especially noticable on the scratch sound effect) that it will interrupt the effect currently playing to start the effect on the same channel. However if you we to start the scratch effect and quickly start another sound effect it would not interrupt it because it's on a different channel.

Currently, SDL_mixer creates 8 channels by default but it's a good idea to be explicit in terms of how many channels you need because this default might change. How many channels you need depends on how you want to do your sound design.
                            //If there is no music playing
                            case SDLK_9:
                            if( Mix_PlayingMusic() == 0 )
                            {
                                //Play the music
                                Mix_PlayMusic( gMusic, -1 );
                            }
                            //If music is being played
                            else
                            {
                                //If the music is paused
                                if( Mix_PausedMusic() == 1 )
                                {
                                    //Resume the music
                                    Mix_ResumeMusic();
                                }
                                //If the music is playing
                                else
                                {
                                    //Pause the music
                                    Mix_PauseMusic();
                                }
                            }
                            break;
                            
                            //Stop the music
                            case SDLK_0:
                            Mix_HaltMusic();
                            break;
                        }
When we press the 9 key we check if the music is playing with Mix_PlayingMusic. If it isn't, we start the music with Mix_PlayMusic. If the music is playing, we check if it's paused with Mix_PausedMusic. If it's paused, we resume the music with Mix_ResumeMusic and if it's not we pause it with Mix_PauseMusic. When we press the 0 key we stop the music with Mix_HaltMusic.