Hi @tienery.
I had some time left and was keen on playing with dynamic audio again so I decided to give you
a more sophisticated example. The basic idea is based on the .mod file format, which was
invented in the glory days of home computers for composing & playback of music using
sampled instruments. We’re using synthesized sounds though. You can even switch between
sine, square, sawtooth, toggle an echo effect and most importantly it renders a tune of the said
glory days!
This should give you a decent base to create a simple step sequencer.
package;
import openfl.display.Sprite;
import openfl.media.Sound;
import openfl.events.SampleDataEvent;
import openfl.utils.ByteArray;
class Main extends Sprite
{
private var sampleRate:Int = 44100;
private var bufferSize:Int = 8192;
private var bpm:Int = 125;
private var numberOfRows:Int = 64;
private var currentRow:Int = 0;
private var quarterNoteLength:Float;
private var sixteenthNoteLength:Float;
private var numOctaves:Int = 8;
private var patterns:Array<Dynamic>= new Array();
private var currentPattern:Int=0;
private var songOrder:Array<Int> = [0, 0, 1, 1];
private var notes:Array<String>= ["c-", "c#", "d-", "d#", "e-", "f-", "f#", "g-", "g#", "a-", "a#", "b-"];
private var frequencies:Array<Dynamic> = new Array();
private var samplePosition:Int = 0;
private var position:Int = 0;
private var channel1:Dynamic = {volume: .05, waveForm: "", frequency: [], noteTriggered: false, envelopePos: 0};
private var envelope:Array<Float>= new Array();
private var echoBytes:ByteArray = new ByteArray();
private var maxEchoBytes:UInt = 8192 * 256; // reserve some memory for the echo effect
private var echoing:Bool = true;
private var echoDelay:Float;
private var echoPosition:UInt=0;
public function new()
{
super();
quarterNoteLength = sampleRate * 60 / bpm;
sixteenthNoteLength = quarterNoteLength / 2 / 2;
echoDelay = sixteenthNoteLength * 4;
for (a in 0...numOctaves)
{
for (b in 0...notes.length)
{
frequencies.push([notes[b % notes.length] + a, 16.35 * Math.pow(2, frequencies.length / 12)]);
}
}
patterns.push(["f-4", "", "", "", "g#4", "", "", "f-4", "", "f-4", "a#4", "", "f-4", "", "d#4", "", "f-4", "", "", "", "c-5", "", "", "f-4", "", "f-4", "c#5", "", "c-5", "", "g#4", "", "f-4", "", "c-5", "", "f-5", "", "f-4", "d#4", "", "d#4", "c-4", "", "g-4", "", "f-4", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", ""]);
patterns.push(["", "", "c-5|a-4", "", "c-5|a-4", "", "c-5|a-4", "d#5|a#4", "", "d#5|a#4", "d#5|a#4", "", "d-5|a#4", "", "d-5|a#4", "", "d-5|a#4", "", "c-5|a-4", "", "c-5|a-4", "", "c-5|a-4", "d#5|a#4", "", "d#5|g-4", "d-5|f-4", "", "c-5|a-4", "", "a-4|c-4", "", "a-4|c-4", "", "g#4|f-4", "", "g#4|f-4", "", "g#4|f-4", "", "g#4|f-4", "a#4|g-4", "", "a#4|g-4", "", "a#4|g-4", "", "a#4|g-4", "a#4|g-4", "", "c-5|a-4", "", "c-5|a-4", "", "c-5|a-4", "", "a#4|g-4", "c-5|a-4", "", "c-5|a-4", "", "c-5|a-4", "", "c-5|a-4"]);
Reflect.setProperty(channel1, "waveForm", "sawtooth");
for (c in 0...Std.int(quarterNoteLength))
{
if (c < sixteenthNoteLength * 1)
{
envelope.push(1.0);
}
else
{
if (c < sixteenthNoteLength * 3)
{
envelope.push(.4);
}
else
{
envelope.push(0.0);
}
}
}
updateRow();
var sound:Sound = new Sound();
sound.addEventListener(SampleDataEvent.SAMPLE_DATA, onSampleData);
sound.play();
}
private function updateRow():Void
{
var tempNote:String = patterns[songOrder[currentPattern]][currentRow];
if (tempNote != "")
{
var tempArray:Array<Float> = new Array();
Reflect.setProperty(channel1, "frequency", []);
if (tempNote.indexOf("|") == -1)
{
// single note
tempArray.push(findFrequency(tempNote));
}
else
{
// chord
var tempNotes:Array<String> = tempNote.split("|");
for (a in 0...tempNotes.length)
{
tempArray.push(findFrequency(tempNotes[a]));
}
}
Reflect.setProperty(channel1, "noteTriggered", true);
Reflect.setProperty(channel1, "frequency", tempArray);
}
}
private function onSampleData(event:SampleDataEvent):Void
{
var tempArray:Array<Float>;
var addDataL:Float = 0.0;
var addDataR:Float = 0.0;
var volumeModifier:Float= 0.0;
var sampleData:Float = 0.0;
for (i in 0...bufferSize)
{
if (++samplePosition == sixteenthNoteLength)
{
if (++currentRow == numberOfRows)
{
currentRow = 0;
if (++currentPattern == songOrder.length)
{
currentPattern = 0;
}
}
updateRow();
samplePosition = 0;
}
if (Reflect.field(channel1, "noteTriggered"))
{
Reflect.setProperty(channel1, "envelopePos", 0);
Reflect.setProperty(channel1, "noteTriggered", false);
}
if (Reflect.field(channel1, "envelopePos")+ 1 < envelope.length)
{
var tempInt:Int = Reflect.field(channel1, "envelopePos") + 1;
Reflect.setProperty(channel1, "envelopePos", tempInt);
}
volumeModifier = envelope[Reflect.field(channel1, "envelopePos")];
tempArray = Reflect.field(channel1, "frequency");
if (tempArray.length == 1)
{
sampleData = generate(Reflect.field(channel1, "waveForm"), position, tempArray[0] , Reflect.field(channel1, "volume") * volumeModifier);
}
else
{
sampleData = generate(Reflect.field(channel1, "waveForm"), position, tempArray[0] , Reflect.field(channel1, "volume") * volumeModifier);
sampleData += generate(Reflect.field(channel1, "waveForm"), position, tempArray[1] , Reflect.field(channel1, "volume") * volumeModifier);
}
if (echoing)
{
if (position > echoDelay)
{
var oldPos:UInt = echoBytes.position;
echoBytes.position = echoPosition;
addDataL = echoBytes.readFloat() / 4;
addDataR = echoBytes.readFloat() / 4;
if (echoPosition + 8 < maxEchoBytes)
{
echoPosition += 8;
}
else
{
echoPosition = 0;
}
echoBytes.position = oldPos;
}
}
event.data.writeFloat(sampleData + addDataL);
event.data.writeFloat(sampleData + addDataR);
echoBytes.writeFloat(sampleData);
echoBytes.writeFloat(sampleData);
if (echoBytes.position == maxEchoBytes)
{
echoBytes.position = 0;
}
position++;
}
}
private function findFrequency(inpNote:String):Float
{
var retVal:Float=0.0;
for (a in 0...frequencies.length)
{
if (frequencies[a][0] == inpNote)
{
retVal = frequencies[a][1];
break;
}
}
return retVal;
}
private function generate(waveForm:String, pos:Int, frequency:Float, volume:Float):Float
{
var retVal:Float = 0.0;
switch (waveForm)
{
case "square":
retVal = Math.sin((pos) * Math.PI * 2 / sampleRate * frequency) > 0 ? volume : -volume;
case "sine":
retVal = Math.sin((pos) * Math.PI * 2 / sampleRate * frequency * 2) * volume;
case "sawtooth":
retVal = (2 * (pos % (sampleRate / frequency)) / (sampleRate / frequency) - 1) * volume;
}
return retVal;
}
}