Repository: godotengine/godot (language: cpp)

Issue:
v4.4 Sound quickly regresses on the Web: freezes, noises, crackles
### Tested versions

Reproducible in: v4.4.dev3.official [f4af8201b], v4.4.dev2, v4.4.dev1
Not reproducible in: v4.3.stable.official [77dcf97d8]

### System information

Godot v4.4.dev3 - Windows 10.0.19045 - Multi-window, 1 monitor - OpenGL 3 (Compatibility) - GeForce GT 740M - Intel(R) Core(TM) i5-3317U CPU @ 1.70GHz (4 threads)

### Issue description

On the Web over time, the sound begins to freeze, break and crackle terribly. The heavier the stage and the weaker the hardware on which the game is launched, the faster it happens. For example, the sounds in the game I created start to freeze after a minute on a modern computer. On a weak laptop almost immediately. And immediately on a phone. Also, in remote debugging, this degradation of sounds occurs more slowly than when exporting a project.

I tried different solutions, but they all didn't work. Because of this, I had to move the whole project to version 4.3 and that solved the problem.

So at the moment version 4.4 is not playable on web platforms. At least for me.

For testing I created separate projects for 4.4 and 4.3. with a minimum number of objects just so that there is at least some load on the engine.

### Steps to reproduce

Run the project in remote debugging on the Web.
Wait a few minutes (depending on your hardware). My sound began to degrade sharply at the 3rd minute.
If you want, try to run the same project on version 4.3. You won't find any such problems, no matter how much you play.

### Minimal reproduction project (MRP)

[audiotest-4.4.zip](https://github.com/user-attachments/files/17602861/audiotest-4.4.zip)

[audiotest-4.3.zip](https://github.com/user-attachments/files/17602859/audiotest-4.3.zip)



Discussion / hints:
I confirm the problem. It is played on Linux in Chrome and FireFox browsers.
I also confirm the issue. Here is my (work-in-progress) project exported from two different Godot versions:

- [Froge 4.3](https://rburing.itch.io/froge-4-3?secret=LEGEJVXHEuaHXoqmv2zlZ0u1SU) (exported from Godot 4.3-stable)
- [Froge 4.4](https://rburing.itch.io/froge-4-4?secret=ZnPD765il6PkacENN3CyUeStyU) (exported from Godot 4.4-beta1)

Start the game and wait about 2 minutes (the exact time may depend on your device).

The looped music (an .ogg file played by an AudioStreamPlayer) starts to stutter, crackle, and slow down.

Tested on Firefox on Linux.

I can provide the project source upon request.

cc @adamscott 
I've narrowed down the issue, it started on 4.4-dev1. For completeness: [Froge 4.4-dev1](https://rburing.itch.io/froge-4-4-dev1?secret=N7HUGjKguo20JFpm36RWKGrpotM).

My guess is that maybe it has something to do with looping and floating point precision.

Edit: Probably related to

- https://github.com/godotengine/godot/pull/95197



Fix patch:
diff --git a/platform/web/js/libs/audio.position.worklet.js b/platform/web/js/libs/audio.position.worklet.js
index bf3ac4ae2d23..7dbeb8a0adde 100644
--- a/platform/web/js/libs/audio.position.worklet.js
+++ b/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;
 	}
 }
diff --git a/platform/web/js/libs/library_godot_audio.js b/platform/web/js/libs/library_godot_audio.js
index 0c83e3587d84..c5fa7d5f0fad 100644
--- a/platform/web/js/libs/library_godot_audio.js
+++ b/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;
 		},
 


Test patch: