In order to make the code so far a little more reusable, I moved it over into its own class, called Tone. I also implemented some optimizations and other little tricks. The most important is that instead of calculating the next batch of samples along with the envelope on every SAMPLE_DATA event, I precalculate all the samples within the envelope right up front, storing it in a Vector of Numbers. Here’s the class:
[as3]package
{
import flash.media.Sound;
import flash.events.SampleDataEvent;
import flash.events.Event;
public class Tone
{
protected const RATE:Number = 44100;
protected var _position:int = 0;
protected var _sound:Sound;
protected var _numSamples:int = 2048;
protected var _samples:Vector.
protected var _isPlaying:Boolean = false;
protected var _frequency:Number;
public function Tone(frequency:Number)
{
_frequency = frequency;
_sound = new Sound();
_sound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
_samples = new Vector.
createSamples();
}
protected function createSamples():void
{
var amp:Number = 1.0;
var i:int = 0;
var mult:Number = frequency / RATE * Math.PI * 2;
while(amp > 0.01)
{
_samples[i] = Math.sin(i * mult) * amp;
amp *= 0.9998;
i++;
}
_samples.length = i;
}
public function play():void
{
if(!_isPlaying)
{
_position = 0;
_sound.play();
_isPlaying = true;
}
}
protected function onSampleData(event:SampleDataEvent):void
{
for (var i:int = 0; i < _numSamples; i++) { if(_position >= _samples.length)
{
_isPlaying = false;
return;
}
event.data.writeFloat(_samples[_position]);
event.data.writeFloat(_samples[_position]);
_position++;
}
}
public function set frequency(value:Number):void
{
_frequency = value;
createSamples();
}
public function get frequency():Number
{
return _frequency;
}
}
}[/as3]
Note that in the constructor I call createSamples(). This creates the Vector with all samples needed for the duration of the note, including the amplitude of the pseudo-envelope. In the frequency setter, the samples are re-created. The result is that in the onSampleData handler method, I just fill up the byte array with the next so many values out of the _samples vector, stopping when I reach the end of that Vector.
Note also that the amplitude is decreased per sample, rather than per SAMPLE_DATA event, thus it needs to be reduced by a much smaller amount each time. This should also give a smoother envelope, though I’m not sure how noticeable it is.
Here’s a brief bit of code that shows it in action:
[as3]import flash.events.MouseEvent;
var tone:Tone = new Tone(800);
stage.addEventListener(MouseEvent.CLICK, onClick);
function onClick(event:MouseEvent):void
{
tone.frequency = 300 + mouseY;
tone.play();
}[/as3]
It creates a tone. Whenever you click on the stage, it calculates a new frequency for the tone based on the y position of the mouse and plays the tone. Simple enough.
I don’t consider this class anywhere near “complete”. Just a beginning evolution in something. I’d like to add support for more flexible and/or complex envelopes, a stop method, and some other parameters to change the sound. But even so, this is relatively useful as is, IMHO.