Chapter 7 — Sounds and Music

Previous | Next

Sound effects and background music are an important part of the gameplay experience. Proper use of these components can turn a boring game into a riveting adventure!

Preloading and Streaming

There are two ways to load audio in your Solar2D app. Which one you use typically depends on how the audio file will be utilized.

Preloading

The first method is to use the audio.loadSound() command. This loads and pre-processes the entire audio file, after which it can be played on demand. For instance:

local explosionSound = audio.loadSound( "explosion.wav" )

Once loaded, the sound can be played as many times as needed using the audio.play() command along with the audio handle you created via audio.loadSound().

This is a crucial aspect to understand — you do not play an audio file by directly specifying the file name. Instead, specify the handle variable assigned to audio.loadSound().

For example, if our game has four objects explode simultaneously and each requires the explosion.wav sound to be played, we could issue these commands:

audio.play( explosionSound )
audio.play( explosionSound )
audio.play( explosionSound )
audio.play( explosionSound )

In other words, there is no need to preload the same audio file multiple times with audio.loadSound() — using the commands above, the explosion.wav sound will play four times and, by default, each instance will be assigned to a distinct audio channel. Then, once each instance has finished playing, the audio system will release/clear its channel so that another sound can be played upon it.

The audio.loadSound() method is very convenient, but if you load large audio files or a considerable number of audio files at the same time, there may be a noticeable pause/skip as they load. Thus, if you need to load a large audio file such as a background music track, it's usually better to use the streaming method discussed in the next section.

Streaming

The second method to load audio into your app is audio.loadStream(). This will gradually load and process small chunks of the audio file as needed. This command is best used in situations where possible latency will not have a critical impact upon the usability of the app. Streaming does not use as much memory, so it's usually the best choice for large audio files such as background music.

local backgroundMusic = audio.loadStream( "musicTrack1.wav" )
Note

Unlike audio.loadSound(), audio files loaded with audio.loadStream() can only be played on one channel at a time. If you need the same audio file to stream on multiple channels, you'll need to load two distinct audio handles, for instance:

local backgroundMusic1 = audio.loadStream( "musicTrack1.wav" )
local backgroundMusic2 = audio.loadStream( "musicTrack1.wav" )

Adding Sound Effects

Including Audio Files

Let's add sound effects to our game! To begin, you'll need to download the sample audio files, courtesy of Eric Matyas. Within the audio subfolder of this chapter's source files, you'll find the following audio files:

File Usage
Escape_Looping.wav Music for the menu scene.
explosion.wav Sound effect when an asteroid is hit.
fire.wav Sound effect when the ship fires a laser.
80s-Space-Game_Looping.wav Main soundtrack for the gameplay.
Midnight-Crawlers_Looping.wav Music for the high scores scene.

For this project, copy the entire audio subfolder and all of its contents into your StarExplorer project folder. If you're planning to use several audio files in a game, it's helpful to keep them organized in a subfolder.

Loading Sounds

First, we need to load the sounds. Since our sound effects are only going to occur during gameplay, we can load them in game.lua:

  1. First, in the scene-accessible code area where you've already pre-declared some variables, add the following forward references:
local backGroup
local mainGroup
local uiGroup

local explosionSound
local fireSound


local function updateText()
    livesText.text = "Lives: " .. lives
    scoreText.text = "Score: " .. score
end
  1. Next, locate the scene:create() function and, directly before its end line, add the following highlighted commands:
    ship:addEventListener( "tap", fireLaser )
    ship:addEventListener( "touch", dragShip )

    explosionSound = audio.loadSound( "audio/explosion.wav" )
    fireSound = audio.loadSound( "audio/fire.wav" )
end

Now, when the scene first loads, the sound files will be loaded into the variable handles explosionSound and fireSound.

Notice that instead of specifying just the file name, we append it with audio/ because our audio files are located inside the audio subfolder.

Playing Sounds

With the sounds loaded, we can now play them with audio.play() whenever they're needed:

  1. The explosion sound should play whenever a laser hits an asteroid. That event is detected in the onCollision() function, so let's add an audio.play() command in the first conditional block, directly following the display.remove() commands that remove the laser and asteroid:
