This is part of a series of articles that go over the steps I’m taking to learn Flash CS3. The project’s goal is to develop an RPG similar to those published by Square in the mid/late 1990’s. (Secret of Mana, Final Fantasy VI, etc..)
Audio wasn’t exactly where I was planning to go next with this project. But inspiration called, so I answered. Every great RPG has great music. Nobuo Uematsu is almost a household name among jRPG fans. Audio can make or break any emotional event and really adds depth to any scene.
Milestone – Background Audio
User Story: I will hear music and background environmental sounds.
Acceptance Criteria:
- There should be more than 1 track of audio playing
- Some audio should loop
- Some audio should be triggered by an event
I was really hoping this would be a 15 minute task. I mean, Flash is built as a multimedia tool. It can import audio, yadda yadda.
The two primary problems I had were somewhat related:
- Flash cannot dynamically load .wav files.
- MP3 files have “gaps” making seamless loops tough.
This made the task of having seamless audio loops in Actionscript 3 a bit of an ordeal.
First I had to convert my flawless .wav files into less-than-flawless .mp3. Then I had to somehow account for the audio gaps that would show up during playback. MP3 is a pissy format that really isn’t suitable for gapless audio.
My solution wasn’t elegant, but did the trick. I’d setup a timer loop that would look at the position of the playhead on the current track. If the playhead was past the length-endOffset, I’d return it to the start+startOffset. I had to play with the milliseconds on the offset variables to get it decent and will likely to continue to tweak this in the future.
My SoundLayer.as file does my heavy lifting taking in the following arguments:
- url //filename of the mp3 to load
- playnow //boolean to determine if the sound should play on startup
- loop // boolean to determine if the sound should loop
- seamless //boolean – additional logic should be applied
SoundLayer.as
public class SoundLayer extends Sprite{ public var channel:SoundChannel; var bgMusicChannel:SoundChannel; var bgMusic:Sound; var bgSound :Sound var positionTimer:Timer; var startPosition:int; var endPosition:int; var playnow:Boolean; var loop:Boolean; var seamless:Boolean; var vol:int; public function SoundLayer (url:String, p:Boolean, l:Boolean, s:Boolean, v:int){ channel = new SoundChannel(); playnow = p; loop = l; seamless = s; vol=v; bgMusic = new Sound(); bgMusic.load(new URLRequest (url)); bgMusic.addEventListener(IOErrorEvent.IO_ERROR, loadError, false, 0, true); bgMusic.addEventListener(Event.COMPLETE, loadComplete,false, 0, true); } private function loadError(e:IOErrorEvent):void{ trace ("load error"); } private function loadComplete(e:Event):void{ bgSound = e.target as Sound; startPosition = 0; endPosition = bgSound.length-100; if (seamless){ startPosition = 1430; endPosition = bgSound.length-500; } if (loop){ positionTimer = new Timer(10); positionTimer.addEventListener(TimerEvent.TIMER, positionTimerHandler); positionTimer.start(); } trace ("music loaded"); var trans:SoundTransform = new SoundTransform(); trans.volume = vol; channel.soundTransform=trans; if (playnow) play(); } private function positionTimerHandler(event:TimerEvent):void { if (channel.position > endPosition) { channel.stop(); channel = bgSound.play(startPosition); } } public function play(){ channel = bgSound.play(); } }
I also created a SoundScape class that would setup several SoundLayers. For this example it would first load and startup my background music which needs to loop but doesn’t need fancy gapless logic. Then it fires up a rain sound layer with fancy gapless logic. And finally I create a layer for the Lightning with a function that randomly plays it every 15-30 seconds.
SoundScape.as
public class SoundScape extends Sprite{ var bgMusic:SoundLayer; var bgAtmosphere1:SoundLayer; var lightning:SoundLayer; var lightningFreqMin:int; var lightningFreqMax:int; var nextLightning:int; public function SoundScape (){ bgMusic = new SoundLayer("XXXXXXX.mp3", true, true, false, 1); bgAtmosphere1 = new SoundLayer("XXXXX.mp3", true, true, true, 0.25); lightning = new SoundLayer("XXXXX.mp3", false, false, false, 1); addEventListener(Event.ENTER_FRAME, loop, false, 0, true); lightningFreqMin = 15*30; lightningFreqMax = 30*30; nextLightning = Math.random()*(lightningFreqMax-lightningFreqMin); } private function loop(e:Event):void { nextLightning--; trace (nextLightning); if (nextLightning == 0){ var trans:SoundTransform = new SoundTransform(); trans.volume = Math.random(); lightning.channel.soundTransform=trans; lightning.play(); nextLightning = lightningFreqMin + (Math.random()*(lightningFreqMax-lightningFreqMin)); } } }




