Advertisement

Generating Digital Audio Using SiON

by

In this tutorial I'll be showing you how to get started with SiON, an AS3 software synthesizer library which generates sound using only code.


Final Result Preview

In the end this is what we're going to obtain:

Click on the darker rectangle area to start/stop the balls movement.


Getting Necessary Files

First you need to get the SiON library. You can download it either as a SWC file or as uncompressed ActionScript files. To do this go to SiON Downloads and select the desired download method.

After you've downloaded the source code add it to your global class path.

Notice that on this page you can also download the ASDoc documentation and older versions of the library.

In this tutorial we'll make use of the well known minimalcomps library, developed by Keith Peters; if you don't have it go ahead and grab it: minimalcomps.

Also add the minimalcomps library to your global class path and let's get started.

Note: As always I'll be using FlashDevelop throughout this tutorial. You can use whatever code editor you like although I recommend sticking with FlashDevelop.


Step 1: What is SiON?

The SiON library is a software synthesizer library built in ActionScript 3.0 and works in Flash Player 10 or higher.

With SiON you can generate dynamic sounds on the run without the need of loading any audio files. Also makes it very easy to synchronize sounds with display objects (eg. object hitting a wall, explosion etc).

From the multitude of features it has I'll show you the essentials of working with it: using MML (Music Macro Language) data to generate sound, using voice presets and effectors on playing sounds, setting the tempo (BPM), panning and changing volume and lastly I'll show you how to sync sounds with display objects.


Step 2: Setting up the Project

Let's start by creating a new project. Open your code editor and create a new ActionScript 3 project.

New project

I've named my project SiON Tutorial. After this open the document class (in my case Main).

You should have something similar to this:

package 
{
	import flash.display.Sprite;
	
	[SWF(width = 550, height = 300, backgroundColor = 0x1f1f1f, frameRate = 30)]
	public class Main extends Sprite 
	{
		
		public function Main():void 
		{
			
		}
		
	}
	
}

In FlashDevelop you would probably have an init() method called when the movie is added to stage. Go ahead and modify the code so that it matches the one above.

Leave this file opened and onto the next step.


Step 3: Basic Usage

To start using SiON and hearing sound we only need to create one object: SiONDriver. This class provides the driver of SiON's digital signal processor emulator and through this class all basic operations are provided as properties (bpm, volume), methods (pause(), play(), stop(), fadeIn() and events (bmp changes, stream start and stop).

Note: Only one instance of the SiON driver class can be created at any given moment. Trying to create multiple instances will make the compiler throw an error. To get the existing instance of the SiONDrive class you can use the SiONDriver.mutex static property.

In your document (Main) class add a new private variable called _driver and instantiate it in the constructor.

package 
{
	import flash.display.Sprite;
	import org.si.sion.SiONDriver;
	
	[SWF(width = 550, height = 300, backgroundColor = 0x1f1f1f, frameRate = 30)]
	public class Main extends Sprite 
	{
		private var _driver:SiONDriver;
		
		public function Main():void 
		{
			_driver = new SiONDriver();
		}
		
	}
	
}

In order to play a sound you need to call upon the play() method of the SiONDriver object and pass as an argument an SiONData object or a MML string. For our example we'll use an MML string (as the SiONData object is actually a compiled MML string in its essence).

_driver.play('l8 cdefgab<c');

Add this line of code in the class and run the project (Ctrl+Enter if using FlashDevelop). You should now hear the notes (eight notes) from C5(or Do in octave five) to C6(or Do in octave six). But what does l8 cdefgab<c actually mean?


Step 4: Music Macro Language

Music Macro Language (or MML) is a music description language used for sequencing music. The following elements are basically in each MML implementation (including SiON):

Note: Letters from a to g correspond to musical pitches and play the corresponding note. So c, d, e, f, g, a, b would have the following equivalent on a stave:

Portative equivalent

To set the length of a note you simply append a whole number to it. Let's say you want a full c you would write c1. If you want c eight note you will write c8 (which is 8 times shorter than c1) and so on. If you don't append any number to a note, it uses the default length of 4 (a quarter).

Rest: This defines the pause between two notes. The macro for rest is r. When followed by a whole number it represents the length of the rest (example: r4 would mean a quarter rest).

Length: To specify the default length of more notes you can use l with a whole number in front of it which represents the length.

Example: l1 cdf l16 cdf