local function onCollision( event )

    if ( event.phase == "began" ) then

        local obj1 = event.object1
        local obj2 = event.object2

        if ( ( obj1.myName == "laser" and obj2.myName == "asteroid" ) or
             ( obj1.myName == "asteroid" and obj2.myName == "laser" ) )
        then
            -- Remove both the laser and asteroid
            display.remove( obj1 )
            display.remove( obj2 )

            -- Play explosion sound!
            audio.play( explosionSound )

            for i = #asteroidsTable, 1, -1 do
                if ( asteroidsTable[i] == obj1 or asteroidsTable[i] == obj2 ) then
                    table.remove( asteroidsTable, i )
                    break
                end
            end
  1. The explosion sound should also play whenever an asteroid hits the ship. That event is detected by the second conditional clause of the onCollision() function, so let's add an audio.play() command directly after the died = true command:
        elseif ( ( obj1.myName == "ship" and obj2.myName == "asteroid" ) or
                 ( obj1.myName == "asteroid" and obj2.myName == "ship" ) )
        then
            if ( died == false ) then
                died = true

                -- Play explosion sound!
                audio.play( explosionSound )

                -- Update lives
                lives = lives - 1
                livesText.text = "Lives: " .. lives

                if ( lives == 0 ) then
                    display.remove( ship )
                    timer.performWithDelay( 2000, endGame )
                else
                    ship.alpha = 0
                    timer.performWithDelay( 1000, restoreShip )
                end
            end
        end
    end
end
  1. Finally, let's add the sound effect for firing lasers. At the beginning of the fireLaser() function, add another audio.play() command:
local function fireLaser()

    -- Play fire sound!
    audio.play( fireSound )

    local newLaser = display.newImageRect( mainGroup, objectSheet, 5, 14, 40 )
    physics.addBody( newLaser, "dynamic", { isSensor=true } )
    newLaser.isBullet = true
    newLaser.myName = "laser"

    newLaser.x = ship.x
    newLaser.y = ship.y
    newLaser:toBack()

    transition.to( newLaser, { y=-40, time=500,
        onComplete = function() display.remove( newLaser ) end
    } )
end
  1. Save your modified game.lua file.

Adding Background Music

To enhance the game further, let's add background music. In the overall span of our game, there are three scenes. We could play the same music for each scene, but it's better to play different audio tracks in each scene and set the tone for the action. For instance, in the menu scene, a more passive track can play, but when we get to the game scene where the action gets intense, it helps to have a faster-paced audio track. Finally, when the game is over, you may want something more solemn.

Loading Music

As discussed above, background music files tend to be large, so it's best to use audio.loadStream(). Let's use an approach similar to the sound effects:

  1. In the scene-accessible code area of game.lua where you've already declared the two forward references for sounds, add a forward reference for the music track:
local explosionSound
local fireSound
local musicTrack
  1. Now, near the end of the scene:create() function where you called audio.loadSound() to load both sound effects, add an audio.loadStream() command to begin streaming the music on the musicTrack variable you just declared:
    explosionSound = audio.loadSound( "audio/explosion.wav" )
    fireSound = audio.loadSound( "audio/fire.wav" )
    musicTrack = audio.loadStream( "audio/80s-Space-Game_Looping.wav")
end

Playing Music

Now it's time to play the music! This time we're going to step deeper into audio setup with channel management. Basically, for our sound effects, we simply let the audio library pick a free channel on which to play any new sound instance. For music however, it's often useful to reserve a specific channel and play all of the background music on that channel — after all, it's unlikely that you'll want to have multiple music files playing at the same time, overlapping and audibly conflicting with each other. By reserving one dedicated channel for music, we can use it for all of the background music throughout the game.

To accomplish this, we need to provide a bit more information to our audio.play() command for the music, and also do a little extra work in preparation for using a dedicated channel.

  1. First, to reserve a channel for music throughout the game, let's add a simple command to the main.lua file. Open that file in your chosen editor and, before the composer.gotoScene( "menu" ) command, add the following:
local composer = require( "composer" )

-- Hide status bar
display.setStatusBar( display.HiddenStatusBar )

-- Seed the random number generator
math.randomseed( os.time() )

-- Reserve channel 1 for background music
audio.reserveChannels( 1 )

Basically, the audio.reserveChannels( 1 ) command tells the Solar2D audio library to reserve channel 1. While reserved, no audio file will play on the channel unless we explicitly command it to.

  1. Next, let's reduce the overall volume of channel 1. This is sometimes necessary when you obtain audio files from third-party sources where you didn't have any control of the sample volume. It can also be useful to control channel volume if you want to eventually build in functionality that lets the user control the volume level of the game music, or even mute it completely.

