summaryrefslogtreecommitdiffstats
path: root/platform/web/js/libs/library_godot_audio.js
diff options
context:
space:
mode:
Diffstat (limited to 'platform/web/js/libs/library_godot_audio.js')
-rw-r--r--platform/web/js/libs/library_godot_audio.js166
1 files changed, 146 insertions, 20 deletions
diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js
index 531dbdaeab..0ba6eed464 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;
@@ -332,6 +330,7 @@ class SampleNodeBus {
* startTime?: number
* loopMode?: LoopMode
* volume?: Float32Array
+ * start?: boolean
* }} SampleNodeOptions
*/
@@ -413,8 +412,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 +422,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} */
@@ -441,8 +445,10 @@ class SampleNode {
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 +458,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 +471,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 +527,12 @@ class SampleNode {
* @returns {void}
*/
start() {
+ if (this.isStarted) {
+ return;
+ }
this._resetSourceStartTime();
this._source.start(this.startTime, this.offset);
+ this.isStarted = true;
}
/**
@@ -558,7 +578,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 +608,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 +684,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 +715,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 +730,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 +773,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 +904,6 @@ class Bus {
/**
* `Bus` constructor.
- * @constructor
*/
constructor() {
/** @type {Set<SampleNode>} */
@@ -856,7 +947,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 +1073,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 +1173,7 @@ const _GodotAudio = {
// `Bus` class
/**
* Registry of `Bus`es.
- * @type {Bus[]}
+ * @type {Array<Bus>}
*/
buses: null,
/**
@@ -1090,6 +1183,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 +1349,7 @@ const _GodotAudio = {
startOptions
) {
GodotAudio.SampleNode.stopSampleNode(playbackObjectId);
- const sampleNode = GodotAudio.SampleNode.create(
+ GodotAudio.SampleNode.create(
{
busIndex,
id: playbackObjectId,
@@ -1258,7 +1357,6 @@ const _GodotAudio = {
},
startOptions
);
- sampleNode.start();
},
/**
@@ -1297,7 +1395,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}
*/
@@ -1578,6 +1676,7 @@ const _GodotAudio = {
offset,
volume,
playbackRate: 1,
+ start: true,
};
GodotAudio.start_sample(
playbackObjectId,
@@ -1623,6 +1722,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 +1879,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');