Moai Tutorial Part 6: Who put the bomp in the Moai, Moai, Moai. Playing audio in… you guessed it… Moai

Moai Tutorial Part 6: Who put the bomp in the Moai, Moai, Moai. Playing audio in… you guessed it… Moai

17. September 2012

 

In this tutorial, we are going to look at how to play audio using Moai.  This is going to make heavy use of moaigui, so if you haven’t already, you may want to check the prior tutorial in this series.

 

As always, let’s jump right in to the code.  You may notice that the code order is starting to seem a little… odd. This is because I am grouping code mostly in order of newness, as opposed to how it would logically be laid out.  If you are thinking “I wouldn’t order things this way!”, well.. neither would I, normally.

 

screenWidth = MOAIEnvironment.screenWidth
screenHeight = MOAIEnvironment.screenHeight
if screenWidth == nil then screenWidth =1280 end
if screenHeight == nil then screenHeight = 800 end


MOAISim.openWindow("Window",screenWidth,screenHeight)

viewport = MOAIViewport.new()
viewport:setSize(screenWidth,screenHeight)
viewport:setScale(screenWidth,screenHeight)

package.path = './moaigui/?.lua;' .. package.path
require "gui/support/class"
local moaigui = require "gui/gui"
local resources = require "gui/support/resources"
local filesystem = require "gui/support/filesystem"
local inputconstants = require "gui/support/inputconstants"
local layermgr = require "layermgr"


local gui = moaigui.GUI(screenWidth,screenHeight)


gui:addToResourcePath(filesystem.pathJoin("moaigui/resources", "fonts"))
gui:addToResourcePath(filesystem.pathJoin("moaigui/resources", "gui"))
gui:addToResourcePath(filesystem.pathJoin("moaigui/resources", "media"))
gui:addToResourcePath(filesystem.pathJoin("moaigui/resources", "themes"))

layermgr.addLayer("gui",99999, gui:layer())
gui:setTheme("basetheme.lua")
gui:setCurrTextStyle("default")

function onPointerEvent(x, y)
    gui:injectMouseMove(x, y)
end

function onMouseLeftEvent(down)
    if(down) then
        gui:injectMouseButtonDown(inputconstants.LEFT_MOUSE_BUTTON)
    else
        gui:injectMouseButtonUp(inputconstants.LEFT_MOUSE_BUTTON)
    end
end

function onTouchEvent(eventType,idx,x,y,tapCount)
    --gui:injectTouch(eventType,idx,x,y,tapCount)
    onPointerEvent(x, y)
    if (MOAITouchSensor.TOUCH_DOWN == eventType) then
        onMouseLeftEvent(true)
    elseif (MOAITouchSensor.TOUCH_UP == eventType) then
        onMouseLeftEvent(false)
    end
end


if MOAIInputMgr.device.pointer then
    MOAIInputMgr.device.pointer:setCallback(onPointerEvent)
    MOAIInputMgr.device.mouseLeft:setCallback(onMouseLeftEvent)
else
    MOAIInputMgr.device.touch:setCallback(onTouchEvent)
end


function onButtonClick(event,data)
    if(string.find(buttonPlay:getText(),"Play") ==1) then
        song1:play()
        buttonPlay:setText("Stop, that's creeping me out!")

        function checkSongDone()
            local song = song1
            while song1:isPlaying() do coroutine:yield() end

            song1:stop()
            buttonPlay:setText("Play Audio File using Untz")
        end

        if not checkLoop:getChecked() then
            songPlayingThread = MOAICoroutine.new()
            songPlayingThread:run(checkSongDone)
        end
    else
        song1:stop()
        buttonPlay:setText("Play Audio File using Untz")
    end
end

function onButtonRewind(event,data)
    if song1:isPlaying() then
        local pos = song1:getPosition()
        local length = song1:getLength()
        local tenPercent = (length * 1.10) - length
        pos = pos - tenPercent
        if(pos > 0) then
            song1:setPosition(pos)
        end
    end
end

function onButtonFastForward(event,data)
    if song1:isPlaying() then
        local pos = song1:getPosition()
        local length = song1:getLength()
        local tenPercent = (length * 1.10) - length
        pos = pos + tenPercent
        if(pos < length) then
            song1:setPosition(pos)
        end
    end
end

function onCheckboxChanged()
    song1:setLooping(checkLoop:getChecked())
end

function onVolumeChanged()
    MOAIUntzSystem.setVolume(sliderVolume:getCurrValue() / 100)
end



buttonPlay = gui:createButton()
buttonPlay:setPos(0,0)
buttonPlay:setDim(100,25)
buttonPlay:setText("Play Audio File using Untz")
buttonPlay:registerEventHandler(buttonPlay.EVENT_BUTTON_CLICK,nil,onButtonClick)

