summaryrefslogtreecommitdiff
path: root/platform/javascript/native
diff options
context:
space:
mode:
Diffstat (limited to 'platform/javascript/native')
-rw-r--r--platform/javascript/native/audio.worklet.js186
-rw-r--r--platform/javascript/native/library_godot_audio.js357
2 files changed, 449 insertions, 94 deletions
diff --git a/platform/javascript/native/audio.worklet.js b/platform/javascript/native/audio.worklet.js
new file mode 100644
index 0000000000..ad7957e45c
--- /dev/null
+++ b/platform/javascript/native/audio.worklet.js
@@ -0,0 +1,186 @@
+/*************************************************************************/
+/* audio.worklet.js */
+/*************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/*************************************************************************/
+/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* */
+/* Permission is hereby granted, free of charge, to any person obtaining */
+/* a copy of this software and associated documentation files (the */
+/* "Software"), to deal in the Software without restriction, including */
+/* without limitation the rights to use, copy, modify, merge, publish, */
+/* distribute, sublicense, and/or sell copies of the Software, and to */
+/* permit persons to whom the Software is furnished to do so, subject to */
+/* the following conditions: */
+/* */
+/* The above copyright notice and this permission notice shall be */
+/* included in all copies or substantial portions of the Software. */
+/* */
+/* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */
+/* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */
+/* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.*/
+/* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */
+/* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */
+/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
+/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
+/*************************************************************************/
+class RingBuffer {
+
+ constructor(p_buffer, p_state) {
+ this.buffer = p_buffer;
+ this.avail = p_state;
+ this.rpos = 0;
+ this.wpos = 0;
+ }
+
+ data_left() {
+ return Atomics.load(this.avail, 0);
+ }
+
+ space_left() {
+ return this.buffer.length - this.data_left();
+ }
+
+ read(output) {
+ const size = this.buffer.length;
+ let from = 0
+ let to_write = output.length;
+ if (this.rpos + to_write > size) {
+ const high = size - this.rpos;
+ output.set(this.buffer.subarray(this.rpos, size));
+ from = high;
+ to_write -= high;
+ this.rpos = 0;
+ }
+ output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+ this.rpos += to_write;
+ Atomics.add(this.avail, 0, -output.length);
+ Atomics.notify(this.avail, 0);
+ }
+
+ write(p_buffer) {
+ const to_write = p_buffer.length;
+ const mw = this.buffer.length - this.wpos;
+ if (mw >= to_write) {
+ this.buffer.set(p_buffer, this.wpos);
+ } else {
+ const high = p_buffer.subarray(0, to_write - mw);
+ const low = p_buffer.subarray(to_write - mw);
+ this.buffer.set(high, this.wpos);
+ this.buffer.set(low);
+ }
+ let diff = to_write;
+ if (this.wpos + diff >= this.buffer.length) {
+ diff -= this.buffer.length;
+ }
+ this.wpos += diff;
+ Atomics.add(this.avail, 0, to_write);
+ Atomics.notify(this.avail, 0);
+ }
+}
+
+class GodotProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ this.running = true;
+ this.lock = null;
+ this.notifier = null;
+ this.output = null;
+ this.output_buffer = new Float32Array();
+ this.input = null;
+ this.input_buffer = new Float32Array();
+ this.port.onmessage = (event) => {
+ const cmd = event.data['cmd'];
+ const data = event.data['data'];
+ this.parse_message(cmd, data);
+ };
+ }
+
+ process_notify() {
+ Atomics.add(this.notifier, 0, 1);
+ Atomics.notify(this.notifier, 0);
+ }
+
+ parse_message(p_cmd, p_data) {
+ if (p_cmd == "start" && p_data) {
+ const state = p_data[0];
+ let idx = 0;
+ this.lock = state.subarray(idx, ++idx);
+ this.notifier = state.subarray(idx, ++idx);
+ const avail_in = state.subarray(idx, ++idx);
+ const avail_out = state.subarray(idx, ++idx);
+ this.input = new RingBuffer(p_data[1], avail_in);
+ this.output = new RingBuffer(p_data[2], avail_out);
+ } else if (p_cmd == "stop") {
+ this.runing = false;
+ this.output = null;
+ this.input = null;
+ }
+ }
+
+ array_has_data(arr) {
+ return arr.length && arr[0].length && arr[0][0].length;
+ }
+
+ process(inputs, outputs, parameters) {
+ if (!this.running) {
+ return false; // Stop processing.
+ }
+ if (this.output === null) {
+ return true; // Not ready yet, keep processing.
+ }
+ const process_input = this.array_has_data(inputs);
+ if (process_input) {
+ const input = inputs[0];
+ const chunk = input[0].length * input.length;
+ if (this.input_buffer.length != chunk) {
+ this.input_buffer = new Float32Array(chunk);
+ }
+ if (this.input.space_left() >= chunk) {
+ this.write_input(this.input_buffer, input);
+ this.input.write(this.input_buffer);
+ } else {
+ this.port.postMessage("Input buffer is full! Skipping input frame.");
+ }
+ }
+ const process_output = this.array_has_data(outputs);
+ if (process_output) {
+ const output = outputs[0];
+ const chunk = output[0].length * output.length;
+ if (this.output_buffer.length != chunk) {
+ this.output_buffer = new Float32Array(chunk)
+ }
+ if (this.output.data_left() >= chunk) {
+ this.output.read(this.output_buffer);
+ this.write_output(output, this.output_buffer);
+ } else {
+ this.port.postMessage("Output buffer has not enough frames! Skipping output frame.");
+ }
+ }
+ this.process_notify();
+ return true;
+ }
+
+ write_output(dest, source) {
+ const channels = dest.length;
+ for (let ch = 0; ch < channels; ch++) {
+ for (let sample = 0; sample < dest[ch].length; sample++) {
+ dest[ch][sample] = source[sample * channels + ch];
+ }
+ }
+ }
+
+ write_input(dest, source) {
+ const channels = source.length;
+ for (let ch = 0; ch < channels; ch++) {
+ for (let sample = 0; sample < source[ch].length; sample++) {
+ dest[sample * channels + ch] = source[ch][sample];
+ }
+ }
+ }
+}
+
+registerProcessor('godot-processor', GodotProcessor);
diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js
index 1e6f787657..846359b8b2 100644
--- a/platform/javascript/native/library_godot_audio.js
+++ b/platform/javascript/native/library_godot_audio.js
@@ -27,89 +27,123 @@
/* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */
/* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */
/*************************************************************************/
-var GodotAudio = {
+
+const GodotAudio = {
$GodotAudio__deps: ['$GodotOS'],
$GodotAudio: {
-
ctx: null,
input: null,
- script: null,
- },
+ driver: null,
+ interval: 0,
- godot_audio_is_available__proxy: 'sync',
- godot_audio_is_available: function () {
- if (!(window.AudioContext || window.webkitAudioContext)) {
- return 0;
- }
- return 1;
- },
+ init: function(mix_rate, latency, onstatechange, onlatencyupdate) {
+ const ctx = new (window.AudioContext || window.webkitAudioContext)({
+ sampleRate: mix_rate,
+ // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance.
+ });
+ GodotAudio.ctx = ctx;
+ onstatechange(ctx.state); // Immeditately notify state.
+ ctx.onstatechange = function() {
+ let state = 0;
+ switch (ctx.state) {
+ case 'suspended':
+ state = 0;
+ break;
+ case 'running':
+ state = 1;
+ break;
+ case 'closed':
+ state = 2;
+ break;
+ }
+ onstatechange(state);
+ }
+ // Update computed latency
+ GodotAudio.interval = setInterval(function() {
+ let latency = 0;
+ if (ctx.baseLatency) {
+ latency += GodotAudio.ctx.baseLatency;
+ }
+ if (ctx.outputLatency) {
+ latency += GodotAudio.ctx.outputLatency;
+ }
+ onlatencyupdate(latency);
+ }, 1000);
+ GodotOS.atexit(GodotAudio.close_async);
+ return ctx.destination.channelCount;
+ },
- godot_audio_init: function(mix_rate, latency) {
- GodotAudio.ctx = new (window.AudioContext || window.webkitAudioContext)({
- sampleRate: mix_rate,
- // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance.
- });
- GodotOS.atexit(function(accept, reject) {
- if (!GodotAudio.ctx) {
- accept();
+ create_input: function(callback) {
+ if (GodotAudio.input) {
+ return; // Already started.
+ }
+ function gotMediaInput(stream) {
+ GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
+ callback(GodotAudio.input)
+ }
+ if (navigator.mediaDevices.getUserMedia) {
+ navigator.mediaDevices.getUserMedia({
+ "audio": true
+ }).then(gotMediaInput, function(e) { out(e) });
+ } else {
+ if (!navigator.getUserMedia) {
+ navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
+ }
+ navigator.getUserMedia({
+ "audio": true
+ }, gotMediaInput, function(e) { out(e) });
+ }
+ },
+
+ close_async: function(resolve, reject) {
+ const ctx = GodotAudio.ctx;
+ GodotAudio.ctx = null;
+ // Audio was not initialized.
+ if (!ctx) {
+ resolve();
return;
}
- if (GodotAudio.script) {
- GodotAudio.script.disconnect();
- GodotAudio.script.onaudioprocess = null;
- GodotAudio.script = null;
+ // Remove latency callback
+ if (GodotAudio.interval) {
+ clearInterval(GodotAudio.interval);
+ GodotAudio.interval = 0;
}
+ // Disconnect input, if it was started.
if (GodotAudio.input) {
GodotAudio.input.disconnect();
GodotAudio.input = null;
}
- GodotAudio.ctx.close().then(function() {
- accept();
+ // Disconnect output
+ let closed = Promise.resolve();
+ if (GodotAudio.driver) {
+ closed = GodotAudio.driver.close();
+ }
+ closed.then(function() {
+ return ctx.close();
+ }).then(function() {
+ ctx.onstatechange = null;
+ resolve();
}).catch(function(e) {
- accept();
+ ctx.onstatechange = null;
+ console.error("Error closing AudioContext", e);
+ resolve();
});
- GodotAudio.ctx = null;
- });
- return GodotAudio.ctx.destination.channelCount;
+ },
},
- godot_audio_create_processor: function(buffer_length, channel_count) {
- GodotAudio.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count);
- GodotAudio.script.connect(GodotAudio.ctx.destination);
- return GodotAudio.script.bufferSize;
+ godot_audio_is_available__proxy: 'sync',
+ godot_audio_is_available: function () {
+ if (!(window.AudioContext || window.webkitAudioContext)) {
+ return 0;
+ }
+ return 1;
},
- godot_audio_start: function(buffer_ptr, p_process_start, p_process_end, p_process_capture) {
- const audioDriverProcessStart = GodotOS.get_func(p_process_start);
- const audioDriverProcessEnd = GodotOS.get_func(p_process_end);
- const audioDriverProcessCapture = GodotOS.get_func(p_process_capture);
- GodotAudio.script.onaudioprocess = function(audioProcessingEvent) {
- audioDriverProcessStart();
-
- var input = audioProcessingEvent.inputBuffer;
- var output = audioProcessingEvent.outputBuffer;
- var internalBuffer = HEAPF32.subarray(
- buffer_ptr / HEAPF32.BYTES_PER_ELEMENT,
- buffer_ptr / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels);
- for (var channel = 0; channel < output.numberOfChannels; channel++) {
- var outputData = output.getChannelData(channel);
- // Loop through samples.
- for (var sample = 0; sample < outputData.length; sample++) {
- outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel];
- }
- }
-
- if (GodotAudio.input) {
- var inputDataL = input.getChannelData(0);
- var inputDataR = input.getChannelData(1);
- for (var i = 0; i < inputDataL.length; i++) {
- audioDriverProcessCapture(inputDataL[i]);
- audioDriverProcessCapture(inputDataR[i]);
- }
- }
- audioDriverProcessEnd();
- };
+ godot_audio_init: function(p_mix_rate, p_latency, p_state_change, p_latency_update) {
+ const statechange = GodotOS.get_func(p_state_change);
+ const latencyupdate = GodotOS.get_func(p_latency_update);
+ return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate);
},
godot_audio_resume: function() {
@@ -118,48 +152,21 @@ var GodotAudio = {
}
},
- godot_audio_get_latency__proxy: 'sync',
- godot_audio_get_latency: function() {
- var latency = 0;
- if (GodotAudio.ctx) {
- if (GodotAudio.ctx.baseLatency) {
- latency += GodotAudio.ctx.baseLatency;
- }
- if (GodotAudio.ctx.outputLatency) {
- latency += GodotAudio.ctx.outputLatency;
- }
- }
- return latency;
- },
-
godot_audio_capture_start__proxy: 'sync',
godot_audio_capture_start: function() {
if (GodotAudio.input) {
return; // Already started.
}
- function gotMediaInput(stream) {
- GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream);
- GodotAudio.input.connect(GodotAudio.script);
- }
-
- function gotMediaInputError(e) {
- out(e);
- }
-
- if (navigator.mediaDevices.getUserMedia) {
- navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError);
- } else {
- if (!navigator.getUserMedia)
- navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia;
- navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError);
- }
+ GodotAudio.create_input(function(input) {
+ input.connect(GodotAudio.driver.get_node());
+ });
},
godot_audio_capture_stop__proxy: 'sync',
godot_audio_capture_stop: function() {
if (GodotAudio.input) {
const tracks = GodotAudio.input['mediaStream']['getTracks']();
- for (var i = 0; i < tracks.length; i++) {
+ for (let i = 0; i < tracks.length; i++) {
tracks[i]['stop']();
}
GodotAudio.input.disconnect();
@@ -170,3 +177,165 @@ var GodotAudio = {
autoAddDeps(GodotAudio, "$GodotAudio");
mergeInto(LibraryManager.library, GodotAudio);
+
+/**
+ * The AudioWorklet API driver, used when threads are available.
+ */
+const GodotAudioWorklet = {
+
+ $GodotAudioWorklet__deps: ['$GodotAudio'],
+ $GodotAudioWorklet: {
+ promise: null,
+ worklet: null,
+
+ create: function(channels) {
+ const path = Module['locateFile']('godot.audio.worklet.js');
+ GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function() {
+ GodotAudioWorklet.worklet = new AudioWorkletNode(
+ GodotAudio.ctx,
+ 'godot-processor',
+ {
+ 'outputChannelCount': [channels]
+ }
+ );
+ return Promise.resolve();
+ });
+ GodotAudio.driver = GodotAudioWorklet;
+ },
+
+ start: function(in_buf, out_buf, state) {
+ GodotAudioWorklet.promise.then(function() {
+ const node = GodotAudioWorklet.worklet;
+ node.connect(GodotAudio.ctx.destination);
+ node.port.postMessage({
+ 'cmd': 'start',
+ 'data': [state, in_buf, out_buf],
+ });
+ node.port.onmessage = function(event) {
+ console.error(event.data);
+ };
+ });
+ },
+
+ get_node: function() {
+ return GodotAudioWorklet.worklet;
+ },
+
+ close: function() {
+ return new Promise(function(resolve, reject) {
+ GodotAudioWorklet.promise.then(function() {
+ GodotAudioWorklet.worklet.port.postMessage({
+ 'cmd': 'stop',
+ 'data': null,
+ });
+ GodotAudioWorklet.worklet.disconnect();
+ GodotAudioWorklet.worklet = null;
+ GodotAudioWorklet.promise = null;
+ resolve();
+ });
+ });
+ },
+ },
+
+ godot_audio_worklet_create: function(channels) {
+ GodotAudioWorklet.create(channels);
+ },
+
+ godot_audio_worklet_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) {
+ const out_buffer = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size);
+ const in_buffer = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size);
+ const state = GodotOS.heapSub(HEAP32, p_state, 4);
+ GodotAudioWorklet.start(in_buffer, out_buffer, state);
+ },
+
+ godot_audio_worklet_state_wait: function(p_state, p_idx, p_expected, p_timeout) {
+ Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
+ return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
+ },
+
+ godot_audio_worklet_state_add: function(p_state, p_idx, p_value) {
+ return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value);
+ },
+
+ godot_audio_worklet_state_get: function(p_state, p_idx) {
+ return Atomics.load(HEAP32, (p_state >> 2) + p_idx);
+ },
+};
+
+autoAddDeps(GodotAudioWorklet, "$GodotAudioWorklet");
+mergeInto(LibraryManager.library, GodotAudioWorklet);
+
+/*
+ * The deprecated ScriptProcessorNode API, used when threads are disabled.
+ */
+const GodotAudioScript = {
+
+ $GodotAudioScript__deps: ['$GodotAudio'],
+ $GodotAudioScript: {
+ script: null,
+
+ create: function(buffer_length, channel_count) {
+ GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count);
+ GodotAudio.driver = GodotAudioScript;
+ return GodotAudioScript.script.bufferSize;
+ },
+
+ start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) {
+ GodotAudioScript.script.onaudioprocess = function(event) {
+ // Read input
+ const inb = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size);
+ const input = event.inputBuffer;
+ if (GodotAudio.input) {
+ const inlen = input.getChannelData(0).length;
+ for (let ch = 0; ch < 2; ch++) {
+ const data = input.getChannelData(ch);
+ for (let s = 0; s < inlen; s++) {
+ inb[s * 2 + ch] = data[s];
+ }
+ }
+ }
+
+ // Let Godot process the input/output.
+ onprocess();
+
+ // Write the output.
+ const outb = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size);
+ const output = event.outputBuffer;
+ const channels = output.numberOfChannels;
+ for (let ch = 0; ch < channels; ch++) {
+ const data = output.getChannelData(ch);
+ // Loop through samples and assign computed values.
+ for (let sample = 0; sample < data.length; sample++) {
+ data[sample] = outb[sample * channels + ch];
+ }
+ }
+ };
+ GodotAudioScript.script.connect(GodotAudio.ctx.destination);
+ },
+
+ get_node: function() {
+ return GodotAudioScript.script;
+ },
+
+ close: function() {
+ return new Promise(function(resolve, reject) {
+ GodotAudioScript.script.disconnect();
+ GodotAudioScript.script.onaudioprocess = null;
+ GodotAudioScript.script = null;
+ resolve();
+ });
+ },
+ },
+
+ godot_audio_script_create: function(buffer_length, channel_count) {
+ return GodotAudioScript.create(buffer_length, channel_count);
+ },
+
+ godot_audio_script_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) {
+ const onprocess = GodotOS.get_func(p_cb);
+ GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess);
+ },
+};
+
+autoAddDeps(GodotAudioScript, "$GodotAudioScript");
+mergeInto(LibraryManager.library, GodotAudioScript);