Tired of those old-fashioned animated Christmas cards with Santa Claus laughing, a sleigh flying in the sky with a bunch of trees and sparkling lights?
This year you have no excuse. I'm going to show you how to build a virtual keyboard that plays Jingle Bells. You'll even be able to extend the concept and add more songs and notes to the piano.
For this tut I'll be using a combination of Tweenlite, the Flex SDK, the Flash IDE and Flash Develop.
Step 1: New File
Start Adobe Flash and create an ActionScript 3 Flash file.

Step 2: Properties
Open the properties and set the FPS to 30 and the stage size to 600 x 380px.

Step 3: Layer 1
Rename layer 1 as "background" and create a white rectangle of 580x360. Convert it to a movieclip symbol named "frame_mc" and set its position to x:10 y:10.


Step 4: Drop Shadow
Add a drop shadow filter with the following parameters:

Step 5: Keyboard Layer
Add a new layer called "keyboard", create a primitive rectangle 60x190 with a 5 pixel corner radius and with no stroke. Make it a movieclip symbol and call it "keybase_mc".


Step 6: Reflection and Glow Layers
Hit F8 and create a new movieclip called "key_mc". Create two more layers inside key_mc (besides the one already present with keybase_mc). Name them: "reflection" and "glow". Copy the frame to the newly created layers.

Step 7: glow_mc
NOTE: I've changed the frame_mc colors for a while to allow me to see the modifications on the keys. Select the movieclip in the glow layer, name it "glow_mc", open the filters and edit them according to the image below:

Step 8: reflection_mc
Select the movieclip in the reflection layer, name it "reflection_mc", open the filters and edit them to match the image below:

Step 9: base_mc
now select the movieclip in the base layer, name it "base_mc", open the filters and edit them to match the image below:

Step 10: Key
Copy and paste the key until you end up with 7 instances. Arrange them evenly across the stage.

Step 11: Align
Open the align tool and click the "horizontal spacing" icon.

Step 12: Notes

Create a new layer, call it "notes". Then write down C D E F G A B on the keys add the text to a new movieclip. Open the movieclip filters and edit them such as the image below:

Step 13: Numbers
Create a new layer, call it "numbers". Write the numbers from 1 to 7, this will represent the numbers that you will press on your keyboard to make the ecard key highlight.


Step 14: Flex SDK Path
Go to edit > preferences > ActionScript > ActionScript 3.0 settings and locate your Flex SDK path (you will need this to embed files directly through code).


