summaryrefslogtreecommitdiffstats
path: root/platform/web/js
diff options
context:
space:
mode:
Diffstat (limited to 'platform/web/js')
-rw-r--r--platform/web/js/engine/config.js2
-rw-r--r--platform/web/js/libs/audio.position.worklet.js50
-rw-r--r--platform/web/js/libs/library_godot_audio.js176
-rw-r--r--platform/web/js/libs/library_godot_input.js1
-rw-r--r--platform/web/js/libs/library_godot_javascript_singleton.js16
5 files changed, 218 insertions, 27 deletions
diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js
index 8c4e1b1b24..61b488cf81 100644
--- a/platform/web/js/engine/config.js
+++ b/platform/web/js/engine/config.js
@@ -299,6 +299,8 @@ const InternalConfig = function (initConfig) { // eslint-disable-line no-unused-
return `${loadPath}.worker.js`;
} else if (path.endsWith('.audio.worklet.js')) {
return `${loadPath}.audio.worklet.js`;
+ } else if (path.endsWith('.audio.position.worklet.js')) {
+ return `${loadPath}.audio.position.worklet.js`;
} else if (path.endsWith('.js')) {
return `${loadPath}.js`;
} else if (path in gdext) {
diff --git a/platform/web/js/libs/audio.position.worklet.js b/platform/web/js/libs/audio.position.worklet.js
new file mode 100644
index 0000000000..bf3ac4ae2d
--- /dev/null
+++ b/platform/web/js/libs/audio.position.worklet.js
@@ -0,0 +1,50 @@
+/**************************************************************************/
+/* godot.audio.position.worklet.js */
+/**************************************************************************/
+/* This file is part of: */
+/* GODOT ENGINE */
+/* https://godotengine.org */
+/**************************************************************************/
+/* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */
+/* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */
+/* */
+/* 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 GodotPositionReportingProcessor extends AudioWorkletProcessor {
+ constructor() {
+ super();
+ this.position = 0;
+ }
+
+ process(inputs, _outputs, _parameters) {
+ if (inputs.length > 0) {
+ const input = inputs[0];
+ if (input.length > 0) {
+ this.position += input[0].length;
+ this.port.postMessage({ 'type': 'position', 'data': this.position });
+ return true;
+ }
+ }
+ return true;
+ }
+}
+
+registerProcessor('godot-position-reporting-processor', GodotPositionReportingProcessor);
diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js
index 531dbdaeab..40fb0c356c 100644
--- a/platform/web/js/libs/library_godot_audio.js
+++ b/platform/web/js/libs/library_godot_audio.js
@@ -77,7 +77,7 @@ class Sample {
* Creates a `Sample` based on the params. Will register it to the
* `GodotAudio.samples` registry.
* @param {SampleParams} params Base params
- * @param {SampleOptions} [options={}] Optional params
+ * @param {SampleOptions} [options={{}}] Optional params
* @returns {Sample}
*/
static create(params, options = {}) {
@@ -98,8 +98,7 @@ class Sample {
/**
* `Sample` constructor.
* @param {SampleParams} params Base params
- * @param {SampleOptions} [options={}] Optional params
- * @constructor
+ * @param {SampleOptions} [options={{}}] Optional params
*/
constructor(params, options = {}) {
/** @type {string} */
@@ -154,7 +153,7 @@ class Sample {
if (this._audioBuffer == null) {
throw new Error('couldn\'t duplicate a null audioBuffer');
}
- /** @type {Float32Array[]} */
+ /** @type {Array<Float32Array>} */
const channels = new Array(this._audioBuffer.numberOfChannels);
for (let i = 0; i < this._audioBuffer.numberOfChannels; i++) {
const channel = new Float32Array(this._audioBuffer.getChannelData(i));
@@ -189,7 +188,6 @@ class SampleNodeBus {
/**
* `SampleNodeBus` constructor.
* @param {Bus} bus The bus related to the new `SampleNodeBus`.
- * @constructor
*/
constructor(bus) {
const NUMBER_OF_WEB_CHANNELS = 6;
@@ -330,8 +328,10 @@ class SampleNodeBus {
* offset?: number
* playbackRate?: number
* startTime?: number
+ * pitchScale?: number
* loopMode?: LoopMode
* volume?: Float32Array
+ * start?: boolean
* }} SampleNodeOptions
*/
@@ -413,8 +413,7 @@ class SampleNode {
/**
* @param {SampleNodeParams} params Base params
- * @param {SampleNodeOptions} [options={}] Optional params
- * @constructor
+ * @param {SampleNodeOptions} [options={{}}] Optional params
*/
constructor(params, options = {}) {
/** @type {string} */
@@ -424,9 +423,15 @@ class SampleNode {
/** @type {number} */
this.offset = options.offset ?? 0;
/** @type {number} */
+ this._playbackPosition = options.offset;
+ /** @type {number} */
this.startTime = options.startTime ?? 0;
/** @type {boolean} */
this.isPaused = false;
+ /** @type {boolean} */
+ this.isStarted = false;
+ /** @type {boolean} */
+ this.isCanceled = false;
/** @type {number} */
this.pauseTime = 0;
/** @type {number} */
@@ -434,15 +439,17 @@ class SampleNode {
/** @type {LoopMode} */
this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled';
/** @type {number} */
- this._pitchScale = 1;
+ this._pitchScale = options.pitchScale ?? 1;
/** @type {number} */
this._sourceStartTime = 0;
/** @type {Map<Bus, SampleNodeBus>} */
this._sampleNodeBuses = new Map();
/** @type {AudioBufferSourceNode | null} */
this._source = GodotAudio.ctx.createBufferSource();
- /** @type {AudioBufferSourceNode["onended"]} */
+
this._onended = null;
+ /** @type {AudioWorkletNode | null} */
+ this._positionWorklet = null;
this.setPlaybackRate(options.playbackRate ?? 44100);
this._source.buffer = this.getSample().getAudioBuffer();
@@ -452,6 +459,8 @@ class SampleNode {
const bus = GodotAudio.Bus.getBus(params.busIndex);
const sampleNodeBus = this.getSampleNodeBus(bus);
sampleNodeBus.setVolume(options.volume);
+
+ this.connectPositionWorklet(options.start);
}
/**
@@ -463,6 +472,14 @@ class SampleNode {
}
/**
+ * Gets the playback position.
+ * @returns {number}
+ */
+ getPlaybackPosition() {
+ return this._playbackPosition;
+ }
+
+ /**
* Sets the playback rate.
* @param {number} val Value to set.
* @returns {void}
@@ -511,8 +528,12 @@ class SampleNode {
* @returns {void}
*/
start() {
+ if (this.isStarted) {
+ return;
+ }
this._resetSourceStartTime();
this._source.start(this.startTime, this.offset);
+ this.isStarted = true;
}
/**
@@ -558,7 +579,7 @@ class SampleNode {
/**
* Sets the volumes of the `SampleNode` for each buses passed in parameters.
- * @param {Bus[]} buses
+ * @param {Array<Bus>} buses
* @param {Float32Array} volumes
*/
setVolumes(buses, volumes) {
@@ -588,17 +609,73 @@ class SampleNode {
}
/**
+ * Sets up and connects the source to the GodotPositionReportingProcessor
+ * If the worklet module is not loaded in, it will be added
+ */
+ connectPositionWorklet(start) {
+ try {
+ this._positionWorklet = this.createPositionWorklet();
+ this._source.connect(this._positionWorklet);
+ if (start) {
+ this.start();
+ }
+ } catch (error) {
+ if (error?.name !== 'InvalidStateError') {
+ throw error;
+ }
+ const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
+ GodotAudio.ctx.audioWorklet
+ .addModule(path)
+ .then(() => {
+ if (!this.isCanceled) {
+ this._positionWorklet = this.createPositionWorklet();
+ this._source.connect(this._positionWorklet);
+ if (start) {
+ this.start();
+ }
+ }
+ }).catch((addModuleError) => {
+ GodotRuntime.error('Failed to create PositionWorklet.', addModuleError);
+ });
+ }
+ }
+
+ /**
+ * Creates the AudioWorkletProcessor used to track playback position.
+ * @returns {AudioWorkletNode}
+ */
+ createPositionWorklet() {
+ const worklet = new AudioWorkletNode(
+ GodotAudio.ctx,
+ 'godot-position-reporting-processor'
+ );
+ worklet.port.onmessage = (event) => {
+ switch (event.data['type']) {
+ case 'position':
+ this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
+ break;
+ default:
+ // Do nothing.
+ }
+ };
+ return worklet;
+ }
+
+ /**
* Clears the `SampleNode`.
* @returns {void}
*/
clear() {
+ this.isCanceled = true;
this.isPaused = false;
this.pauseTime = 0;
if (this._source != null) {
this._source.removeEventListener('ended', this._onended);
this._onended = null;
- this._source.stop();
+ if (this.isStarted) {
+ this._source.stop();
+ }
this._source.disconnect();
this._source = null;
}
@@ -608,6 +685,12 @@ class SampleNode {
}
this._sampleNodeBuses.clear();
+ if (this._positionWorklet) {
+ this._positionWorklet.disconnect();
+ this._positionWorklet.port.onmessage = null;
+ this._positionWorklet = null;
+ }
+
GodotAudio.SampleNode.delete(this.id);
}
@@ -633,7 +716,9 @@ class SampleNode {
* @returns {void}
*/
_restart() {
- this._source.disconnect();
+ if (this._source != null) {
+ this._source.disconnect();
+ }
this._source = GodotAudio.ctx.createBufferSource();
this._source.buffer = this.getSample().getAudioBuffer();
@@ -646,7 +731,9 @@ class SampleNode {
const pauseTime = this.isPaused
? this.pauseTime
: 0;
+ this.connectPositionWorklet();
this._source.start(this.startTime, this.offset + pauseTime);
+ this.isStarted = true;
}
/**
@@ -687,9 +774,15 @@ class SampleNode {
}
switch (self.getSample().loopMode) {
- case 'disabled':
+ case 'disabled': {
+ const id = this.id;
self.stop();
- break;
+ if (GodotAudio.sampleFinishedCallback != null) {
+ const idCharPtr = GodotRuntime.allocString(id);
+ GodotAudio.sampleFinishedCallback(idCharPtr);
+ GodotRuntime.free(idCharPtr);
+ }
+ } break;
case 'forward':
case 'backward':
self.restart();
@@ -812,7 +905,6 @@ class Bus {
/**
* `Bus` constructor.
- * @constructor
*/
constructor() {
/** @type {Set<SampleNode>} */
@@ -856,7 +948,10 @@ class Bus {
* @returns {void}
*/
setVolumeDb(val) {
- this._gainNode.gain.value = GodotAudio.db_to_linear(val);
+ const linear = GodotAudio.db_to_linear(val);
+ if (isFinite(linear)) {
+ this._gainNode.gain.value = linear;
+ }
}
/**
@@ -979,7 +1074,6 @@ class Bus {
GodotAudio.buses = GodotAudio.buses.filter((v) => v !== this);
}
- /** @type {Bus["prototype"]["_syncSampleNodes"]} */
_syncSampleNodes() {
const sampleNodes = Array.from(this._sampleNodes);
for (let i = 0; i < sampleNodes.length; i++) {
@@ -1080,7 +1174,7 @@ const _GodotAudio = {
// `Bus` class
/**
* Registry of `Bus`es.
- * @type {Bus[]}
+ * @type {Array<Bus>}
*/
buses: null,
/**
@@ -1090,6 +1184,12 @@ const _GodotAudio = {
busSolo: null,
Bus,
+ /**
+ * Callback to signal that a sample has finished.
+ * @type {(playbackObjectIdPtr: number) => void | null}
+ */
+ sampleFinishedCallback: null,
+
/** @type {AudioContext} */
ctx: null,
input: null,
@@ -1250,7 +1350,7 @@ const _GodotAudio = {
startOptions
) {
GodotAudio.SampleNode.stopSampleNode(playbackObjectId);
- const sampleNode = GodotAudio.SampleNode.create(
+ GodotAudio.SampleNode.create(
{
busIndex,
id: playbackObjectId,
@@ -1258,7 +1358,6 @@ const _GodotAudio = {
},
startOptions
);
- sampleNode.start();
},
/**
@@ -1297,7 +1396,7 @@ const _GodotAudio = {
/**
* Triggered when a sample node volumes need to be updated.
* @param {string} playbackObjectId Id of the sample playback
- * @param {number[]} busIndexes Indexes of the buses that need to be updated
+ * @param {Array<number>} busIndexes Indexes of the buses that need to be updated
* @param {Float32Array} volumes Array of the volumes
* @returns {void}
*/
@@ -1550,13 +1649,14 @@ const _GodotAudio = {
},
godot_audio_sample_start__proxy: 'sync',
- godot_audio_sample_start__sig: 'viiiii',
+ godot_audio_sample_start__sig: 'viiiifi',
/**
* Starts a sample.
* @param {number} playbackObjectIdStrPtr Playback object id pointer
* @param {number} streamObjectIdStrPtr Stream object id pointer
* @param {number} busIndex Bus index
* @param {number} offset Sample offset
+ * @param {number} pitchScale Pitch scale
* @param {number} volumePtr Volume pointer
* @returns {void}
*/
@@ -1565,6 +1665,7 @@ const _GodotAudio = {
streamObjectIdStrPtr,
busIndex,
offset,
+ pitchScale,
volumePtr
) {
/** @type {string} */
@@ -1573,11 +1674,13 @@ const _GodotAudio = {
const streamObjectId = GodotRuntime.parseString(streamObjectIdStrPtr);
/** @type {Float32Array} */
const volume = GodotRuntime.heapSub(HEAPF32, volumePtr, 8);
- /** @type {SampleNodeConstructorOptions} */
+ /** @type {SampleNodeOptions} */
const startOptions = {
offset,
volume,
playbackRate: 1,
+ pitchScale,
+ start: true,
};
GodotAudio.start_sample(
playbackObjectId,
@@ -1623,6 +1726,22 @@ const _GodotAudio = {
return Number(GodotAudio.sampleNodes.has(playbackObjectId));
},
+ godot_audio_get_sample_playback_position__proxy: 'sync',
+ godot_audio_get_sample_playback_position__sig: 'di',
+ /**
+ * Returns the position of the playback position.
+ * @param {number} playbackObjectIdStrPtr Playback object id pointer
+ * @returns {number}
+ */
+ godot_audio_get_sample_playback_position: function (playbackObjectIdStrPtr) {
+ const playbackObjectId = GodotRuntime.parseString(playbackObjectIdStrPtr);
+ const sampleNode = GodotAudio.SampleNode.getSampleNodeOrNull(playbackObjectId);
+ if (sampleNode == null) {
+ return 0;
+ }
+ return sampleNode.getPlaybackPosition();
+ },
+
godot_audio_sample_update_pitch_scale__proxy: 'sync',
godot_audio_sample_update_pitch_scale__sig: 'vii',
/**
@@ -1764,6 +1883,17 @@ const _GodotAudio = {
godot_audio_sample_bus_set_mute: function (bus, enable) {
GodotAudio.set_sample_bus_mute(bus, Boolean(enable));
},
+
+ godot_audio_sample_set_finished_callback__proxy: 'sync',
+ godot_audio_sample_set_finished_callback__sig: 'vi',
+ /**
+ * Sets the finished callback
+ * @param {Number} callbackPtr Finished callback pointer
+ * @returns {void}
+ */
+ godot_audio_sample_set_finished_callback: function (callbackPtr) {
+ GodotAudio.sampleFinishedCallback = GodotRuntime.get_func(callbackPtr);
+ },
};
autoAddDeps(_GodotAudio, '$GodotAudio');
diff --git a/platform/web/js/libs/library_godot_input.js b/platform/web/js/libs/library_godot_input.js
index 7ea89d553f..6e3b97023d 100644
--- a/platform/web/js/libs/library_godot_input.js
+++ b/platform/web/js/libs/library_godot_input.js
@@ -112,6 +112,7 @@ const GodotIME = {
ime.style.top = '0px';
ime.style.width = '100%';
ime.style.height = '40px';
+ ime.style.pointerEvents = 'none';
ime.style.display = 'none';
ime.contentEditable = 'true';
diff --git a/platform/web/js/libs/library_godot_javascript_singleton.js b/platform/web/js/libs/library_godot_javascript_singleton.js
index b17fde1544..6bb69bca95 100644
--- a/platform/web/js/libs/library_godot_javascript_singleton.js
+++ b/platform/web/js/libs/library_godot_javascript_singleton.js
@@ -81,11 +81,16 @@ const GodotJSWrapper = {
case 0:
return null;
case 1:
- return !!GodotRuntime.getHeapValue(val, 'i64');
- case 2:
- return GodotRuntime.getHeapValue(val, 'i64');
+ return Boolean(GodotRuntime.getHeapValue(val, 'i64'));
+ case 2: {
+ // `heap_value` may be a bigint.
+ const heap_value = GodotRuntime.getHeapValue(val, 'i64');
+ return heap_value >= Number.MIN_SAFE_INTEGER && heap_value <= Number.MAX_SAFE_INTEGER
+ ? Number(heap_value)
+ : heap_value;
+ }
case 3:
- return GodotRuntime.getHeapValue(val, 'double');
+ return Number(GodotRuntime.getHeapValue(val, 'double'));
case 4:
return GodotRuntime.parseString(GodotRuntime.getHeapValue(val, '*'));
case 24: // OBJECT
@@ -110,6 +115,9 @@ const GodotJSWrapper = {
}
GodotRuntime.setHeapValue(p_exchange, p_val, 'double');
return 3; // FLOAT
+ } else if (type === 'bigint') {
+ GodotRuntime.setHeapValue(p_exchange, p_val, 'i64');
+ return 2; // INT
} else if (type === 'string') {
const c_str = GodotRuntime.allocString(p_val);
GodotRuntime.setHeapValue(p_exchange, c_str, '*');