Chapter 7 — Sounds and Music

Previous | Next

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

Preloading and Streaming

There are two ways to load audio in your 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 example:

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(), in this case explosionSound.

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

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

In other words, there is no need to preload any one audio file multiple times with audio.loadSound() — the explosion.wav sound will play multiple times, and each instance will be assigned to a distinct audio channel by default. Then, once an instance has finished playing, the audio system will release/clear the channel so that another sound can be played on 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 point in your app, there may be a noticeable pause/skip as they load. Thus, if you need to load a large audio file in your app, 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 with 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 for some reason 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! First, you'll need to download the sample audio files, provided courtesy of Eric Matyas. These are available in this chapter's source files here. Within the audio subfolder, 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 many audio files in a game, it's often helpful to keep them organized in a subfolder like this.

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. In the scene-accessible code area, where you've already pre-declared some variables, add the following forward references:

local explosionSound
local fireSound

Next, locate the scene:create() function and, near the bottom directly before its end line, add these commands:

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

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 all located inside the audio subfolder.

Playing Sounds

With the sounds loaded, we can now play them with audio.play() whenever they are needed. The explosion will be played whenever a laser hits an asteroid or an asteroid hits the ship. Both of these instances are detected by the onCollision() function, so we'll add an audio.play( explosionSound ) command in these two places:

  1. In the first conditional block, following the display.remove() commands that remove the laser and asteroid.
  2. In the second conditional block, following the died = true command.
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

            -- Increase score
            score = score + 100
            scoreText.text = "Score: " .. score

        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

Finally, we need to add the sound effect for shooting lasers. At the beginning of the fireLaser() function, add the audio.play( fireSound ) 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
Action!

We now have sound effects in our game! Save your modified game.lua file and then relaunch the Simulator. Play a game and you should hear both a sound when you fire lasers and an explosion sound when asteroids are hit.

Adding Background Music

To enhance the game further, let's add background music. In the course of our game, there are three scenes. You could play the same music for each scene, but it's best if you have different audio tracks for each scene and it's also a great way to set the tone for the game. For instance, in the menu scene, a more passive track can play, but when we get to the game scene where the action can get 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 larger, so it's best to use audio.loadStream(). First, however, let's add a forward reference for our music track, similar to the sound effects. In the scene-accessible code area of game.lua where you've already declared the two forward references to your sounds, add this line:

local musicTrack

Now, near the end of the scene:create() function where you called audio.loadSound() to load both sound effects, add a new command to begin streaming the music on the musicTrack variable you just declared:

    musicTrack = audio.loadStream( "audio/80s-Space-Game_Looping.wav")

Playing Music

Now it's time to play the music! This time we're going to step deeper into audio 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 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.

First, to reserve a channel for music throughout the game, we can 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:

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

This command simply tells the Corona audio library to reserve channel 1. While reserved, no audio file will play on the channel unless we explicitly command it to.

Now, 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 later 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 wrote, include the following:

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

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 an audio.play() command so that the function looks like this:

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

Action!

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 so that the function looks like this:

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

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 supplying the 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 we've outlined 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.