ソースを参照

Merge pull request #95197 from yahkr/95128-audio-fix

Fix AudioStreamPlayer `get_playback_position()` for web build
Rémi Verschelde 1 年間 前
コミット
f2fb3353cb

+ 5 - 0
platform/web/audio_driver_web.cpp

@@ -312,6 +312,11 @@ bool AudioDriverWeb::is_sample_playback_active(const Ref<AudioSamplePlayback> &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<AudioSamplePlayback> &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<AudioSamplePlayback> &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(

+ 1 - 0
platform/web/audio_driver_web.h

@@ -96,6 +96,7 @@ public:
 	virtual void stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback) override;
 	virtual void set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused) override;
 	virtual bool is_sample_playback_active(const Ref<AudioSamplePlayback> &p_playback) override;
+	virtual double get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) override;
 	virtual void update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale = 0.0f) override;
 	virtual void set_sample_playback_bus_volumes_linear(const Ref<AudioSamplePlayback> &p_playback, const HashMap<StringName, Vector<AudioFrame>> &p_bus_volumes) override;
 

+ 3 - 0
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",
         ]

+ 2 - 0
platform/web/export/export_plugin.cpp

@@ -242,6 +242,7 @@ Error EditorExportPlatformWeb::_build_pwa(const Ref<EditorExportPreset> &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<EditorExportPreset> &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");

+ 1 - 0
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);
 extern void godot_audio_sample_set_finished_callback(void (*p_callback)(const char *));

+ 2 - 0
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) {

+ 50 - 0
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);

+ 106 - 3
platform/web/js/libs/library_godot_audio.js

@@ -330,6 +330,7 @@ class SampleNodeBus {
  *   startTime?: number
  *   loopMode?: LoopMode
  *   volume?: Float32Array
+ *   start?: boolean
  * }} SampleNodeOptions
  */
 
@@ -421,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} */
@@ -440,6 +447,8 @@ class SampleNode {
 		this._source = GodotAudio.ctx.createBufferSource();
 
 		this._onended = null;
+		/** @type {AudioWorkletNode | null} */
+		this._positionWorklet = null;
 
 		this.setPlaybackRate(options.playbackRate ?? 44100);
 		this._source.buffer = this.getSample().getAudioBuffer();
@@ -449,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);
 	}
 
 	/**
@@ -459,6 +470,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.
@@ -508,8 +527,12 @@ class SampleNode {
 	 * @returns {void}
 	 */
 	start() {
+		if (this.isStarted) {
+			return;
+		}
 		this._resetSourceStartTime();
 		this._source.start(this.startTime, this.offset);
+		this.isStarted = true;
 	}
 
 	/**
@@ -584,18 +607,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;
 		}
@@ -605,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);
 	}
 
@@ -645,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;
 	}
 
 	/**
@@ -1262,7 +1349,7 @@ const _GodotAudio = {
 			startOptions
 		) {
 			GodotAudio.SampleNode.stopSampleNode(playbackObjectId);
-			const sampleNode = GodotAudio.SampleNode.create(
+			GodotAudio.SampleNode.create(
 				{
 					busIndex,
 					id: playbackObjectId,
@@ -1270,7 +1357,6 @@ const _GodotAudio = {
 				},
 				startOptions
 			);
-			sampleNode.start();
 		},
 
 		/**
@@ -1590,6 +1676,7 @@ const _GodotAudio = {
 			offset,
 			volume,
 			playbackRate: 1,
+			start: true,
 		};
 		GodotAudio.start_sample(
 			playbackObjectId,
@@ -1635,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',
 	/**

+ 11 - 0
servers/audio_server.cpp

@@ -1379,6 +1379,12 @@ bool AudioServer::is_playback_active(Ref<AudioStreamPlayback> p_playback) {
 float AudioServer::get_playback_position(Ref<AudioStreamPlayback> 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<AudioSamplePlayback> 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;
@@ -1847,6 +1853,11 @@ bool AudioServer::is_sample_playback_active(const Ref<AudioSamplePlayback> &p_pl
 	return AudioDriver::get_singleton()->is_sample_playback_active(p_playback);
 }
 
+double AudioServer::get_sample_playback_position(const Ref<AudioSamplePlayback> &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<AudioSamplePlayback> &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);

+ 2 - 0
servers/audio_server.h

@@ -141,6 +141,7 @@ public:
 	virtual void stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback) {}
 	virtual void set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused) {}
 	virtual bool is_sample_playback_active(const Ref<AudioSamplePlayback> &p_playback) { return false; }
+	virtual double get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback) { return false; }
 	virtual void update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale = 0.0f) {}
 	virtual void set_sample_playback_bus_volumes_linear(const Ref<AudioSamplePlayback> &p_playback, const HashMap<StringName, Vector<AudioFrame>> &p_bus_volumes) {}
 
@@ -484,6 +485,7 @@ public:
 	void stop_sample_playback(const Ref<AudioSamplePlayback> &p_playback);
 	void set_sample_playback_pause(const Ref<AudioSamplePlayback> &p_playback, bool p_paused);
 	bool is_sample_playback_active(const Ref<AudioSamplePlayback> &p_playback);
+	double get_sample_playback_position(const Ref<AudioSamplePlayback> &p_playback);
 	void update_sample_playback_pitch_scale(const Ref<AudioSamplePlayback> &p_playback, float p_pitch_scale = 0.0f);
 
 	AudioServer();