openfl icon indicating copy to clipboard operation
openfl copied to clipboard

SampleDataEvent

Open X-sam opened this issue 2 years ago • 6 comments

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;
	}
}

X-sam avatar Nov 27 '21 06:11 X-sam

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.

EliteMasterEric avatar Aug 16 '22 07:08 EliteMasterEric

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 👀

TerryCavanagh avatar Aug 16 '22 09:08 TerryCavanagh

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

TerryCavanagh avatar Aug 16 '22 09:08 TerryCavanagh

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!

X-sam avatar Aug 16 '22 12:08 X-sam

I have a few PRs that I plan to review after 9.2 is released. I'll add this to the list.

joshtynjala avatar Aug 16 '22 15:08 joshtynjala

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)

TerryCavanagh avatar Aug 17 '22 18:08 TerryCavanagh