Below the lines you just added, include the following:

-- Reserve channel 1 for background music
audio.reserveChannels( 1 )
-- Reduce the overall volume of the channel
audio.setVolume( 0.5, { channel=1 } )

This essentially tells the audio system to play back any audio file on channel 1 at 50% volume (0.5). You may want to adjust this in your game if you feel the music is either too loud or too quiet in relation to the sound effects.

  1. Now, save main.lua and return to game.lua in your editor. We will begin playing the music when the scene comes fully on screen, so in the "did" phase condition of the scene:show() function, add the following lines:
-- show()
function scene:show( event )

    local sceneGroup = self.view
    local phase = event.phase

    if ( phase == "will" ) then
        -- Code here runs when the scene is still off screen (but is about to come on screen)

    elseif ( phase == "did" ) then
        -- Code here runs when the scene is entirely on screen
        physics.start()
        Runtime:addEventListener( "collision", onCollision )
        gameLoopTimer = timer.performWithDelay( 500, gameLoop, 0 )
        -- Start the music!
        audio.play( musicTrack, { channel=1, loops=-1 } )
    end
end

This audio.play() command simply starts playing the music. It's similar to how we play the sound effects except that it includes a Lua table as the second argument containing options for the command. Specifically, channel=1 instructs the audio library to explicitly play the music on channel 1 and loops=-1 tells the audio system to repeat (loop) the file indefinitely.

These additions should get music playing in your app. Make sure that you save your modified main.lua and game.lua files and then relaunch the Simulator. Play a game and you should now hear a looping music track in addition to the sound effects.

Stopping Music

Unlike sound effects which are typically short and get cleared from their channel upon completion, streaming music should usually be stopped at an appropriate time when you're about to leave the scene. This can be easily handled in the "did" phase condition of the scene:hide() function. Locate this block within game.lua and add the audio.stop( 1 ) command:

-- hide()
function scene:hide( event )

    local sceneGroup = self.view
    local phase = event.phase

    if ( phase == "will" ) then
        -- Code here runs when the scene is on screen (but is about to go off screen)
        timer.cancel( gameLoopTimer )

    elseif ( phase == "did" ) then
        -- Code here runs immediately after the scene goes entirely off screen
        Runtime:removeEventListener( "collision", onCollision )
        physics.pause()
        -- Stop the music!
        audio.stop( 1 )
        composer.removeScene( "game" )
    end
end

That's it! When the scene goes fully off screen, the music playing on channel 1 will stop, clearing the way for a different music track to play on the channel in the next scene.

Disposing Audio

Whether preloaded or streaming, audio takes up memory and it's a resource that is not automatically managed or cleaned up by Composer. As a result, there's one final important step in this chapter: disposing audio. This is where the scene:destroy() function comes in handy, since it gets triggered as a result of calling composer.removeScene() or when Composer itself destroys the scene.

In your game.lua file, locate the scene:destroy() function near the bottom. Within it, add three audio.dispose() commands as shown here:

-- destroy()
function scene:destroy( event )

    local sceneGroup = self.view
    -- Code here runs prior to the removal of scene's view
    -- Dispose audio!
    audio.dispose( explosionSound )
    audio.dispose( fireSound )
    audio.dispose( musicTrack )
end

Using these commands, we effectively release the memory taken up by the audio file.

Similar to audio.play(), notice that we supply an audio handle to the audio.dispose() command, for example explosionSound. You should not attempt to dispose audio by simply indicating an audio file name.

Extra Credit

Although we've done it for you in this chapter's source files, challenge yourself to implement music inside the other two scenes, using the same audio techniques and scene event concepts that you learned above!

Scene Music File
menu.lua Escape_Looping.wav
highscores.lua Midnight-Crawlers_Looping.wav

Chapter Concepts

We've covered several concepts in this chapter, all related to audio:

Command/Property Description
audio.loadSound() Loads an entire file completely into memory and returns a reference to the audio data.
audio.loadStream() Loads (opens) a file to be read as streaming audio.
audio.reserveChannels() Reserves a certain number of channels so they won't be automatically assigned to play on.
audio.setVolume() Sets the volume either for a specific channel, or sets the master volume.
audio.play() Plays the audio specified by the audio handle on a channel.
audio.stop() Stops playback on a channel (or all channels) and clears the channel(s) so they can be played on again.
audio.dispose() Releases audio memory associated with a handle.