checkLoop = gui:createCheckBox()
checkLoop:setPos(0,26)
checkLoop:setDim(100,10)
checkLoop:setText("Loop?")
checkLoop:registerEventHandler(checkLoop.EVENT_CHECK_BOX_STATE_CHANGE,nil,onCheckboxChanged)

labelVolume = gui:createLabel()
labelVolume:setText("Volume")
labelVolume:setPos(0,36)
labelVolume:setDim(30,10)

sliderVolume = gui:createHorzSlider()
sliderVolume:setPos(30,36)
sliderVolume:setDim(70,10)
sliderVolume:setRange(0,100)
sliderVolume:setCurrValue(50)
sliderVolume:setValueDisplayLoc(sliderVolume.VALUE_DISPLAY_RIGHT)
sliderVolume:registerEventHandler(sliderVolume.EVENT_SLIDER_VALUE_CHANGED,nil,onVolumeChanged)

buttonRewind = gui:createButton()
buttonRewind:setPos(0,50)
buttonRewind:setDim(50,10)
buttonRewind:setText("Rewind")
buttonRewind:registerEventHandler(buttonRewind.EVENT_BUTTON_CLICK,nil,onButtonRewind)

buttonFastForward = gui:createButton()
buttonFastForward:setPos(51,50)
buttonFastForward:setDim(50,10)
buttonFastForward:setText("Fast Forward")
buttonFastForward:registerEventHandler(buttonFastForward.EVENT_BUTTON_CLICK,nil,onButtonFastForward)

MOAIUntzSystem.initialize ()
MOAIUntzSystem.setVolume(0.5)
song1 = MOAIUntzSound.new()
song1:load("demongirls.ogg")

 

If we run the application( in the Moai-untz host ), we see:

 

image

 

Click the “Play Audio File using Untz” button and the song will play, the slider increases and decreases the volume, the loop check box I bet you can guess what it does.  Rewind and Fast Forward change the playback position by 10% at a time.

 

This looks like a wall of code, but don’t worry, it’s not all that bad.  In reality, most of it is maoigui code that we covered earlier.  We use a couple of new controls ( the check box and slider ), but nothing in the UI code should be all together new.  Most of our new logic is in the click handler methods, so let’s start there.

 

function onButtonClick(event,data)
    if(string.find(buttonPlay:getText(),"Play") ==1) then
        song1:play()
        buttonPlay:setText("Stop, that's creeping me out!")

        function checkSongDone()
            local song = song1
            while song1:isPlaying() do coroutine:yield() end

            song1:stop()
            buttonPlay:setText("Play Audio File using Untz")
        end

        if not checkLoop:getChecked() then
            songPlayingThread = MOAICoroutine.new()
            songPlayingThread:run(checkSongDone)
        end
    else
        song1:stop()
        buttonPlay:setText("Play Audio File using Untz")
    end
end

This is the code that is executed if the brilliantly named buttonPlay button is clicked.  The first thing we do is check to see if the button’s text starts with “Play”, which if it does, that means we are currently not playing a song.  Therefore we start playing our song ( aptly named song1 ) with a call to the play() method.  We then update our button’s text to indicate it’s new status.  The next little bit of code might seem a bit confusing at first, but it is pretty powerful.

 

First we are declaring a nested function checkSongDone().  The next line is where the magic happens.  It’s a while loop that loops until the condition isPlaying() becomes false.  Each pass through the loop we call coroutine:yield().  So, what the heck was that all about? Well… this is going to be a bit tricky to explain, but I will do my best.

 

If you are an old fart like me, you may remember multitasking in the pre-Win95 pre-emptive multitasking days.  Essentially you ran multiple programs at the same time by giving each one a chunk of processing time and a well behaved program was supposed to yield control when it wasn’t busy.  Essentially the operating system switched between running programs and said “ok, your turn” and a well behaved program would quickly exit if it was done doing whatever it needed to do.  Well, in a nutshell that is what is happening here.

 

A coroutine is sort of the Moai version of a thread.  Using coroutines, you are able to run code in parallel, so while our game loop is still running, the function checkSongDone is blissfully churning away in the background checking to see if the song is done.  Thing is, until the song is actually done, the coroutine doesn’t really have anything else to do, so it calls yield().  It is not until that condition is true ( isSongPlaying returns false ) will the function continue execution.  Once this happens we (somewhat redundantly) call song1:stop() then update the button’s text to “Play Audio…”.  The end result of this whole chunk of code is, we essentially created an on song end handler, so when the song is done playing, the text of the button will update.

 

