openfl
openfl copied to clipboard
SampleDataEvent
This is a freshened up version of #1723.
Quoting the original PR:
Implementation of Flash's SampleDataEvent using WebAudio on HTML5 and OpenAL on Windows/Neko. Though untested it should work on other targets using on OpenAL.
In the time since the original PR was proposed, Chrome has banned autoplaying audio, so here's a modified version of that test program supporting modern browser & desktop via neko/hl targets:
package;
import openfl.display.BitmapData;
import openfl.events.MouseEvent;
import openfl.events.Event;
import openfl.display.InteractiveObject;
import openfl.display.SimpleButton;
import openfl.display.Bitmap;
import openfl.Assets;
import openfl.display.Sprite;
import openfl.media.Sound;
import openfl.events.SampleDataEvent;
import openfl.utils.ByteArray;
class Main extends Sprite {
private var sampleRate:Int;
private var bufferSize:Int = 8192;
private var bpm:Int = 90;
private var numberOfRows:Int = 48; // 48=3/4 time | 64=4/4 time
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, 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;
private var echoing:Bool = true;
private var echoDelay:Float;
private var echoPosition:UInt = 0;
public function new() {
super();
var bitmapData = new BitmapData(120, 80, false, 0x010203FF);
var pressButton = new BitmapData(120, 80, false, 0x008800FF);
var bitmap = new Bitmap(bitmapData);
var pressbitmap = new Bitmap(pressButton);
var button = new SimpleButton(bitmap, bitmap, pressbitmap, bitmap);
button.x = (stage.stageWidth - bitmap.width) / 2;
button.y = (stage.stageHeight - bitmap.height) / 2;
addChild(button);
button.addEventListener(MouseEvent.CLICK, startSound);
}
function startSound(e:MouseEvent) {
var sound:Sound = new Sound();
sampleRate = sound.sampleRate;
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([
"g-2|g-4", "", "", "a-4", "a#4", "", "d-2|a-4", "", "g-4", "", "f-4", "", "g-2|g-4", "", "d-4", "", "", "", "", "", "", "", "d-4", "",
"g-2|g-4", "", "", "a-4", "a#4", "", "c-2|c-5", "", "a#4", "", "c-5", "", "d-2|d-5", "", "", "", "", "", "", "", "", "", "d-5", ""
]);
patterns.push([
"g-2|d-5", "", "", "d#5", "d-5", "", "f-2|c-5", "", "a#4", "", "a-4", "", "d#2|a#4", "", "c-5", "", "a#4", "", "d-2|a-4", "", "", "", "d-4", "",
"g-2|g-4", "", "", "a-4", "g-4", "", "d-2|f-4", "", "d-4", "", "f-4", "", "g-2|g-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();
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;
trace("Here?");
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;
}
}
I've been using this for a recent project and it works exactly as expected. I'm hoping this can get merged eventually as it's a great feature.
I'll second that! At some point I'd love to port my flash based audio tool Bosca Ceoil to haxe, and I've been unable to because SampleDataEvent doesn't work outside of Flash targets. I have my eyes on this 👀
I have this relevant thread bookmarked, which seems to collide here, and which was my most promising lead on this: https://community.openfl.org/t/is-sampledataevent-implemented-for-windows/9222/9
I'll second that! At some point I'd love to port my flash based audio tool Bosca Ceoil to haxe, and I've been unable to because SampleDataEvent doesn't work outside of Flash targets. I have my eyes on this 👀
It was the exact process of porting Bosca Ceoil to haxe that led me to needing this, actually!
I'm convinced there's still some underlying implementation quirks with the approach in this PR but, well, some functionality is better than none!
I have a few PRs that I plan to review after 9.2 is released. I'll add this to the list.
I'll second that! At some point I'd love to port my flash based audio tool Bosca Ceoil to haxe, and I've been unable to because SampleDataEvent doesn't work outside of Flash targets. I have my eyes on this 👀
It was the exact process of porting Bosca Ceoil to haxe that led me to needing this, actually! I'm convinced there's still some underlying implementation quirks with the approach in this PR but, well, some functionality is better than none!
Oh whoa!
We should talk - I'd love to merge your fork as a starting point of a future Bosca Ceoil v3.0 update. Please get in touch! (email address removed)