diff options
Diffstat (limited to 'platform/web/js/libs/library_godot_audio.js')
-rw-r--r-- | platform/web/js/libs/library_godot_audio.js | 166 |
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'); |