sound-moduleFollowing up on last week’s “Goodbye Globals!” tutorial, today we’ll discuss how to manage audio files between scenes in a non-global method. In most apps, certain audio files are needed throughout the app while other audio files are only required in a specific scene. You may also encounter an instance where a particular audio file is loaded in one scene but “overlaps” into another scene, which can complicate the cleanup process.

So how do you manage this? Well, you could load your common sounds in main.lua and provide global handles to them, allowing them to be played from any scene. However, we always encourage you to avoid globals, and fortunately you can use the same method discussed last week to create your own “global” space that isn’t actually global.


Creating the “sfx.lua” Module

The first step is creating a module named sfx.lua to handle your audio. The first few lines are basic enough: we’ll require the myData.lua module as discussed last week (which stores some settings and other data), and then create a namespace to hold some variables.

local myData = require( "mydata" )
local sfx = {}  --create the main Sound Effects (sfx) table.
return sfx

Keep in mind that the first time a module is require‘d by Lua, its main chunk is executed only once. Things that you want to set/configure at the beginning can be done in the core code, for example:

sfx.boomSound = audio.loadSound( "audio/explosion2.wav" )
sfx.bigBoomSound = audio.loadSound( "audio/explosion.wav" )
sfx.killMeSound = audio.loadSound( "audio/idied.wav" )
sfx.missileSound = audio.loadSound( "audio/missile.wav" )
sfx.blasterSound = audio.loadSound( "audio/scifi048.wav" )
sfx.bossSound = audio.loadSound( "audio/scifi026.wav" )
sfx.shieldsSound = audio.loadSound( "audio/shields.wav" )

Here, you’re simply creating handles for the sounds and telling the audio API to load each sound and store the handle in the sfx table. Since Lua lets you index these tables either in dot syntax or bracket syntax — sfx.boomSound or sfx[“boomSound”] — this lets you use the handle names to reference the sounds later. For example, if you require sfx.lua within a scene, you can play an audio file as follows:

audio.play( sfx.boomSound )

Furthermore, you can extend this method to provide additional features. Let’s say you want to set up some additional audio parameters like these:

audio.reserveChannels( 5 )
masterVolume = audio.getVolume()
audio.setVolume( 0.80, { channel = 1 } )  --music track
audio.setVolume( 0.66, { channel = 2 } )  --boss sound
audio.setVolume( 1.0,  { channel = 3 } )  --voice overs
audio.setVolume( 1.0,  { channel = 4 } )  --alien voice
audio.setVolume( 0.25, { channel = 5 } )  --weak explosion

You can easily create an init function that you can call from the sfx table:

sfx.init = function()
   audio.reserveChannels(5)
   sfx.masterVolume = audio.getVolume()  --print( "volume "..masterVolume )
   audio.setVolume( 0.80, { channel = 1 } )  --music track
   audio.setVolume( 0.66, { channel = 2 } )  --boss sound
   audio.setVolume( 1.0,  { channel = 3 } )  --voice overs
   audio.setVolume( 1.0,  { channel = 4 } )  --alien voice
   audio.setVolume( 0.25, { channel = 5 } )  --weak explosion
end

Now, masterVolume is stored in our sfx table. And, if you want to defer these actions until later, you can.

Another interesting trick is to make our own version of audio.play that will honor your app’s sound on/off settings. For example, if you find yourself coding many checks like this…

if ( settings.soundOn ) then
   audio.play( "beep" )
end

…we can avoid it by creating our own function called sfx.play. It will work similarly to audio.play but it will honor your sound settings:

sfx.play = function( handle, options )

   if ( myData.settings and myData.settings.soundOn == false ) then
      --your settings dictate NOT to play this sound
      return false
   end

   --otherwise, one of three things is true:
   --1. myData.settings is nil. You haven't set up control, so play the sound.
   --2. myData.settings.soundOn is nil. You have settings, but not a soundOn flag.
   --   So, play the sound since you haven't set up control.
   --3. soundOn is true, which means you want to play the sound. So, play it!
   audio.play( handle, options )

end

Loading Scene-Specific Sounds

Using audio along with Storyboard creates a few challenging situations, including:

  • Loading sounds in the scene’s “main chunk” will cause problems if the sounds are disposed of — which they should be! — because the only way to completely “reload” them is to remove and recreate the scene.
  • The createScene event doesn’t necessarily load every time the scene loads, and loading large sounds may delay transitions.
  • The enterScene event will fire every time, but sounds won’t load until the scene is completely on the screen.

Given that the enterScene and exitScene events happen in pairs, enterScene is probably the best place to load scene-specific sounds. You can then dispose of them using audio.dispose() in the exitScene event (don’t forget to nil out the handle!).

But wait… what if your sound is still playing (and can’t just be abruptly stopped) when your app tries to go to a new scene? In the next scene, you no longer have access to the audio handle, so proper cleanup and disposal is tricky. Fortunately, this can be solved by loading the sound into the sfx table and using an anonymous function to dispose it on the onComplete phase. Consider this code:

local sfx = require( "sfx" )

-- forward declare the handle
sfx.longsound = nil

function scene:createScene( event )
   local group = self.view

   local background = display.newRect( 0, 0, display.contentWidth, display.contentHeight )
   background.x = display.contentCenterX
   background.y = display.contentCenterY
   group:insert( background )

   local function leaveScene(event)
      if ( event.phase == "ended" ) then
         storyboard.gotoScene( "b" )
      end
   end

   local button = display.newRect( 100,100,100,100 )
   group:insert( button )
   button:setFillColor( 255,0,255 )
   button:addEventListener( "touch", leaveScene )

end

function scene:enterScene( event )
    sfx.longsound = audio.loadSound("audio/mirv_missiles_online.wav")
    audio.play( sfx.longsound, { onComplete = function() 
                                      audio.dispose( sfx.longsound ) 
                                      end } )
end

Since Lua lets you write “anonymous functions” that onComplete events can call, you can use this method to dispose of an audio file after it’s finished. Notice that we still need to forward-declare the sound handle in the sfx table before it actually gets used.

You of course would play the sound where appropriate for your app.


In Summary…

Hopefully you can see the benefits of handling audio in a module, especially if you’re using Storyboard or another scene manager utility. We can’t emphasize enough that audio must be carefully and properly managed — and disposed of! — to prevent memory leaks and other erratic behavior. Using the module method to “isolate” your audio is one technique of doing exactly this.

  1. Rob, I don’t get your last example. I know it’s just a snippet, but it looks like you’re playing that audio file again when you’re ready to leave the scene — which seems odd.

    So elsewhere in the code when you want that sound to play you’re just using audio.play() without the anonymous onComplete function, yes?

    If you load a sound in enterScene then typically you unload it in exitScene, which isn’t happening in this case — and it’s being played again?

    As I said, I don’t get it. :)

    Jay

  2. I see the confusion. I was just testing to make sure I could change scenes and it would clean up properly. You of course play the sound where appropriate and it will self-dispose of. I’ll tweak the text to clear it up.

  3. Just for confirmation, it seems like that the audio.play onComplete will dispose the audio even if the scene has changed and the audio is not complete in playing yet?

  4. @maz, are you referring to resampling from like 44khz to 11khz? or something else?

    @austin, if you put the audio.dispose in the onComplete it will dispose of the sound when it’s done, regardless of the scene change. The problem people run into is that sometimes they will have a long sound like a voice over that once they change scenes, they can’t dispose of. This technique will take care of that, but it can create other problems.

  5. Hi Guys. I’m in a situation that I have:

    Intro.lua [0]
    Menu.lua [1]
    Options.lua [1.1]
    Levels.lua [1.2]
    Level1.lua [1.2.1]
    Level2.lua [1.2.2]

    well, When I go from intro.lua to Options.lua, I want to start a background sound with loop = -1

    So I can go from Menu to Options and from Menu to Levels and I want Audio to keep playing. Even if I return to menu again.

    But, If I go to Level1 or Level2, I want to stop the background music and start the game music.

    The way presented here, I understand I need to stop the music every time I change the scene.

    Is there a way to do the way I want? If yes, do you have a simple example?
    I don’t know, maybe making the audio handler a super global variable across scenes? (I don’t know if it is possible)…

    • You certainly can work this to do what you want. We were just presenting one way of doing this. You can always check to see if your background music channel is playing when your enterScene() function fires and if it’s not start it, if it is let it be.

  6. Hi Rob,
    i am working on an app, where i will be using many audio files. i am using audio.loadSound() and audio.pause() for the audio files . But my problem s , my app is working for 7 to 10 times properly , but after that its get stuck at some scene. At that point of time , the audio files are not playing and the app is not working . Pls suggest me some solution .

    • purgeScene() will not do anything with sounds. removeScene() will cause all of the pointers to the audio handles to be destroyed but it will not release the memory. You have to specifically call audio.dispose on any audio that you load in that scene.

  7. Trying to get this to work for a few sounds that I will use on several scenes. So far it is giving me trouble.
    This is my sfx.lua module with one of the sounds:

    local myData = require(“myData”)
    local sfx = {} –create main SFX Table
    return sfx
    sfx.clickSound = audio.loadSound( “sfx/click.mp3″ )

    Then in the first scene of my game I require the sfx module, declare the handle and then try to play the sound.

    local sfx = require(“sfx”)

    local sfx.clickSound = nillocal function buttonPressed(event)
    audio.fadeOut({ channel=0, time=3000 } )
    local id = event.target.id
    print (“Hello!”, id)
    if id == “startButton” then
    print (“start Button has been triggered”)
    audio.play( sfx.clickSound )
    storyboard.gotoScene( “page01″, “fade” )
    end
    end

    It’s not happy.

  8. Thank you! That was it. Sfx module is working now.

    Audio still fades on scene changes. working on that now. Need it to go across a few scene changes, if possible.

Leave a Reply

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

You may use these HTML tags and attributes: <a href="" title="" rel=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>