Octave: The default octave of a note (which defines the note's pitch) is the fifth octave. You can change the octave in two ways:

  • Using the o macro (followed by an integer from 0-9) in front of the note. For example o7c would play the C note in the seventh octave.
  • Using the octave increment > or decrement < macro in front of a note. For example if you want to play the C note in the fourth octave you would write: <c (remember that the default octave is the fifth one).

Sharp and flat notes: In order to play a sharp note you append a + or # to the note (e.g. b+ or b#, both having the same result) and to play a flat note you append a - (e.g. a-).

Loops: To play a loop you need to wrap the macro around square brackets (e.g. [cdb]3 would translate into cdb cdb cdb). To specify how many times a loop should play you append an integer to the text. If no number is specified the loop will play two times.

That's the basic usage of MML. To read more on the subject you can visit the wiki page here. Also visit the MML reference manual for SiON for a complete documentation on MML for SiON.

Note: For a large database of MML songs examples you can visit the MMLTalks application. Click on a song title from the list to play it. To see the MML source of a song click the rightmost button.

Getting MML source

Step 5: SiONData

So I've said that a SiONData object is actually a MML string compiled. To compile an MML string into an SiONData object you can use the compile() method of the SiONDriver class.

package 
{
	import flash.display.Sprite;
	import org.si.sion.SiONData;
	import org.si.sion.SiONDriver;
	
	[SWF(width = 550, height = 300, backgroundColor = 0x1f1f1f, frameRate = 30)]
	public class Main extends Sprite 
	{
		private var _driver:SiONDriver;
		private var _data:SiONData;
		
		public function Main():void 
		{
			_driver = new SiONDriver();
			_data = _driver.compile('l8 cdefgab&lt;c');
			_driver.play(_data);
		}
		
	}
	
}

As you can see first we use the drivers compile() method to compile the MML string into a SiONData object after which we call upon the play() method and pass the _data object as an argument. As you can see this has the same result as before when a string was used.


Step 6: Playing Multiple Sounds

So everything looks fine when playing one sound at a time but what about playing multiple sounds? Modify the class to look like the code below and let's see what happens.

package 
{
	import com.bit101.components.PushButton;
	import flash.display.Sprite;
	import flash.events.Event;
	import org.si.sion.SiONData;
	import org.si.sion.SiONDriver;
	
	[SWF(width = 550, height = 300, backgroundColor = 0x1f1f1f, frameRate = 30)]
	public class Main extends Sprite 
	{
		private var _driver:SiONDriver;
		private var _s1:SiONData, _s2:SiONData;
		
		public function Main():void 
		{
			_driver = new SiONDriver();
			_s1 = _driver.compile('l8 cdefgab&lt;c');
			_s2 = _driver.compile('l8 o6c o5bagfedc');
			
			addSoundButtons();
		}
		
		private function addSoundButtons():void 
		{
			var b1:PushButton = new PushButton(this, 10, 10, "Play sound 1", onPlaySound);
			var b2:PushButton = new PushButton(this, b1.x + b1.width + 5, 10, "Play sound 2", onPlaySound);
			
			function onPlaySound(e:Event):void 
			{
				if (e.target == b1) _driver.play(_s1);
				if (e.target == b2) _driver.play(_s2);
			}
		}
		
	}
	
}

Run the code and click on one of the two buttons that appear (Play sound 1 or Play sound 2). The first button plays the notes C5 to C6 and the second button plays the reverse of the first sound. If you click one and click the other before the sound finishes the first played sound will be stopped and the second one will be played.

If you want to play both sounds independently you need to use the sequenceOn() method of the SiONDriver class.

Note: Some methods (like sequenceOn()) will work only after the play() method of the SiONDriver object has been called. If you try to call these methods before that, the compiler will throw an error.

To enable our code to play both sounds at the same time first call the play() method in the constructor just before the addSoundButtons() method.

 _driver.play();
addSoundButtons();

Now that the driver is active we can safely modify the onPlaySound() function like so:

function onPlaySound(e:Event):void 
{
	if (e.target == b1) _driver.sequenceOn(_s1);
	if (e.target == b2) _driver.sequenceOn(_s2);
}

If you run the code now and push both buttons you will be able to hear the two sounds playing at the same time. In a real case scenario you would use more complex sounds, of course, but these are just fine to understand how it works.


Step 7: Stopping Sequences

The example above works just fine but there's one glitch. If you push a button repeatedly the same sound will start playing multiple times. This might work for you but what if you want to play it only once? In a real case scenario you might be in the need of stopping a sound before it's finished.

For this you can use the sequenceOff() method of the driver class. This method requires a track id representing the track that you want to stop playing and so we'll need to set that track id when a sound is played using sequenceOn().

Make the changes in the highlighted lines from the onPlaySound() function:

function onPlaySound(e:Event):void 
{
	if (e.target == b1) 
	{
		_driver.sequenceOff(1);
		_driver.sequenceOn(_s1, null, 0, 0, 1, 1 );
	}
	if (e.target == b2) 
	{
		_driver.sequenceOff(2);
		_driver.sequenceOn(_s2, null, 0, 0, 1, 2 );
	}
}

As you might have already noticed we set a track id for each sequence (the sixth parameter) so that we can reference it later by using the sequenceOff() method to stop it.


Step 8: Voices

Think of "voices" as being different instruments. As you have noticed the default sounds played in SiON are a bit annoying and you may want to hear sound from a piano.

Lucky for us the SiON library comes with some preset voices (462 voices in the current version to be precise).

All these voice presets are contained in the SiONPresetVoice class and are sorted in 15 categories.

Voices

Accessing these voices or categories is very simple. First you need an instance of the SiONPresetVoice class and then access them either by voice key (in case of voices) or by voice number and category key (in case of categories) or by category number.

The following example demonstrates how to access a voice or category in different ways.

var presets:SiONPresetVoice = new SiONPresetVoice(); // Instance of the SiONPresetVoices
			
// Accessing voice by key
var voiceByKey:SiONVoice = presets["valsound.brass2"]; // Returns the valsound.brass2 voice from the valsound.brass category
// Accessing category by key
var categoryByKey:Array = presets["valsound.wind"]; // Returns the valsound.wind category
// Accessing voice by number
var voiceByNo:SiONVoice = categoryByKey[3]; // Returns the 4th voice (valsound.wind4) in the valsound.wind category
// Accessing category by number
var categoryByNo:Array = presets.categolies[3]; // Returns the valsound.brass category

(Yes, categolies with an L; that's not a typo.)


Step 9: Voice Selector

Now we're going to create a new class which will help us to view categories and voices in each category.

First create a new class in your project named VoiceSelector that extends the Sprite class.

package  
{
	import com.bit101.components.ComboBox;
	import flash.display.Sprite;
	import org.si.sion.utils.SiONPresetVoice;
	
	public class VoiceSelector extends Sprite 
	{
		private var _presets:SiONPresetVoice;
		private var _categories:ComboBox;
		private var _voices:ComboBox;
		
		public function VoiceSelector() 
		{
			_presets = new SiONPresetVoice();
			_categories = new ComboBox(this, 0, 0, "Select a category");
			
			_voices = new ComboBox(this, _categories.width + _categories.x + 5, 0, "Select a voice");
		}
		
	}

}

In here we need a reference to a SiONPresetVoice object so we add a new variable called _presets and instantiate in the constructor.

Also we need two combo boxes: one for the categories and one for the voices. We'll use the ComboBox component from the minimalcomps library.


Step 10: VoiceSelector Categories and Voices

Now that we have our UI placed, we first need to populate the categories list. For this we'll write a method called populateCategories(), so go ahead and add it to the class.

private function populateCategories():void 
{
	for each (var cat:Array in _presets.categolies)
		_categories.addItem(cat.name);
}

Also it's necessary to call this method from the constructor of the class, so add it on the last line of the constructor. We also need an event listener to modify the voices list when a category is selected.

package  
{
	import com.bit101.components.ComboBox;
	import flash.display.Sprite;
	import flash.events.Event;
	import org.si.sion.utils.SiONPresetVoice;
	
	public class VoiceSelector extends Sprite 
	{
		private var _presets:SiONPresetVoice;
		private var _categories:ComboBox;
		private var _voices:ComboBox;
		
		public function VoiceSelector() 
		{
			_presets = new SiONPresetVoice();
			_categories = new ComboBox(this, 0, 0, "Select a category");
			_categories.width = 120;
			
			_voices = new ComboBox(this, _categories.width + _categories.x + 5, 0, "Select a voice");
			_voices.width = 120;
			
			populateCategories();
			_categories.addEventListener(Event.SELECT, populateVoices);
		}
		
		private function populateCategories():void 
		{
			for each (var cat:Array in _presets.categolies)
				_categories.addItem(cat.name);
		}
		
		private function populateVoices(e:Event = null):void 
		{
			_voices.removeAll();
			_voices.selectedIndex = 0;
			var voices:Array = _presets[_categories.selectedItem];
			for (var i:int = 0; i < voices.length; i++) 
				_voices.addItem(voices[i].name);
		}
		
	}

}

As you can see in the populateCategories() method we use a for-each loop to get each category available.

In the populateVoices() method we need first to remove all items from the voices list, get the array corresponding to the selected category and add the items to the voices list.

Last thing we need to do to complete our voice selector is add a getter to return the selected voice.

public function get voice():SiONVoice 
{
	if(_categories.selectedItem)
		return _presets[_categories.selectedItem][_voices.selectedIndex]; 
	else return null;
}

Step 11: Using the Selector

OK. So we have the voice selector ready. Go back to the main class and add a new private property called _selector of type VoiceSelector.

private var _selector:VoiceSelector;

Now we are going to add a method to create the selector called addSelector() in which we instantiate the VoiceSelector object and add it to the stage.

private function addSelector():void 
{
	_selector = new VoiceSelector();
	_selector.x = 220;
	_selector.y = 10;
	addChild(_selector);
}

Also call this method from within the constructor just after the addSoundButtons() method call.

public function Main():void 
{
	_driver = new SiONDriver();
	_s1 = _driver.compile('l8 cdefgab&lt;c');
	_s2 = _driver.compile('l8 o6co5bagfedc');
	_driver.play();
	addSoundButtons();
	addSelector();
}

Step 12: Playing Sounds with Selected Voice

At step 6 we played some notes using the default voice. Now we want to play the same notes but using the selected voice in the voice selector.

To do this simply modify the onPlaySound() function like so:

function onPlaySound(e:Event):void 
{
	if (e.target == b1) 
	{
		_driver.sequenceOff(1);
		_driver.sequenceOn(_s1, _selector.voice, 0, 0, 1, 1 );
	}
	if (e.target == b2) 
	{
		_driver.sequenceOff(2);
		_driver.sequenceOn(_s2, _selector.voice, 0, 0, 1, 2 );
	}
}

When playing a sequence the second parameter of the sequenceOn method represents the voice used for the sound data.

Run the code and play around with different voices to see how each sounds (click the Play sound 1 or Play sound 2 button to play one of the two sounds).


Step 13: Sound Objects

Another way to get more control over playing sounds in SiON is using the SoundObjects. The SoundObject class is the base class for all objects that can play sounds by operating SiONDriver.

You can define your own sound object by extending the SoundObject class and implementing everything you need. The SiON library has some built in classes that extend the SoundObject to implement different features. Some of these are MMLPlayer (used to play an MML string with control over the played tracks), PatternSequencer (which provides simple one track pattern player) and MultiTrackSoundObject (this being the base class for SoundObjects that use multiple tracks).

To keep it simple we won't create a custom sound object but we'll use one already existing in SiON. There is an interesting class DrumMachine that provides independent bass drum, snare drum and hi-hat cymbals tracks. It is not a direct descendant of the SoundObject class but I find it better suited for this tutorial.


Step 14: DrumMachine

As I've said before the DrumMachine class is a MultiTrackSoundObject which in its turn is a descendant of the SoundObject class.

Drum machine provides three main tracks: bass, hi-hat and snare. All these tracks can be independently controlled meaning that you can change properties like volume, voices or patterns on each without affecting the others.

First let's add two new variables to our Main class:

private var _drumsToggler:PushButton;
private var _drums:DrumMachine;

The first one provides a button used to start/stop the drums and the second one represents the drum machine used by us.

Now add the addDrums() method which is used to create DrumMachine object and the button that toggles it on and off.

private function addDrums():void 
{
	_drums = new DrumMachine();
	_drumsToggler = new PushButton(this, 10, 35, "Drums OFF", toggleDrums);
	_drumsToggler.toggle = true;
}

Also we need to add the handler to the button:

private function toggleDrums(e:Event):void 
{
	_drumsToggler.label = _drumsToggler.selected ? "Drums ON" : "Drums OFF";
	if (_drumsToggler.selected) _drums.play();
	else _drums.stop();
}

And last but not least call the addDrums() method from within the constructor:

public function Main():void 
{
	_driver = new SiONDriver();
	_s1 = _driver.compile('l8 cdefgab&lt;c');
	_s2 = _driver.compile('l8 o6co5bagfedc');
	_driver.play();
	addSoundButtons();
	addSelector();
	addDrums();
}

If you run the code now you should see a new button. If you click it you will hear the DrumMachine playing. Neat, isn't it?


Step 15: Independent Volume

Like I've said before each track in a MultiTrackSoundObject (like DrumMachine) can be controlled independently from each other. To demonstrate this we'll change the volume on each track of the DrumMachine object created earlier.

Let's start by adding some knobs for the volume, using the minimalcomps Knob class. For this we're going to create a new class called DrumsVolume.

package  
{
	import com.bit101.components.Knob;
	import flash.display.Sprite;
	import flash.events.Event;
	
	public class DrumsVolume extends Sprite 
	{
		private var _bassKnob:Knob;
		private var _snareKnob:Knob;
		private var _hihatKnob:Knob;
		
		public function DrumsVolume() 
		{
			addBassKnob();
			addSnareKnob();
			addHiHatKnob();
		}
		
		private function addBassKnob():void 
		{
			_bassKnob = new Knob(this, 15, 0, "Bass vol.", onChange);
			_bassKnob.radius = 10;
			_bassKnob.labelPrecision = 0;
			_bassKnob.value = 100;
		}
		
		private function addSnareKnob():void 
		{
			_snareKnob = new Knob(this, 15, _bassKnob.y + _bassKnob.height + 5, "Snare vol.", onChange);
			_snareKnob.radius = 10;
			_snareKnob.labelPrecision = 0;
			_snareKnob.value = 100;
		}
		
		private function addHiHatKnob():void 
		{
			_hihatKnob = new Knob(this, 15, _snareKnob.y + _snareKnob.height + 5, "Hi-Hat vol.", onChange);
			_hihatKnob.radius = 10;
			_hihatKnob.labelPrecision = 0;
			_hihatKnob.value = 100;
		}

		public function get bassVol():Number { return _bassKnob.value/100; }
		public function get snareVol():Number { return _snareKnob.value/100; }
		public function get hihatVol():Number { return _hihatKnob.value/100; }

		private function onChange(e:Event):void
		{
			dispatchEvent(new Event(Event.CHANGE));
		}
	}

}

In this class we simply create three different knobs (one for each track) and dispatch a CHANGE event when a knob is turned. Also there are three getters to retrieve the value of each volume knob.


Step 16: Setting the Volume

Now back in the Main class add a new variable called _drumsVolume and add the addDrumsVolume() method:

private function addDrumsVolume():void 
{
	_drumsVolume = new DrumsVolume();
	_drumsVolume.addEventListener(Event.CHANGE, onVolumeChange);
	_drumsVolume.x = 5;
	_drumsVolume.y = _drumsToggler.y + _drumsToggler.height + 5;
	addChild(_drumsVolume);
}

Also as you can see we add a listener to the DrumsVolume object for CHANGE events so we need to add the handler also.

private function onVolumeChange(e:Event):void 
{
	_drums.bassVolume = _drumsVolume.bassVol;
	_drums.snareVolume = _drumsVolume.snareVol;
	_drums.hihatVolume = _drumsVolume.hihatVol;
}

And finally add the highlighted line in the constructor of the Main class.

public function Main():void 
{
	_driver = new SiONDriver();
	_s1 = _driver.compile('l8 cdefgab&lt;c');
	_s2 = _driver.compile('l8 o6co5bagfedc');
	_driver.play();
	addSoundButtons();
	addSelector();
	addDrums();
	addDrumsVolume();
}

Run the project and play with the knobs. Be sure you turn on the DrumMachine to see how each track's volume is changed.


Step 17: Effectors

SoundObjects are useful for yet another purpose. You can add effectors to them in order to obtain different sound effects.

This way you can obtain effects like reverb, distortion, stereo chorus, auto pan, delay and many others.

You can find a complete list of built in effectors in the org.si.sion.effector package. Of course you can also make your own custom effectors by extending the SiEffectBase class.

To add one effector (or more) you can simply set them using the effectors property of the SoundObjects instance.

In our case if we wanted to use a distortion effect and an auto pan effect on the drums, we would do so like this:

_drums.effectors = [new SiEffectDistortion(), new SiEffectAutoPan()];

To remove all effectors from the SoundObject simply pass in an empty array.

_drums.effectors = [];

Step 18: Playback Speed

There might be a case where you need to dynamically specify the playback speed of all sound played within SiON (e.g. in a game where you enter in slow motion mode).

To specify the playback speed you need to modify a property called bpm (which stands for beats per minute). This is a property of the SiONDriver class and can be used as a global modifier for all sounds played within SiON.

Let's add a knob to modify this property.

First define a new private variable in the Main class named _bpmKnob.

private var _bpmKnob:Knob;

Next we'll create a method to add the knob and attach an event handler to it.

private function addBPMKnob():void
{
	_bpmKnob = new Knob(this, 70, _drumsVolume.y + 10, "BPM", changeMaster);
	_bpmKnob.minimum = 48;
	_bpmKnob.maximum = 256;
	_bpmKnob.labelPrecision = 0;
	_bpmKnob.value = _driver.bpm;
}

private function changeMaster(e:Event):void 
{
	if(e.target == _bpmKnob) _driver.bpm = _bpmKnob.value;
}

As you can see in the event handler we simply check if the bpm knob has been turned and set the master bpm accordingly.

Run the code and give it a try. Turn on the drum machine or play one of the two sounds to see how the bpm affects the playback.

Note: Don't forget to call the addBPMKnob() method from within the constructor.

public function Main():void 
{
	_driver = new SiONDriver();
	_s1 = _driver.compile('l8 cdefgab&lt;c');
	_s2 = _driver.compile('l8 o6co5bagfedc');
	_driver.play();
	addSoundButtons();
	addSelector();
	addDrums();
	addDrumsVolume();
	addBPMKnob();
}

Step 19: Master Volume

As done with the bpm you can change the volume of all sounds playing in SiON using the driver's volume property.

We're going to add another knob to serve as a master volume modifier.

private var _volumeKnob:Knob;

The addVolumeKnob() method will instantiate the volume knob and configure it.

private function addVolumeKnob():void
{
	_volumeKnob = new Knob(this, 70, _bpmKnob.y + _bpmKnob.height + 10, "Volume", changeMaster);
	_volumeKnob.labelPrecision = 0;
	_volumeKnob.value = _driver.volume * 100;
}

As seen above the volume knob uses the same changeMaster() event handler to change the volume so we need to add one more line in the handler:

private function changeMaster(e:Event):void 
{
	if (e.target == _bpmKnob) _driver.bpm = _bpmKnob.value;
	if (e.target == _volumeKnob) _driver.volume = _volumeKnob.value / 100;
}

Note: The value of the volume knob is divided by 100 because the volume property of the SiONDriver class takes a value from 0 to 1.

Also add the following code as the last line in the constructor:

addVolumeKnob();

Step 20: Panning

Currently SiON supports only one channel or two channel output. If you're using two channel output (like headphones or dual speakers -- the default when instantiating the SiONDriver) you can pan the sound so that you hear them in one channel or the other or more in one than the other.

The SiONDriver class has a property called pan which can be used to pan all sound coming out of SiON. Let's add a pan control to our project.

First we'll use a horizontal slider to represent the pan controller.

private var _panController:HUISlider;

Then we'll make use of the addPanControl() method to create and add the slider to the stage.

private function addPanControl():void
{
	_panController = new HUISlider(this, _selector.x, _selector.y + _selector.height + 10 , "Pan", panSound);
	_panController.minimum = -100;
	_panController.maximum = 100;
	_panController.labelPrecision = 0;
}

As you can see the handler for the slider is panSound() so let's add this one too.

private function panSound(e:Event):void 
{
	_driver.pan = _panController.value / 100;
}

In the handler we simply set the driver's pan property to the value of the slider divided by 100. The division is necessary because the pan property can only take values from -1 to 1.

Add the addPanControl() method call in the last line of the constructor, compile the code and give it a try.

Note: To better observe the difference you can use some headphones.


Step 21: Synchronizing

As I've said at the beginning of the tutorial SiON makes it very easy to sync sounds with display data.

The simplest way to sync sounds with display objects is by using the noteOn() method of the SiONDriver class.

So let's say you have a ball that bounces and an event is fired when that ball hits the floor/roof/walls. You can add an event listener for that event and in the event handler you would do something like this:

private function onBallHit(e:Event):void
{
	_driver.noteOn(50, _presets["valsound.percus3"], 1);
}

This would play a percussion note when the ball hits something.

Of course for the example above we've considered the _driver as the SiONDriver instance and _presets being an instance of the SiONPresetVoice class.

Note: As any other operation in SiON you need the driver to be streaming in order for it to work (_drive.play() must be called before).


Step 22: Integrating the Example

Now that you have an idea how easy it is to sync sounds with display data let's integrate the above example in our project.

We'll start by adding a new class called Ball which will represent the moving object.

package  
{
	import flash.display.Sprite;
	
	public class Ball extends Sprite 
	{
		public var vx:Number;
		public var vy:Number;
		
		public function Ball() 
		{
			draw();
		}
		
		private function draw():void
		{
			graphics.clear();
			graphics.beginFill(0xffffff, 0.9);
			graphics.drawCircle(5, 5, 5);
			graphics.endFill();
		}
	}

}

In this class we simply draw a white circle with a diameter of 10px which has two public properties vx and vy that represent the velocities on each axis.


Step 23: Ball Container

Now that we have an object to move we'll create a container in order to specify some boundaries. When the ball hits these boundaries some short sound will be played. A rectangle shape will do just fine as a container.

Add a new class to the project called BallContainer

package  
{
	import flash.display.Sprite;
	import org.si.sion.SiONDriver;
	import org.si.sion.utils.SiONPresetVoice;
	
	public class BallContainer extends Sprite 
	{
		private var _balls:Array;
		private var _voices:SiONPresetVoice;
		private var _driver:SiONDriver;
		
		public function BallContainer() 
		{
			_voices = new SiONPresetVoice();
			_driver = SiONDriver.mutex ? SiONDriver.mutex : new SiONDriver();
			_driver.play();
			draw();
		}
		
		private function draw():void 
		{
			graphics.beginFill(0, .3);
			graphics.lineStyle(1, 0, 0.6);
			graphics.drawRect(0, 0, 320, 170);
			graphics.endFill();
		}
		
	}

}

In this class we have an array called _balls (this will hold the instances of the created balls), an instance of the SiONDriver named _driver and an instance of the SiONPresetVoice meaning the _voices variable.

In the constructor we simply instantiate the voice presets after which we get the driver instance if it already exists (remember the note from Step 3 about how we can only create one instance of the SiON driver class at any given moment) or create a new one. Also we start the driver with the play() method.

The draw() method simply draws the containers background and the walls.

Adding balls to the container

Now that the container is drawn we need to add some balls to it. The addBalls() method does just that so go ahead and add it to the BallContainer class.

private function addBalls():void 
{
	_balls = [];
	for (var i:int = 0; i < 5; i++) 
	{
		var b:Ball = new Ball();
		addChild(b);
		_balls.push(b);
		b.x = Math.random() * 310;
		b.y = Math.random() * 160;
	}
}

In this method a for-loop is used to create five balls and add them in the container at a random position.

Also add the following code just after the last line in the constructor:

addBalls();

Moving them

OK. Everything's fine so far. A last thing we need to add to our class to make it functional is an event listener for ENTER_FRAME events where we move the balls.

First add the event listener in the constructor:

public function BallContainer() 
{
	_voices = new SiONPresetVoice();
	_driver = SiONDriver.mutex ? SiONDriver.mutex : new SiONDriver();
	_driver.play();
	draw();
	addBalls();
	addEventListener(Event.ENTER_FRAME, onEnterFrame);
}

Then add the handler for this listener:

private function onEnterFrame(e:Event):void 
{
	var b:Ball;
	for (var i:int = 0; i < _balls.length; i++) 
	{
		b = _balls[i];
		b.x += b.vx;
		b.y += b.vy;
		b.vx = b.x >= 310 || b.x <= 0 ? -b.vx : b.vx;
		b.vy = b.y >= 160 || b.y <= 0 ? -b.vy : b.vy;
		
		if (b.x >= 310 || b.x <= 0 || b.y >= 160 || b.y <= 0)
			_driver.noteOn(Scale(new Scale("Amp")).getNote(i), _voices["midi.chrom6"], 1);
	}
}

In the onEnterFrame() handler we us a for-loop to go through every ball and update its position. Also we check whether a ball has hit a boundary using an if-statement and if it has we use the noteOn() method to play a sound.

In the noteOn() method we've used the following parameters:

  • note - this represents the note to be played and is an integer from 0 to 127 (or C0 to B9). As you can see we get the appropriate note depending on the ball number (just to mix the notes and not have same sound).
  • voice - this is the voice in which the note will be played. In our case a Xylophone is used (or midi.chrom6 voice).
  • length - this parameter represents the note length in 16th beat. If this is set to 0 the note will not be removed and will remain in memory (if you use a length of 0 you should use the noteOff() method in order to remove it).

Adding the container

Now that everything is set up let's make use of this container. Open the Main class and add a new private variable named _ballCont of type BallContainer.

We'll make use of another method (conveniently named addBallContainer()) to add the container to the stage.

private function addBallContainer():void 
{
	_ballCont = new BallContainer();
	addChild(_ballCont);
	_ballCont.x = 170;
	_ballCont.y = 80;
}

And also call this method from the constructor.

public function Main():void 
{
	_driver = new SiONDriver();
	_s1 = _driver.compile('l8 cdefgab < c');
	_s2 = _driver.compile('l8 o6co5bagfedc');
	_driver.play();
	addSoundButtons();
	addSelector();
	addDrums();
	addDrumsVolume();
	addBPMKnob();
	addVolumeKnob();
	addPanControl();
	addBallContainer();
}

Run the code and see how it works. When a ball hits a wall and changes direction a note should be played.


Step 24: Final Touch

Now just as a final touch to we'll add some functionality to the ball container so that we can start and stop it.

package  
{
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.MouseEvent;
	import org.si.sion.SiONDriver;
	import org.si.sion.utils.Scale;
	import org.si.sion.utils.SiONPresetVoice;
	
	public class BallContainer extends Sprite 
	{
		private var _balls:Array;
		private var _on:Boolean;
		private var _voices:SiONPresetVoice;
		private var _driver:SiONDriver;
		
		public function BallContainer() 
		{
			_voices = new SiONPresetVoice();
			_driver = SiONDriver.mutex ? SiONDriver.mutex : new SiONDriver();
			_driver.play();
			draw();
			addBalls();
			addEventListener(MouseEvent.CLICK, onClick);
		}
		
		private function onClick(e:MouseEvent):void 
		{
			if (_on) stop();
			else start();
		}
		
		private function onEnterFrame(e:Event):void 
		{
			var b:Ball;
			for (var i:int = 0; i < _balls.length; i++) 
			{
				b = _balls[i];
				b.x += b.vx;
				b.y += b.vy;
				b.vx = b.x >= 310 || b.x <= 0 ? -b.vx : b.vx;
				b.vy = b.y >= 160 || b.y <= 0 ? -b.vy : b.vy;
				
				if (b.x >= 310 || b.x <= 0 || b.y >= 160 || b.y <= 0)
					_driver.noteOn(Scale(new Scale("Amp")).getNote(i), _voices["midi.chrom6"], 1);
			}
		}
		
		private function draw():void 
		{
			graphics.beginFill(0, .3);
			graphics.lineStyle(1, 0, 0.6);
			graphics.drawRect(0, 0, 320, 170);
			graphics.endFill();
		}
		
		private function addBalls():void 
		{
			_balls = [];
			for (var i:int = 0; i < 5; i++) 
			{
				var b:Ball = new Ball();
				addChild(b);
				_balls.push(b);
				b.x = Math.random() * 310;
				b.y = Math.random() * 160;
			}
		}
		
		public function start():void
		{
			var b:Ball;
			for (var i:int = 0; i < _balls.length; i++) 
			{
				b = _balls[i];
				b.vx = (Math.random() * 10) - 5;
				b.vy = (Math.random() * 10) - 5;
			}
			_on = true;
			addEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
		
		public function stop():void 
		{
			_on = false;
			removeEventListener(Event.ENTER_FRAME, onEnterFrame);
		}
		
	}

}

As you can see I've highlighted the additions in the BallContainer class.

As a brief explanation I've first added a boolean variable _on which keeps track if the movie is playing (balls are moving) or not. In the constructor I've changed the line that adds an event listener for ENTER_FRAME event with one for MOUSE_CLICK events. Also the MouseEvent handler named onClick() is used to start or stop the movie when it's clicked.

Lastly in the start() and stop() methods the ENTER_FRAME event listener is added and respectively removed. Also in the start() method we reset the velocities on each ball.


Conclusion

The SiON library is very useful when you need to use a lot of sound (in games most often) but you can't afford the extra size of the SWF or time to load them. As you can see it's not that hard at all to create interesting sounds on the run.

You can see many examples of cool implementations of the library at wonderfl.net. Also these examples are useful to learn more about other features in SiON.

I hope you enjoyed this tutorial and thank you for reading it.

Advertisement