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!
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.
The first method is to use the audio.loadSound()
command. This loads and
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.
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" )
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" )
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.
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
:
local backGroup local mainGroup local uiGroup local explosionSound local fireSound local function updateText() livesText.text = "Lives: " .. lives scoreText.text = "Score: " .. score end
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.
With the sounds loaded, we can now play them with audio.play()
whenever they’re needed:
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
onCollision()
function, so let’s add an audio.play()
command directly after the died = true
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
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
game.lua
file.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
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:
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
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
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.
main.lua
file. Open that file in your chosen editor and, before the composer.gotoScene( "menu" )
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 )
1
. While reserved, no audio file will play on the channel unless we explicitly command it to.
1
. This is sometimes necessary when you obtain audio files from 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 0.5
)
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
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.
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 )
-- 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.
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.
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 |
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 |
audio.dispose() | Releases audio memory associated with a handle. |