The next chunk of code checks the checkbox checkLoop, which if checked indicates that we want the song to loop continuously.  If it is not checked, that means we want to run our coroutine, which we create with a call to MOAICoroutine.new(), then run with the run() method, passing in the function to run.

 

If the button’s text however indicates that the song is playing, in which case the button acts like a STOP button, simply stopping playback ( even if loop is checked ) and resetting back to the “Play…” text.  I encourage you to look deeper in to Coroutines, they are a very powerful feature of Lua. MOAICoroutine itself is only a thin wrapper over Lua’s coroutine abilities.

 

Also, you may have noticed MOAIThread in the examples and wondered when to using MOAIThread instead of MOAICoroutine.  The short answer is, you dont!  The more accurate and useful short answer is, MOAIThread *IS* MOAICoroutine.  It was renamed and the documentation just hasn’t caught up yet.  Well at least, it hadn’t at the point I wrote this.

 

TL;DR, coroutines are cool, learn about them, use them, love them.

 

function onButtonRewind(event,data)
    if song1:isPlaying() then
        local pos = song1:getPosition()
        local length = song1:getLength()
        local tenPercent = (length * 1.10) - length
        pos = pos - tenPercent
        if(pos > 0) then
            song1:setPosition(pos)
        end
    end
end

function onButtonFastForward(event,data)
    if song1:isPlaying() then
        local pos = song1:getPosition()
        local length = song1:getLength()
        local tenPercent = (length * 1.10) - length
        pos = pos + tenPercent
        if(pos < length) then
            song1:setPosition(pos)
        end
    end
end

This is the code that executes if you hit the Fast Forward or Rewind buttons.  Basically it gets the current position of the song ( playback-wise ) using getPosition() and the length of the song using getLength().  Next we calculate what 10% of the length is, and either add or subtract that amount from the current position depending if we are fast forwarding or rewinding.  Next we check sure to make sure the position is valid after being modified and if it is, we change the position of the song using setPosition().  End result, pressing Fast forward advances the song by 10% of it’s length, while hitting rewind moves it back 10%.

 

function onCheckboxChanged()
    song1:setLooping(checkLoop:getChecked())
end

This is the code that executes if the checkbox checkLoop is checked or unchecked.  It simply toggles the value of setLooping based off the if the checkbox is checked, using getChecked().

 

function onVolumeChanged()
    MOAIUntzSystem.setVolume(sliderVolume:getCurrValue() / 100)
end

This is the code that is executed if the slider sliderVolume is changed.  This uses the MOAIUntzSystem singleton to change the global volume level using setVolume().  setVolume takes a value from 0.0 to 1.0, while the sliders value returned by getCurrValue() is from 0 to 100, so we divide it by 100.

 

So then, what the heck is an Untz?  It’s the included cross platform audio system.  You also have the option to use FMOD, a commercial audio solution.  Untz does the trick though, works on Android and iOS.  There is a very important caveat… if you are going to be running using the UNTZ audio library you need to use the Moai-untz host!!! when running on the PC.  You will get exceptions if you try to call MOAIUntzSystem from the normal Moai host.  The Moai-untz host is in the same directory as the Moai host.

 

Most of the UI code is pretty much identical to what you’ve seen already, so I wont really go into it, that leaves:

 

MOAIUntzSystem.initialize ()
MOAIUntzSystem.setVolume(0.5)
song1 = MOAIUntzSound.new()
song1:load("demongirls.ogg")

This code first initializes the MOAIUntzSystem singleton.  Then it sets the global volume to 50%.  Next we create our song song1 with a call to MOAIUntzSound.new().  Finally load the song ( in the same directory as main.lua ) with a call to load().

 

You might be asking yourself “what sound formats does Untz support?”.  That’s a good question with a tricky answer.  Basically the safe answer is ogg and wav.  The not so safe answer is, it depends on the platform.  For example, iOS, Mac and Windows will work with mp3, but Android wont.  Beyond WAV and OGG, the file is just passed to the native system for handling, so if the platform supports it, Moai supports it.

 

But if you really want to be cross platform, just encode everything in to ogg.  In this day of increasingly wealthy lawyers, the last thing we want to do is deal with a patent encumbered format like mp3!

 

Oh yeah, and the music… it’s seriously creepy stuff and I downloaded it from here.  Obviously you can substitute any song you want from your Justin Beiber collection, just make sure it’s not an MP3 if you are running on Android.  Of course, you can convert from just about any audio format to just about any other audio format using the free and amazing Audacity.  I do get 10% of all proceeds for saying that.  I may need a new business model.

 

Programming , ,







blog comments powered by Disqus