Step 15: Flash Develop
Open Flash Develop (I'm just using this cause I like it so much more than the usual ActionScript editor from the flash IDE when writing packages) and create 2 blank as3 files. Name them "Main.as" and "Background.as", save them in the same folder as your .fla.


Step 16: Document Class
Inside Flash IDE set Main.as as your Document class.

Step 17: Autoplay Button
Create an autoplay movieClip and name it "autoplay_mc". This will be an autoplay button.

Step 18: Snowflakes
To create some snow flakes falling create a new movieclip, draw a smalll white circle inside and add a linkage identifier of "SnowFlake".

Step 19: Main.as
In Flash Develop open Main.as, define your Main class extending a movieclip and create a function called Main.
Start by importing these Classes inside your package:
import flash.display.MovieClip; import flash.events.Event; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.display.StageScaleMode; import flash.events.KeyboardEvent; import flash.utils.Timer; import com.greensock.TweenLite; import com.greensock.easing.*; import Background; //we will create a background class based on perlin noise and a transform matrix along with a few filled shapes so we can have a nice smooth transition import Snow; //i just grabbed kirupas snow and packaged it -> http://troyworks.com/blog/2008/11/26/flash-kirupa-snow-in-as30/ import flash.media.Sound;
Step 20: Variables
Inside your class define these variables:
//this will be our background private var _background:Background; //this will be our snow storm private var snow:Snow; //->notes //Embeding this way requires FLEX SDK -> alternative method is to add these sounds to your library and add a linkage ID to them. [Embed(source="assets/A.mp3")] private var A:Class; private var a:Sound = new A() as Sound; [Embed(source="assets/B.mp3")] private var B:Class; private var b:Sound = new B() as Sound; [Embed(source="assets/C.mp3")] private var C:Class; private var c:Sound = new C() as Sound; [Embed(source="assets/D.mp3")] private var D:Class; private var d:Sound = new D() as Sound; [Embed(source = "assets/E.mp3")] private var E:Class; private var e:Sound = new E() as Sound; [Embed(source="assets/F.mp3")] private var F:Class; private var f:Sound = new F() as Sound; [Embed(source="assets/G.mp3")] private var G:Class; private var g:Sound = new G() as Sound; //Storing the notes in an array will make it easier to link to the keyboard private var notes:Array = [c,d,e,f,g,a,b] //Note sequence for the music private var noteSequence:Array = [f, f, f, f, f, f, f, a, d, e, f, g, g, g, g, g, e, e, d, b, a, f, d, c] //Current note that is playing private var curnote:Number = 0 //Sequence of delay that the music needs to have between the notes private var noteDelay:Array = [100, 100 , 300, 100, 100 ,300, 100, 100, 100,100,200, 100, 100, 200, 90, 100, 90,100, 100, 120, 120, 120, 120, 300] //Timer to play the music private var tunetimer:Timer = new Timer(noteDelay[0]);
Step 21: Main()
The Main function
//Main function waits for the the maintimeline to be added to stage
public function Main():void{
addEventListener(Event.ADDED_TO_STAGE, addedToStage);
}
Step 22: Initialize
After being added to stage we'll initialize the background and the virtual keyboard:
//when added to stage we can set a stage scale mode, the background and the start the virtual piano
private function addedToStage(e:Event):void {
stage.scaleMode = StageScaleMode.NO_SCALE;
addBackground();
startMachine();
}
Step 23: Background Effects
Let's sort out the moving background and the Snow:
//adds the background
private function addBackground():void {
_background = new Background(150,150); //the reason the size is smaller is because it's very CPU intensive
_background.x = 5; //to give a white margin for the frame
_background.y = 5;
_background.width = 570 //scale it up to the size right size
_background.height = 350
frame_mc.addChild(_background); //adds background to the frame
snow = new Snow(570, 350) //creates a snow storm instance
Step 24: Virtual Keyboard
The initialization of the virtual keyboard
private function startMachine(e:MouseEvent = null):void {
//associates the keyboard events
stage.addEventListener(KeyboardEvent.KEY_DOWN, onkeyDown)
stage.addEventListener(KeyboardEvent.KEY_UP, onkeyUp)
//associates an autoplay method to the autoplay button
autoplay_mc.addEventListener(MouseEvent.CLICK, startAutoPlay);
autoplay_mc.buttonMode = true;
//associates the notes to the keys
var i:int = 0
while (i < 7) {
this["key"+i].note = notes[i]
i++
}
//makes the highlight of the keys disappear
lowlightKeys();
}
Step 25: Highlight
We need to create a function to remove the highlight from the keys:
private function lowlightKeys() {
var i:int = 0
while (i < 7) {
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:0} );
i++
}
}
Step 26: Key Events
Let's now handle the Key up and Key down events:
private function onkeyDown(e:KeyboardEvent):void {
var i:int=0
switch(e.keyCode) {
case 49: //keycode for 1
i=0
break;
case 50: //keycode for 2
i=1
break;
case 51: //keycode for 3
i=2
break;
case 52: //keycode for 4
i=3
break;
case 53: //keycode for 5
i=4
break;
case 54: //keycode for 6
i=5
break;
case 55: //keycode for 7
i=6
break;
}
notes[i].play();
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:1} ); //highlights the key
}
private function onkeyUp(e:KeyboardEvent):void {
var i:int=0
switch(e.keyCode) {
case 49:
i=0
break;
case 50:
i=1
break;
case 51:
i=2
break;
case 52:
i=3
break;
case 53:
i=4
break;
case 54:
i=5
break;
case 55:
i=6
break;
}
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:0} ); //lowlights the key
}
Step 27: Autoplay Functions
How to start and stop the autoplay
private function startAutoPlay(e:MouseEvent = null) {
curnote = 0; //everytime i start autoplay i reset the current playing note
tunetimer.delay = noteDelay[curnote] * 3 //this increases the delay set previously
tunetimer.addEventListener(TimerEvent.TIMER, autoPlayTune) //adds a listener to the timer event for everytime the timer is triggered
tunetimer.start() //starts the timer
}
private function stopAutoPlay(e:MouseEvent = null) {
tunetimer.stop() //stops timer
tunetimer.removeEventListener(TimerEvent.TIMER, autoPlayTune) //removes the event
}
Step 28: Update
Updating the virtual keyboard along with the music
private function updateMachine():void {
lowlightKeys(); // resets the keys highlights
var i:int = 0
while (i < 7) {
if (this["key" + i].note == noteSequence[curnote]) {
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:1} ); //if current note is the one associeated with the key then highlights it
}
i++
}
curnote++ //goes to next note
if (curnote > noteSequence.length) {
curnote = 0; //resets current note
stopAutoPlay(); //stops autoplay
}
}
Step 29: The Complete Code
Here's the full Main.as code
package{
import flash.display.MovieClip;
import flash.events.Event;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.display.StageScaleMode;
import flash.events.KeyboardEvent;
import flash.utils.Timer;
import com.greensock.TweenLite;
import com.greensock.easing.*;
import Background;
import Snow
import flash.media.Sound;
public class Main extends MovieClip{
private var _background:Background;
private var snow:Snow;
[Embed(source="assets/A.mp3")]
private var A:Class;
private var a:Sound = new A() as Sound;
[Embed(source="assets/B.mp3")]
private var B:Class;
private var b:Sound = new B() as Sound;
[Embed(source="assets/C.mp3")]
private var C:Class;
private var c:Sound = new C() as Sound;
[Embed(source="assets/D.mp3")]
private var D:Class;
private var d:Sound = new D() as Sound;
[Embed(source = "assets/E.mp3")]
private var E:Class;
private var e:Sound = new E() as Sound;
[Embed(source="assets/F.mp3")]
private var F:Class;
private var f:Sound = new F() as Sound;
[Embed(source="assets/G.mp3")]
private var G:Class;
private var g:Sound = new G() as Sound;
private var notes:Array = [c,d,e,f,g,a,b]
private var noteSequence:Array = [f, f, f,
f, f, f,
f, a, d, e, f,
g, g, g,
g, g, e, e,
d, b, a, f, d, c]
private var curnote:Number = 0
private var noteDelay:Array = [100, 100 , 300,
100, 100 ,300,
100, 100, 100,100,200,
100, 100, 200,
90, 100, 90,100,
100, 120, 120, 120, 120, 300]
private var tunetimer:Timer = new Timer(noteDelay[0]);
public function Main():void{
addEventListener(Event.ADDED_TO_STAGE, addedToStage);
}
private function addedToStage(e:Event):void {
stage.scaleMode = StageScaleMode.NO_SCALE;
addBackground();
startMachine();
}
//adds the background
private function addBackground():void {
_background = new Background(150,150);
_background.x = 5;
_background.y = 5;
_background.width = 570
_background.height = 350
frame_mc.addChild(_background);
snow = new Snow(570, 350);
frame_mc.addChild(snow);
}
private function startMachine(e:MouseEvent = null):void {
stage.addEventListener(KeyboardEvent.KEY_DOWN, onkeyDown)
stage.addEventListener(KeyboardEvent.KEY_UP, onkeyUp)
autoplay_mc.addEventListener(MouseEvent.CLICK, startAutoPlay);
autoplay_mc.buttonMode = true;
var i:int = 0
while (i < 7) {
this["key"+i].note = notes[i]
i++
}
lowlightKeys();
}
private function lowlightKeys() {
var i:int = 0
while (i < 7) {
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:0} );
i++
}
}
private function onkeyDown(e:KeyboardEvent):void {
var i:int=0
switch(e.keyCode) {
case 49:
i=0
break;
case 50:
i=1
break;
case 51:
i=2
break;
case 52:
i=3
break;
case 53:
i=4
break;
case 54:
i=5
break;
case 55:
i=6
break;
}
notes[i].play();
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:1} );
}
private function onkeyUp(e:KeyboardEvent):void {
var i:int=0
switch(e.keyCode) {
case 49:
i=0
break;
case 50:
i=1
break;
case 51:
i=2
break;
case 52:
i=3
break;
case 53:
i=4
break;
case 54:
i=5
break;
case 55:
i=6
break;
}
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:0} );
}
//AUTO PLAY FUNCTIONS
private function startAutoPlay(e:MouseEvent = null) {
curnote = 0;
tunetimer.delay = noteDelay[curnote] * 3
tunetimer.addEventListener(TimerEvent.TIMER, autoPlayTune)
tunetimer.start()
}
private function stopAutoPlay(e:MouseEvent = null) {
tunetimer.stop()
tunetimer.removeEventListener(TimerEvent.TIMER, autoPlayTune)
}
private function autoPlayTune(e:TimerEvent) {
if(curnote<noteSequence.length){
noteSequence[curnote].play(); //plays the note
tunetimer.delay = noteDelay[curnote] * 3
}
updateMachine()
}
private function updateMachine():void {
lowlightKeys();
var i:int = 0
while (i < 7) {
if (this["key" + i].note == noteSequence[curnote]) {
TweenLite.to(this["key" + i].glow_mc, 0.5,{alpha:1} );
}
i++
}
curnote++
if (curnote > noteSequence.length) {
curnote = 0;
stopAutoPlay();
}
}
}
}
Step 30: Background Class
Now on to the background class. We'll begin by importing these classes..
import flash.display.Shape;
import flash.events.Event;
import flash.display.Sprite;
import flash.display.MovieClip;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.geom.ColorTransform;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.geom.Matrix;
import flash.filters.ColorMatrixFilter;
import flash.display.GradientType;
import flash.display.Graphics;
import flash.display.InterpolationMethod;
import flash.display.SpreadMethod;
Step 31: Variables
..then defining the variables:
//Noise
private var dir:Array
private var point:Point
private var bd:BitmapData;
private var bmp:Bitmap;
private var bdmultiply:BitmapData;
private var bms:Sprite;
private var rect:Rectangle
private var cmf:ColorMatrixFilter;
private var w:Number
private var h:Number
//Linear Gradient Fill
private var gshape:Shape
private var gmetrics:Rectangle
private var gmatrix:Matrix
private var gtype:String
private var gspread:String
private var ginterpolation:String
private var gcolours:Array
private var galphas:Array
private var gratios:Array
//Solid Fill
private var sshape:Shape
Step 32: Initial Function
Here's the initial function:
public function Background($width:Number=100, $height:Number=100)
{
w = $width
h = $height
rect = new Rectangle(0, 0, w, h);
point = new Point(0, 0);
dir = [new Point(1, 262), new Point(400, 262)];
//this one is just to give a solid background to the whole stage
initBackgroundSolid();
//this will control the bightness contrast and saturation of the noise
initColorMatrix();
//there will be two noise backgrounds this will initiate them
initBackgroundNoise();
//a gradient is added so we dont get noisiated.. (get it? too much noise gets you noisiated? haha...hmmm)
initBackgroundGradient();
}
Step 33: initColorMatrix()
This function will control the bightness contrast and saturation of the noise, it's a very powerfull filter.
private function initColorMatrix():void {
cmf = new ColorMatrixFilter([2, 0, 0, 0, -20, //red
0, 2, 0, 0, -20, //green
0, 0, 2, 0, -20, //blue
0, 0, 0, 1, -20 ]); //alpha
}
Step 34: Solid Background Color
This one is just to give a solid background to the whole stage.
private function initBackgroundSolid():void {
sshape = new Shape();
sshape.graphics.beginFill(0x170a02,1)
sshape.graphics.drawRect( 0, 0, w, h );
sshape.graphics.endFill();
addChild(sshape)
}
Step 35: Noises
The noises:
private function initBackgroundNoise():void {
//first noise
bd = new BitmapData(w, h, false, 0 );
bmp = new Bitmap(bd);
bmp.smoothing = true;
addChild(bmp);
//second noise that overlaps the first noise through an overlay blend mode
bdmultiply = new BitmapData(w, h, false, 0 );
bms = new Sprite();
bms.addChild(new Bitmap(bdmultiply))
addChild(bms)
bms.blendMode = "overlay";
//renders the background so that the noise seems to be moving
addEventListener(Event.ENTER_FRAME, renderBG);
}
Step 36: Mask
Here's the gradient mask:
private function initBackgroundGradient() {
//this is a basic gradient box with alpha and rotated 90ยบ so that it starts from top-bottom instead of left-right
gshape = new Shape();
gmetrics = new Rectangle();
gmatrix = new Matrix();
gtype = GradientType.LINEAR;
gspread = SpreadMethod.PAD;
ginterpolation = InterpolationMethod.LINEAR_RGB;
gcolours = [ 0x170a02, 0x170a02 ];
galphas = [ 0, 1 ];
gratios = [ 0, 255 ];
gmatrix.createGradientBox(w, h,(Math.PI/180)*90 );
gshape.graphics.clear();
gshape.graphics.beginGradientFill(gtype, gcolours, galphas, gratios, gmatrix, gspread, ginterpolation );
gshape.graphics.drawRect( 0, 0, w, h );
gshape.graphics.endFill();
addChild(gshape)
}
Step 37: Render
It's render time!
private function renderBG(event:Event):void {
//updates noise direction
dir[0].x-= 1.5
dir[0].y-= 0 //these are here for you to play with
dir[1].x-= 0 //these are here for you to play with
dir[1].y -= 0 //these are here for you to play with
//defines the first background bitmap to have a perlin noise
bd.perlinNoise(w, h, 2, 10, false, true, 7, true, dir);
//coloring time (play with these values)
bd.colorTransform(rect, new ColorTransform(1, 0.7, 0.5));
//aplies the brigthness contrast and saturation modifications made earlier
bd.applyFilter(bd, rect, point, cmf)
//the other perlin noise
bdmultiply.perlinNoise(w, h, 3, 21, false, true, 7, true, dir)
//the other perlin noise collors
bdmultiply.colorTransform(rect, new ColorTransform(1, 0.6, 0.4));
}
Step 38: Complete Background Class
Here's the whole background Class:
package
{
import flash.display.Shape;
import flash.events.Event;
import flash.display.Sprite;
import flash.display.MovieClip;
import flash.display.Bitmap;
import flash.display.BitmapData;
import flash.display.BlendMode;
import flash.geom.ColorTransform;
import flash.geom.Rectangle;
import flash.geom.Point;
import flash.geom.Matrix;
import flash.filters.ColorMatrixFilter;
import flash.display.GradientType;
import flash.display.Graphics;
import flash.display.InterpolationMethod;
import flash.display.SpreadMethod;
public class Background extends MovieClip
{
private var dir:Array
private var point:Point
private var bd:BitmapData;
private var bmp:Bitmap;
private var bdmultiply:BitmapData;
private var bms:Sprite;
private var rect:Rectangle
private var cmf:ColorMatrixFilter;
private var w:Number
private var h:Number
private var gshape:Shape
private var gmetrics:Rectangle
private var gmatrix:Matrix
private var gtype:String
private var gspread:String
private var ginterpolation:String
private var gcolours:Array
private var galphas:Array
private var gratios:Array
private var sshape:Shape
public function Background($width:Number=100, $height:Number=100)
{
w = $width
h = $height
rect = new Rectangle(0, 0, w, h);
point = new Point(0, 0);
dir = [new Point(1, 262), new Point(400, 262)];
initBackgroundSolid();
initColorMatrix();
initBackgroundNoise();
initBackgroundGradient();
}
private function initColorMatrix():void {
cmf = new ColorMatrixFilter([2, 0, 0, 0, -20, //red
0, 2, 0, 0, -20, //green
0, 0, 2, 0, -20, //blue
0, 0, 0, 1, -20 ]); //alpha
}
private function initBackgroundSolid():void {
sshape = new Shape();
sshape.graphics.beginFill(0x170a02,1)
sshape.graphics.drawRect( 0, 0, w, h );
sshape.graphics.endFill();
addChild(sshape)
}
private function initBackgroundNoise():void {
bd = new BitmapData(w, h, false, 0 );
bmp = new Bitmap(bd);
bmp.smoothing = true;
addChild(bmp);
bdmultiply = new BitmapData(w, h, false, 0 );
bms = new Sprite();
bms.addChild(new Bitmap(bdmultiply))
addChild(bms)
bms.blendMode = "overlay";
addEventListener(Event.ENTER_FRAME, renderBG);
}
private function initBackgroundGradient() {
gshape = new Shape();
gmetrics = new Rectangle();
gmatrix = new Matrix();
gtype = GradientType.LINEAR;
gspread = SpreadMethod.PAD;
ginterpolation = InterpolationMethod.LINEAR_RGB;
gcolours = [ 0x170a02, 0x170a02 ];
galphas = [ 0, 1 ];
gratios = [ 0, 255 ];
gmatrix.createGradientBox(w, h,(Math.PI/180)*90 );
gshape.graphics.clear();
gshape.graphics.beginGradientFill(gtype, gcolours, galphas, gratios, gmatrix, gspread, ginterpolation );
gshape.graphics.drawRect( 0, 0, w, h );
gshape.graphics.endFill();
addChild(gshape)
}
private function renderBG(event:Event):void {
dir[0].x-= 1.5
dir[0].y-= 0
dir[1].x-= 0
dir[1].y -= 0
bd.perlinNoise(w, h, 2, 10, false, true, 7, true, dir);
bd.colorTransform(rect, new ColorTransform(1, 0.7, 0.5));
bd.applyFilter(bd, rect, point, cmf)
bdmultiply.perlinNoise(w, h, 3, 21, false, true, 7, true, dir)
bdmultiply.colorTransform(rect, new ColorTransform(1, 0.6, 0.4));
}
}
}
Step 39: Snow
The snow class is not mine, it was written by Troy Gardner, I just adapted it from the timeline to a package this is why I'm not commenting on the code. Create a "Snow.as" and copy this code inside.
package
{
import flash.display.MovieClip;
import flash.events.Event;
import flash.utils.Dictionary;
public class Snow extends MovieClip
{
var snowflakes:Array = new Array();
var snowflakeProps:Dictionary= new Dictionary(true);
var max_snowsize:Number = .04;
// pixels
var snowflakesCnt:Number = 150;
var oheight:Number;
var owidth:Number;
public function Snow($width,$height):void {
owidth = $width;
oheight = $height;
// quantity
for (var i:int=0; i<snowflakesCnt; i++) {
var t:MovieClip = new SnowFlake();//
t.name = "snowflake"+i;
t.alpha = 20+Math.random()*60;
t.x = -(owidth/2)+Math.random()*(1.5*owidth);
t.y = -(oheight/2)+Math.random()*(1.5*oheight);
t.scaleX = t.scaleY=.5+Math.random()*(max_snowsize*10);
var o:Object = new Object();
o.k = 1+Math.random()*2;
o.wind = -1.5+Math.random()*(1.4*3);
snowflakeProps[t] = o;
addChild(t);
snowflakes.push(t);
}
addEventListener(Event.ENTER_FRAME,snowFlakeMover)
}
private function shakeUp():void{
for (var i:int=0; i<snowflakes.length; i++) {
var t:MovieClip = snowflakes[i] as MovieClip;
t.x = -(owidth/2)+Math.random()*(1.5*owidth);
t.y = -(oheight/2)+Math.random()*(1.5*oheight);
}
}
private function snowFlakeMover(evt:Event):void {
var dO:MovieClip;
var o :Object;
if(visible && parent.visible){
for (var i:int = 0; i < snowflakes.length; i++) {
dO = snowflakes[i] as MovieClip;
o = snowflakeProps[dO];
dO.y += o.k;
dO.x += o.wind;
if (dO.y>oheight+10) {
dO.y = -20;
}
if (dO.x>owidth+20) {
dO.x = -(owidth/2)+Math.random()*(1.5*owidth);
dO.y = -20;
} else if (dO.x<-20) {
dO.x= -(owidth/2)+Math.random()*(1.5*owidth);
dO.y = -20;
}
}
}
}
}
}
Conclusion
My music skills aren't the greatest, the music may sound a bit strange. Oh well, with this tutorial you should now be able to create your own songs with more notes and different tones :). I hope you liked this tutorial, you'll find the commented code and both cs4 and cs3 versions up on the zip file. Thanks for reading!
