Browse Source

Merge pull request #102163 from adamscott/fix-glitched-audio-web

[Web] Fix audio issues with samples and GodotPositionReportingProcessor
Thaddeus Crews 6 months ago
parent
commit
a7e5469155

+ 19 - 4
platform/web/js/libs/audio.position.worklet.js

@@ -28,10 +28,20 @@
 /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.                 */
 /**************************************************************************/
 
+const POST_THRESHOLD_S = 0.1;
+
 class GodotPositionReportingProcessor extends AudioWorkletProcessor {
-	constructor() {
-		super();
+	constructor(...args) {
+		super(...args);
+		this.lastPostTime = currentTime;
 		this.position = 0;
+
+		this.port.onmessage = (event) => {
+			if (event?.data?.type === 'clear') {
+				this.lastPostTime = currentTime;
+				this.position = 0;
+			}
+		};
 	}
 
 	process(inputs, _outputs, _parameters) {
@@ -39,10 +49,15 @@ class GodotPositionReportingProcessor extends AudioWorkletProcessor {
 			const input = inputs[0];
 			if (input.length > 0) {
 				this.position += input[0].length;
-				this.port.postMessage({ 'type': 'position', 'data': this.position });
-				return true;
 			}
 		}
+
+		// Posting messages is expensive. Let's limit the number of posts.
+		if (currentTime - this.lastPostTime > POST_THRESHOLD_S) {
+			this.lastPostTime = currentTime;
+			this.port.postMessage({ 'type': 'position', 'data': this.position });
+		}
+
 		return true;
 	}
 }

+ 42 - 36
platform/web/js/libs/library_godot_audio.js

@@ -460,7 +460,11 @@ class SampleNode {
 		const sampleNodeBus = this.getSampleNodeBus(bus);
 		sampleNodeBus.setVolume(options.volume);
 
-		this.connectPositionWorklet(options.start);
+		this.connectPositionWorklet(options.start).catch((err) => {
+			const newErr = new Error('Failed to create PositionWorklet.');
+			newErr.cause = err;
+			GodotRuntime.error(newErr);
+		});
 	}
 
 	/**
@@ -612,44 +616,34 @@ 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);
-				});
+	async connectPositionWorklet(start) {
+		await GodotAudio.audioPositionWorkletPromise;
+		if (this.isCanceled) {
+			return;
+		}
+		this.getPositionWorklet();
+		this._source.connect(this._positionWorklet);
+		if (start) {
+			this.start();
 		}
 	}
 
 	/**
-	 * Creates the AudioWorkletProcessor used to track playback position.
-	 * @returns {AudioWorkletNode}
+	 * Get a AudioWorkletProcessor from the pool, or create one if no processor is available.
 	 */
-	createPositionWorklet() {
-		const worklet = new AudioWorkletNode(
-			GodotAudio.ctx,
-			'godot-position-reporting-processor'
-		);
-		worklet.port.onmessage = (event) => {
+	getPositionWorklet() {
+		if (this._positionWorklet != null) {
+			return;
+		}
+		if (GodotAudio.audioPositionWorkletPool.length == 0) {
+			this._positionWorklet = new AudioWorkletNode(
+				GodotAudio.ctx,
+				'godot-position-reporting-processor'
+			);
+		} else {
+			this._positionWorklet = GodotAudio.audioPositionWorkletPool.pop();
+		}
+		this._positionWorklet.port.onmessage = (event) => {
 			switch (event.data['type']) {
 			case 'position':
 				this._playbackPosition = (parseInt(event.data.data, 10) / this.getSample().sampleRate) + this.offset;
@@ -658,7 +652,6 @@ class SampleNode {
 				// Do nothing.
 			}
 		};
-		return worklet;
 	}
 
 	/**
@@ -688,6 +681,8 @@ class SampleNode {
 		if (this._positionWorklet) {
 			this._positionWorklet.disconnect();
 			this._positionWorklet.port.onmessage = null;
+			this._positionWorklet.port.postMessage({ type: 'clear' });
+			GodotAudio.audioPositionWorkletPool.push(this._positionWorklet);
 			this._positionWorklet = null;
 		}
 
@@ -731,7 +726,10 @@ class SampleNode {
 		const pauseTime = this.isPaused
 			? this.pauseTime
 			: 0;
-		this.connectPositionWorklet();
+		if (this._positionWorklet != null) {
+			this._positionWorklet.port.postMessage({ type: 'clear' });
+			this._source.connect(this._positionWorklet);
+		}
 		this._source.start(this.startTime, this.offset + pauseTime);
 		this.isStarted = true;
 	}
@@ -1199,6 +1197,10 @@ const _GodotAudio = {
 		driver: null,
 		interval: 0,
 
+		/** @type {Promise} */
+		audioPositionWorkletPromise: null,
+		audioPositionWorkletPool: [],
+
 		/**
 		 * Converts linear volume to Db.
 		 * @param {number} linear Linear value to convert.
@@ -1265,6 +1267,10 @@ const _GodotAudio = {
 				onlatencyupdate(computed_latency);
 			}, 1000);
 			GodotOS.atexit(GodotAudio.close_async);
+
+			const path = GodotConfig.locate_file('godot.audio.position.worklet.js');
+			GodotAudio.audioPositionWorkletPromise = ctx.audioWorklet.addModule(path);
+
 			return ctx.destination.channelCount;
 		},