From bcd776e44174677f1995a49b697f9651f1f692ec Mon Sep 17 00:00:00 2001 From: Yahkub-R <62478788+Yahkub-R@users.noreply.github.com> Date: Tue, 6 Aug 2024 09:46:37 -0400 Subject: [PATCH] Fix AudioStreamPlayer get_playback_position() for web build --- platform/web/audio_driver_web.cpp | 5 + platform/web/audio_driver_web.h | 1 + platform/web/emscripten_helpers.py | 3 + platform/web/export/export_plugin.cpp | 2 + platform/web/godot_audio.h | 1 + platform/web/js/engine/config.js | 2 + .../web/js/libs/audio.position.worklet.js | 50 ++++++++ platform/web/js/libs/library_godot_audio.js | 109 +++++++++++++++++- servers/audio_server.cpp | 11 ++ servers/audio_server.h | 2 + 10 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 platform/web/js/libs/audio.position.worklet.js diff --git a/platform/web/audio_driver_web.cpp b/platform/web/audio_driver_web.cpp index dd986e650cf..7f327ed6a3e 100644 --- a/platform/web/audio_driver_web.cpp +++ b/platform/web/audio_driver_web.cpp @@ -292,6 +292,11 @@ bool AudioDriverWeb::is_sample_playback_active(const Ref &p return godot_audio_sample_is_active(itos(p_playback->get_instance_id()).utf8().get_data()) != 0; } +double AudioDriverWeb::get_sample_playback_position(const Ref &p_playback) { + ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null."); + return godot_audio_get_sample_playback_position(itos(p_playback->get_instance_id()).utf8().get_data()); +} + void AudioDriverWeb::update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale) { ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); godot_audio_sample_update_pitch_scale( diff --git a/platform/web/audio_driver_web.h b/platform/web/audio_driver_web.h index 298ad90fae1..9280094086c 100644 --- a/platform/web/audio_driver_web.h +++ b/platform/web/audio_driver_web.h @@ -95,6 +95,7 @@ public: virtual void stop_sample_playback(const Ref &p_playback) override; virtual void set_sample_playback_pause(const Ref &p_playback, bool p_paused) override; virtual bool is_sample_playback_active(const Ref &p_playback) override; + virtual double get_sample_playback_position(const Ref &p_playback) override; virtual void update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale = 0.0f) override; virtual void set_sample_playback_bus_volumes_linear(const Ref &p_playback, const HashMap> &p_bus_volumes) override; diff --git a/platform/web/emscripten_helpers.py b/platform/web/emscripten_helpers.py index 2cee3e8110d..8fcabb21c74 100644 --- a/platform/web/emscripten_helpers.py +++ b/platform/web/emscripten_helpers.py @@ -51,11 +51,13 @@ def create_template_zip(env, js, wasm, worker, side): js, wasm, "#platform/web/js/libs/audio.worklet.js", + "#platform/web/js/libs/audio.position.worklet.js", ] out_files = [ zip_dir.File(binary_name + ".js"), zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".audio.worklet.js"), + zip_dir.File(binary_name + ".audio.position.worklet.js"), ] if env["threads"]: in_files.append(worker) @@ -74,6 +76,7 @@ def create_template_zip(env, js, wasm, worker, side): "offline.html", "godot.editor.js", "godot.editor.audio.worklet.js", + "godot.editor.audio.position.worklet.js", "logo.svg", "favicon.png", ] diff --git a/platform/web/export/export_plugin.cpp b/platform/web/export/export_plugin.cpp index d83e465e8e5..d8c1b6033d0 100644 --- a/platform/web/export/export_plugin.cpp +++ b/platform/web/export/export_plugin.cpp @@ -242,6 +242,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref &p_prese } cache_files.push_back(name + ".worker.js"); cache_files.push_back(name + ".audio.worklet.js"); + cache_files.push_back(name + ".audio.position.worklet.js"); replaces["___GODOT_CACHE___"] = Variant(cache_files).to_json_string(); // Heavy files that are cached on demand. @@ -835,6 +836,7 @@ Error EditorExportPlatformWeb::_export_project(const Ref &p_ DirAccess::remove_file_or_error(basepath + ".js"); DirAccess::remove_file_or_error(basepath + ".worker.js"); DirAccess::remove_file_or_error(basepath + ".audio.worklet.js"); + DirAccess::remove_file_or_error(basepath + ".audio.position.worklet.js"); DirAccess::remove_file_or_error(basepath + ".service.worker.js"); DirAccess::remove_file_or_error(basepath + ".pck"); DirAccess::remove_file_or_error(basepath + ".png"); diff --git a/platform/web/godot_audio.h b/platform/web/godot_audio.h index 8bebbcf7de1..3f1a4780224 100644 --- a/platform/web/godot_audio.h +++ b/platform/web/godot_audio.h @@ -55,6 +55,7 @@ extern void godot_audio_sample_start(const char *p_playback_object_id, const cha extern void godot_audio_sample_stop(const char *p_playback_object_id); extern void godot_audio_sample_set_pause(const char *p_playback_object_id, bool p_pause); extern int godot_audio_sample_is_active(const char *p_playback_object_id); +extern double godot_audio_get_sample_playback_position(const char *p_playback_object_id); extern void godot_audio_sample_update_pitch_scale(const char *p_playback_object_id, float p_pitch_scale); extern void godot_audio_sample_set_volumes_linear(const char *p_playback_object_id, int *p_buses_buf, int p_buses_size, float *p_volumes_buf, int p_volumes_size); diff --git a/platform/web/js/engine/config.js b/platform/web/js/engine/config.js index 8c4e1b1b248..61b488cf81c 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 00000000000..bf3ac4ae2d2 --- /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 4bca13d2d6f..2331b59fe93 100644 --- a/platform/web/js/libs/library_godot_audio.js +++ b/platform/web/js/libs/library_godot_audio.js @@ -332,6 +332,7 @@ class SampleNodeBus { * startTime?: number * loopMode?: LoopMode * volume?: Float32Array + * start?: boolean * }} SampleNodeOptions */ @@ -424,9 +425,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} */ @@ -443,6 +450,8 @@ class SampleNode { this._source = GodotAudio.ctx.createBufferSource(); /** @type {AudioBufferSourceNode["onended"]} */ this._onended = null; + /** @type {AudioWorkletNode | null} */ + this._positionWorklet = null; this.setPlaybackRate(options.playbackRate ?? 44100); this.loopMode = options.loopMode ?? this.getSample().loopMode ?? 'disabled'; @@ -453,6 +462,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 +474,14 @@ class SampleNode { return this._playbackRate; } + /** + * Gets the playback position. + * @returns {number} + */ + getPlaybackPosition() { + return this._playbackPosition; + } + /** * Sets the playback rate. * @param {number} val Value to set. @@ -512,8 +531,12 @@ class SampleNode { * @returns {void} */ start() { + if (this.isStarted) { + return; + } this._resetSourceStartTime(); this._source.start(this.startTime, this.offset); + this.isStarted = true; } /** @@ -588,18 +611,74 @@ class SampleNode { return this._sampleNodeBuses.get(bus); } + /** + * 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; } @@ -609,6 +688,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); } @@ -647,7 +732,9 @@ class SampleNode { const pauseTime = this.isPaused ? this.pauseTime : 0; + this.connectPositionWorklet(); this._source.start(this.startTime, this.offset + pauseTime); + this.isStarted = true; } /** @@ -1252,7 +1339,7 @@ const _GodotAudio = { startOptions ) { GodotAudio.SampleNode.stopSampleNode(playbackObjectId); - const sampleNode = GodotAudio.SampleNode.create( + GodotAudio.SampleNode.create( { busIndex, id: playbackObjectId, @@ -1260,7 +1347,6 @@ const _GodotAudio = { }, startOptions ); - sampleNode.start(); }, /** @@ -1580,6 +1666,7 @@ const _GodotAudio = { offset, volume, playbackRate: 1, + start: true, }; GodotAudio.start_sample( playbackObjectId, @@ -1625,6 +1712,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', /** diff --git a/servers/audio_server.cpp b/servers/audio_server.cpp index fefb8bfd413..2f01f05a766 100644 --- a/servers/audio_server.cpp +++ b/servers/audio_server.cpp @@ -1369,6 +1369,12 @@ bool AudioServer::is_playback_active(Ref p_playback) { float AudioServer::get_playback_position(Ref p_playback) { ERR_FAIL_COND_V(p_playback.is_null(), 0); + // Samples. + if (p_playback->get_is_sample() && p_playback->get_sample_playback().is_valid()) { + Ref sample_playback = p_playback->get_sample_playback(); + return AudioServer::get_singleton()->get_sample_playback_position(sample_playback); + } + AudioStreamPlaybackListNode *playback_node = _find_playback_list_node(p_playback); if (!playback_node) { return 0; @@ -1835,6 +1841,11 @@ bool AudioServer::is_sample_playback_active(const Ref &p_pl return AudioDriver::get_singleton()->is_sample_playback_active(p_playback); } +double AudioServer::get_sample_playback_position(const Ref &p_playback) { + ERR_FAIL_COND_V_MSG(p_playback.is_null(), false, "Parameter p_playback is null."); + return AudioDriver::get_singleton()->get_sample_playback_position(p_playback); +} + void AudioServer::update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale) { ERR_FAIL_COND_MSG(p_playback.is_null(), "Parameter p_playback is null."); return AudioDriver::get_singleton()->update_sample_playback_pitch_scale(p_playback, p_pitch_scale); diff --git a/servers/audio_server.h b/servers/audio_server.h index 4825e243364..03f3675bba4 100644 --- a/servers/audio_server.h +++ b/servers/audio_server.h @@ -141,6 +141,7 @@ public: virtual void stop_sample_playback(const Ref &p_playback) {} virtual void set_sample_playback_pause(const Ref &p_playback, bool p_paused) {} virtual bool is_sample_playback_active(const Ref &p_playback) { return false; } + virtual double get_sample_playback_position(const Ref &p_playback) { return false; } virtual void update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale = 0.0f) {} virtual void set_sample_playback_bus_volumes_linear(const Ref &p_playback, const HashMap> &p_bus_volumes) {} @@ -482,6 +483,7 @@ public: void stop_sample_playback(const Ref &p_playback); void set_sample_playback_pause(const Ref &p_playback, bool p_paused); bool is_sample_playback_active(const Ref &p_playback); + double get_sample_playback_position(const Ref &p_playback); void update_sample_playback_pitch_scale(const Ref &p_playback, float p_pitch_scale = 0.0f); AudioServer();