Browse Source

Merge pull request #52650 from Faless/js/3.x_audioworklet_nothreads_pr

[3.x] [HTML5] Refactor audio drivers. Implement AudioWorklet w/o threads.
Rémi Verschelde 4 years ago
parent
commit
8a48be6980

+ 74 - 67
platform/javascript/audio_driver_javascript.cpp

@@ -34,22 +34,18 @@
 
 #include <emscripten.h>
 
-AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr;
+AudioDriverJavaScript::AudioContext AudioDriverJavaScript::audio_context;
 
 bool AudioDriverJavaScript::is_available() {
 	return godot_audio_is_available() != 0;
 }
 
-const char *AudioDriverJavaScript::get_name() const {
-	return "JavaScript";
-}
-
 void AudioDriverJavaScript::_state_change_callback(int p_state) {
-	singleton->state = p_state;
+	AudioDriverJavaScript::audio_context.state = p_state;
 }
 
 void AudioDriverJavaScript::_latency_update_callback(float p_latency) {
-	singleton->output_latency = p_latency;
+	AudioDriverJavaScript::audio_context.output_latency = p_latency;
 }
 
 void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) {
@@ -105,17 +101,19 @@ void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) {
 }
 
 Error AudioDriverJavaScript::init() {
-	mix_rate = GLOBAL_GET("audio/mix_rate");
 	int latency = GLOBAL_GET("audio/output_latency");
-
-	channel_count = godot_audio_init(&mix_rate, latency, &_state_change_callback, &_latency_update_callback);
+	if (!audio_context.inited) {
+		audio_context.mix_rate = GLOBAL_GET("audio/mix_rate");
+		audio_context.channel_count = godot_audio_init(&audio_context.mix_rate, latency, &_state_change_callback, &_latency_update_callback);
+		audio_context.inited = true;
+	}
+	mix_rate = audio_context.mix_rate;
+	channel_count = audio_context.channel_count;
 	buffer_length = closest_power_of_2((latency * mix_rate / 1000));
-#ifndef NO_THREADS
-	node = memnew(WorkletNode);
-#else
-	node = memnew(ScriptProcessorNode);
-#endif
-	buffer_length = node->create(buffer_length, channel_count);
+	Error err = create(buffer_length, channel_count);
+	if (err != OK) {
+		return err;
+	}
 	if (output_rb) {
 		memdelete_arr(output_rb);
 	}
@@ -134,19 +132,17 @@ Error AudioDriverJavaScript::init() {
 }
 
 void AudioDriverJavaScript::start() {
-	if (node) {
-		node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb));
-	}
+	start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb));
 }
 
 void AudioDriverJavaScript::resume() {
-	if (state == 0) { // 'suspended'
+	if (audio_context.state == 0) { // 'suspended'
 		godot_audio_resume();
 	}
 }
 
 float AudioDriverJavaScript::get_latency() {
-	return output_latency + (float(buffer_length) / mix_rate);
+	return audio_context.output_latency + (float(buffer_length) / mix_rate);
 }
 
 int AudioDriverJavaScript::get_mix_rate() const {
@@ -157,24 +153,8 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
 	return get_speaker_mode_by_total_channels(channel_count);
 }
 
-void AudioDriverJavaScript::lock() {
-	if (node) {
-		node->unlock();
-	}
-}
-
-void AudioDriverJavaScript::unlock() {
-	if (node) {
-		node->unlock();
-	}
-}
-
 void AudioDriverJavaScript::finish() {
-	if (node) {
-		node->finish();
-		memdelete(node);
-		node = nullptr;
-	}
+	finish_driver();
 	if (output_rb) {
 		memdelete_arr(output_rb);
 		output_rb = nullptr;
@@ -203,41 +183,66 @@ Error AudioDriverJavaScript::capture_stop() {
 	return OK;
 }
 
-AudioDriverJavaScript::AudioDriverJavaScript() {
-	singleton = this;
-}
-
 #ifdef NO_THREADS
 /// ScriptProcessorNode implementation
-void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() {
-	AudioDriverJavaScript::singleton->_audio_driver_capture();
-	AudioDriverJavaScript::singleton->_audio_driver_process();
+AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr;
+
+void AudioDriverScriptProcessor::_process_callback() {
+	AudioDriverScriptProcessor::singleton->_audio_driver_capture();
+	AudioDriverScriptProcessor::singleton->_audio_driver_process();
 }
 
-int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) {
-	return godot_audio_script_create(p_buffer_samples, p_channels);
+Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels) {
+	if (!godot_audio_has_script_processor()) {
+		return ERR_UNAVAILABLE;
+	}
+	return (Error)godot_audio_script_create(&p_buffer_samples, p_channels);
 }
 
-void AudioDriverJavaScript::ScriptProcessorNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+void AudioDriverScriptProcessor::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
 	godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback);
 }
+
+/// AudioWorkletNode implementation (no threads)
+AudioDriverWorklet *AudioDriverWorklet::singleton = nullptr;
+
+Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) {
+	if (!godot_audio_has_worklet()) {
+		return ERR_UNAVAILABLE;
+	}
+	return (Error)godot_audio_worklet_create(p_channels);
+}
+
+void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+	_audio_driver_process();
+	godot_audio_worklet_start_no_threads(p_out_buf, p_out_buf_size, &_process_callback, p_in_buf, p_in_buf_size, &_capture_callback);
+}
+
+void AudioDriverWorklet::_process_callback(int p_pos, int p_samples) {
+	AudioDriverWorklet *driver = AudioDriverWorklet::singleton;
+	driver->_audio_driver_process(p_pos, p_samples);
+}
+
+void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) {
+	AudioDriverWorklet *driver = AudioDriverWorklet::singleton;
+	driver->_audio_driver_capture(p_pos, p_samples);
+}
 #else
-/// AudioWorkletNode implementation
-void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) {
-	AudioDriverJavaScript::WorkletNode *obj = static_cast<AudioDriverJavaScript::WorkletNode *>(p_data);
-	AudioDriverJavaScript *driver = AudioDriverJavaScript::singleton;
-	const int out_samples = memarr_len(driver->output_rb);
-	const int in_samples = memarr_len(driver->input_rb);
+/// AudioWorkletNode implementation (threads)
+void AudioDriverWorklet::_audio_thread_func(void *p_data) {
+	AudioDriverWorklet *driver = static_cast<AudioDriverWorklet *>(p_data);
+	const int out_samples = memarr_len(driver->get_output_rb());
+	const int in_samples = memarr_len(driver->get_input_rb());
 	int wpos = 0;
 	int to_write = out_samples;
 	int rpos = 0;
 	int to_read = 0;
 	int32_t step = 0;
-	while (!obj->quit) {
+	while (!driver->quit) {
 		if (to_read) {
 			driver->lock();
 			driver->_audio_driver_capture(rpos, to_read);
-			godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_IN, -to_read);
+			godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_IN, -to_read);
 			driver->unlock();
 			rpos += to_read;
 			if (rpos >= in_samples) {
@@ -247,38 +252,40 @@ void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) {
 		if (to_write) {
 			driver->lock();
 			driver->_audio_driver_process(wpos, to_write);
-			godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_OUT, to_write);
+			godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_OUT, to_write);
 			driver->unlock();
 			wpos += to_write;
 			if (wpos >= out_samples) {
 				wpos -= out_samples;
 			}
 		}
-		step = godot_audio_worklet_state_wait(obj->state, STATE_PROCESS, step, 1);
-		to_write = out_samples - godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_OUT);
-		to_read = godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_IN);
+		step = godot_audio_worklet_state_wait(driver->state, STATE_PROCESS, step, 1);
+		to_write = out_samples - godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_OUT);
+		to_read = godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_IN);
 	}
 }
 
-int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) {
-	godot_audio_worklet_create(p_channels);
-	return p_buffer_size;
+Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) {
+	if (!godot_audio_has_worklet()) {
+		return ERR_UNAVAILABLE;
+	}
+	return (Error)godot_audio_worklet_create(p_channels);
 }
 
-void AudioDriverJavaScript::WorkletNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
+void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
 	godot_audio_worklet_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, state);
 	thread.start(_audio_thread_func, this);
 }
 
-void AudioDriverJavaScript::WorkletNode::lock() {
+void AudioDriverWorklet::lock() {
 	mutex.lock();
 }
 
-void AudioDriverJavaScript::WorkletNode::unlock() {
+void AudioDriverWorklet::unlock() {
 	mutex.unlock();
 }
 
-void AudioDriverJavaScript::WorkletNode::finish() {
+void AudioDriverWorklet::finish_driver() {
 	quit = true; // Ask thread to quit.
 	thread.wait_to_finish();
 }

+ 96 - 61
platform/javascript/audio_driver_javascript.h

@@ -38,52 +38,15 @@
 #include "godot_audio.h"
 
 class AudioDriverJavaScript : public AudioDriver {
-public:
-	class AudioNode {
-	public:
-		virtual int create(int p_buffer_size, int p_output_channels) = 0;
-		virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0;
-		virtual void finish() {}
-		virtual void lock() {}
-		virtual void unlock() {}
-		virtual ~AudioNode() {}
-	};
-
-	class WorkletNode : public AudioNode {
-	private:
-		enum {
-			STATE_LOCK,
-			STATE_PROCESS,
-			STATE_SAMPLES_IN,
-			STATE_SAMPLES_OUT,
-			STATE_MAX,
-		};
-		Mutex mutex;
-		Thread thread;
-		bool quit = false;
-		int32_t state[STATE_MAX] = { 0 };
-
-		static void _audio_thread_func(void *p_data);
-
-	public:
-		int create(int p_buffer_size, int p_output_channels) override;
-		void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
-		void finish() override;
-		void lock() override;
-		void unlock() override;
-	};
-
-	class ScriptProcessorNode : public AudioNode {
-	private:
-		static void _process_callback();
-
-	public:
-		int create(int p_buffer_samples, int p_channels) override;
-		void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
-	};
-
 private:
-	AudioNode *node = nullptr;
+	struct AudioContext {
+		bool inited = false;
+		float output_latency = 0.0;
+		int state = -1;
+		int channel_count = 0;
+		int mix_rate = 0;
+	};
+	static AudioContext audio_context;
 
 	float *output_rb = nullptr;
 	float *input_rb = nullptr;
@@ -91,36 +54,108 @@ private:
 	int buffer_length = 0;
 	int mix_rate = 0;
 	int channel_count = 0;
-	int state = 0;
-	float output_latency = 0.0;
 
 	static void _state_change_callback(int p_state);
 	static void _latency_update_callback(float p_latency);
 
+	static AudioDriverJavaScript *singleton;
+
 protected:
 	void _audio_driver_process(int p_from = 0, int p_samples = 0);
 	void _audio_driver_capture(int p_from = 0, int p_samples = 0);
+	float *get_output_rb() const { return output_rb; }
+	float *get_input_rb() const { return input_rb; }
+
+	virtual Error create(int &p_buffer_samples, int p_channels) = 0;
+	virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0;
+	virtual void finish_driver() {}
 
 public:
 	static bool is_available();
 
-	static AudioDriverJavaScript *singleton;
+	virtual Error init() final;
+	virtual void start() final;
+	virtual void finish() final;
+
+	virtual float get_latency() override;
+	virtual int get_mix_rate() const override;
+	virtual SpeakerMode get_speaker_mode() const override;
+
+	virtual Error capture_start() override;
+	virtual Error capture_stop() override;
+
+	static void resume();
+
+	AudioDriverJavaScript() {}
+};
+
+#ifdef NO_THREADS
+class AudioDriverScriptProcessor : public AudioDriverJavaScript {
+private:
+	static void _process_callback();
+
+	static AudioDriverScriptProcessor *singleton;
+
+protected:
+	Error create(int &p_buffer_samples, int p_channels) override;
+	void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
+
+public:
+	virtual const char *get_name() const override { return "ScriptProcessor"; }
+
+	virtual void lock() override {}
+	virtual void unlock() override {}
+
+	AudioDriverScriptProcessor() { singleton = this; }
+};
+
+class AudioDriverWorklet : public AudioDriverJavaScript {
+private:
+	static void _process_callback(int p_pos, int p_samples);
+	static void _capture_callback(int p_pos, int p_samples);
+
+	static AudioDriverWorklet *singleton;
+
+protected:
+	virtual Error create(int &p_buffer_size, int p_output_channels) override;
+	virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
 
-	virtual const char *get_name() const;
+public:
+	virtual const char *get_name() const override { return "AudioWorklet"; }
+
+	virtual void lock() override {}
+	virtual void unlock() override {}
 
-	virtual Error init();
-	virtual void start();
-	void resume();
-	virtual float get_latency();
-	virtual int get_mix_rate() const;
-	virtual SpeakerMode get_speaker_mode() const;
-	virtual void lock();
-	virtual void unlock();
-	virtual void finish();
+	AudioDriverWorklet() { singleton = this; }
+};
+#else
+class AudioDriverWorklet : public AudioDriverJavaScript {
+private:
+	enum {
+		STATE_LOCK,
+		STATE_PROCESS,
+		STATE_SAMPLES_IN,
+		STATE_SAMPLES_OUT,
+		STATE_MAX,
+	};
+	Mutex mutex;
+	Thread thread;
+	bool quit = false;
+	int32_t state[STATE_MAX] = { 0 };
 
-	virtual Error capture_start();
-	virtual Error capture_stop();
+	static void _audio_thread_func(void *p_data);
 
-	AudioDriverJavaScript();
+protected:
+	virtual Error create(int &p_buffer_size, int p_output_channels) override;
+	virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
+	virtual void finish_driver() override;
+
+public:
+	virtual const char *get_name() const override { return "AudioWorklet"; }
+
+	void lock() override;
+	void unlock() override;
 };
 #endif
+
+#endif

+ 5 - 2
platform/javascript/godot_audio.h

@@ -38,6 +38,8 @@ extern "C" {
 #include "stddef.h"
 
 extern int godot_audio_is_available();
+extern int godot_audio_has_worklet();
+extern int godot_audio_has_script_processor();
 extern int godot_audio_init(int *p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float));
 extern void godot_audio_resume();
 
@@ -46,14 +48,15 @@ extern void godot_audio_capture_stop();
 
 // Worklet
 typedef int32_t GodotAudioState[4];
-extern void godot_audio_worklet_create(int p_channels);
+extern int godot_audio_worklet_create(int p_channels);
 extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state);
+extern void godot_audio_worklet_start_no_threads(float *p_out_buf, int p_out_size, void (*p_out_cb)(int p_pos, int p_frames), float *p_in_buf, int p_in_size, void (*p_in_cb)(int p_pos, int p_frames));
 extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
 extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx);
 extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout);
 
 // Script
-extern int godot_audio_script_create(int p_buffer_size, int p_channels);
+extern int godot_audio_script_create(int *p_buffer_size, int p_channels);
 extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)());
 
 #ifdef __cplusplus

+ 38 - 13
platform/javascript/js/libs/audio.worklet.js

@@ -29,15 +29,16 @@
 /*************************************************************************/
 
 class RingBuffer {
-	constructor(p_buffer, p_state) {
+	constructor(p_buffer, p_state, p_threads) {
 		this.buffer = p_buffer;
 		this.avail = p_state;
+		this.threads = p_threads;
 		this.rpos = 0;
 		this.wpos = 0;
 	}
 
 	data_left() {
-		return Atomics.load(this.avail, 0);
+		return this.threads ? Atomics.load(this.avail, 0) : this.avail;
 	}
 
 	space_left() {
@@ -55,10 +56,16 @@ class RingBuffer {
 			to_write -= high;
 			this.rpos = 0;
 		}
-		output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		if (to_write) {
+			output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
+		}
 		this.rpos += to_write;
-		Atomics.add(this.avail, 0, -output.length);
-		Atomics.notify(this.avail, 0);
+		if (this.threads) {
+			Atomics.add(this.avail, 0, -output.length);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail -= output.length;
+		}
 	}
 
 	write(p_buffer) {
@@ -77,14 +84,19 @@ class RingBuffer {
 			this.buffer.set(low);
 			this.wpos = low.length;
 		}
-		Atomics.add(this.avail, 0, to_write);
-		Atomics.notify(this.avail, 0);
+		if (this.threads) {
+			Atomics.add(this.avail, 0, to_write);
+			Atomics.notify(this.avail, 0);
+		} else {
+			this.avail += to_write;
+		}
 	}
 }
 
 class GodotProcessor extends AudioWorkletProcessor {
 	constructor() {
 		super();
+		this.threads = false;
 		this.running = true;
 		this.lock = null;
 		this.notifier = null;
@@ -100,24 +112,31 @@ class GodotProcessor extends AudioWorkletProcessor {
 	}
 
 	process_notify() {
-		Atomics.add(this.notifier, 0, 1);
-		Atomics.notify(this.notifier, 0);
+		if (this.notifier) {
+			Atomics.add(this.notifier, 0, 1);
+			Atomics.notify(this.notifier, 0);
+		}
 	}
 
 	parse_message(p_cmd, p_data) {
 		if (p_cmd === 'start' && p_data) {
 			const state = p_data[0];
 			let idx = 0;
+			this.threads = true;
 			this.lock = state.subarray(idx, ++idx);
 			this.notifier = state.subarray(idx, ++idx);
 			const avail_in = state.subarray(idx, ++idx);
 			const avail_out = state.subarray(idx, ++idx);
-			this.input = new RingBuffer(p_data[1], avail_in);
-			this.output = new RingBuffer(p_data[2], avail_out);
+			this.input = new RingBuffer(p_data[1], avail_in, true);
+			this.output = new RingBuffer(p_data[2], avail_out, true);
 		} else if (p_cmd === 'stop') {
-			this.runing = false;
+			this.running = false;
 			this.output = null;
 			this.input = null;
+		} else if (p_cmd === 'start_nothreads') {
+			this.output = new RingBuffer(p_data[0], p_data[0].length, false);
+		} else if (p_cmd === 'chunk') {
+			this.output.write(p_data);
 		}
 	}
 
@@ -139,7 +158,10 @@ class GodotProcessor extends AudioWorkletProcessor {
 			if (this.input_buffer.length !== chunk) {
 				this.input_buffer = new Float32Array(chunk);
 			}
-			if (this.input.space_left() >= chunk) {
+			if (!this.threads) {
+				GodotProcessor.write_input(this.input_buffer, input);
+				this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
+			} else if (this.input.space_left() >= chunk) {
 				GodotProcessor.write_input(this.input_buffer, input);
 				this.input.write(this.input_buffer);
 			} else {
@@ -156,6 +178,9 @@ class GodotProcessor extends AudioWorkletProcessor {
 			if (this.output.data_left() >= chunk) {
 				this.output.read(this.output_buffer);
 				GodotProcessor.write_output(output, this.output_buffer);
+				if (!this.threads) {
+					this.port.postMessage({ 'cmd': 'read', 'data': chunk });
+				}
 			} else {
 				this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
 			}

+ 115 - 3
platform/javascript/js/libs/library_godot_audio.js

@@ -159,6 +159,16 @@ const GodotAudio = {
 		return 1;
 	},
 
+	godot_audio_has_worklet__sig: 'i',
+	godot_audio_has_worklet: function () {
+		return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0;
+	},
+
+	godot_audio_has_script_processor__sig: 'i',
+	godot_audio_has_script_processor: function () {
+		return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0;
+	},
+
 	godot_audio_init__sig: 'iiiii',
 	godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) {
 		const statechange = GodotRuntime.get_func(p_state_change);
@@ -209,6 +219,7 @@ const GodotAudioWorklet = {
 	$GodotAudioWorklet: {
 		promise: null,
 		worklet: null,
+		ring_buffer: null,
 
 		create: function (channels) {
 			const path = GodotConfig.locate_file('godot.audio.worklet.js');
@@ -239,6 +250,86 @@ const GodotAudioWorklet = {
 			});
 		},
 
+		start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) {
+			function RingBuffer() {
+				let wpos = 0;
+				let rpos = 0;
+				let pending_samples = 0;
+				const wbuf = new Float32Array(p_out_size);
+
+				function send(port) {
+					if (pending_samples === 0) {
+						return;
+					}
+					const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
+					const size = buffer.length;
+					const tot_sent = pending_samples;
+					out_callback(wpos, pending_samples);
+					if (wpos + pending_samples >= size) {
+						const high = size - wpos;
+						wbuf.set(buffer.subarray(wpos, size));
+						pending_samples -= high;
+						wpos = 0;
+					}
+					if (pending_samples > 0) {
+						wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples);
+					}
+					port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) });
+					wpos += pending_samples;
+					pending_samples = 0;
+				}
+				this.receive = function (recv_buf) {
+					const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
+					const from = rpos;
+					let to_write = recv_buf.length;
+					let high = 0;
+					if (rpos + to_write >= p_in_size) {
+						high = p_in_size - rpos;
+						buffer.set(recv_buf.subarray(0, high), rpos);
+						to_write -= high;
+						rpos = 0;
+					}
+					if (to_write) {
+						buffer.set(recv_buf.subarray(high, to_write), rpos);
+					}
+					in_callback(from, recv_buf.length);
+					rpos += to_write;
+				};
+				this.consumed = function (size, port) {
+					pending_samples += size;
+					send(port);
+				};
+			}
+			GodotAudioWorklet.ring_buffer = new RingBuffer();
+			GodotAudioWorklet.promise.then(function () {
+				const node = GodotAudioWorklet.worklet;
+				const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size);
+				node.connect(GodotAudio.ctx.destination);
+				node.port.postMessage({
+					'cmd': 'start_nothreads',
+					'data': [buffer, p_in_size],
+				});
+				node.port.onmessage = function (event) {
+					if (!GodotAudioWorklet.worklet) {
+						return;
+					}
+					if (event.data['cmd'] === 'read') {
+						const read = event.data['data'];
+						GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port);
+					} else if (event.data['cmd'] === 'input') {
+						const buf = event.data['data'];
+						if (buf.length > p_in_size) {
+							GodotRuntime.error('Input chunk is too big');
+							return;
+						}
+						GodotAudioWorklet.ring_buffer.receive(buf);
+					} else {
+						GodotRuntime.error(event.data);
+					}
+				};
+			});
+		},
+
 		get_node: function () {
 			return GodotAudioWorklet.worklet;
 		},
@@ -262,9 +353,15 @@ const GodotAudioWorklet = {
 		},
 	},
 
-	godot_audio_worklet_create__sig: 'vi',
+	godot_audio_worklet_create__sig: 'ii',
 	godot_audio_worklet_create: function (channels) {
-		GodotAudioWorklet.create(channels);
+		try {
+			GodotAudioWorklet.create(channels);
+		} catch (e) {
+			GodotRuntime.error('Error starting AudioDriverWorklet', e);
+			return 1;
+		}
+		return 0;
 	},
 
 	godot_audio_worklet_start__sig: 'viiiii',
@@ -275,6 +372,13 @@ const GodotAudioWorklet = {
 		GodotAudioWorklet.start(in_buffer, out_buffer, state);
 	},
 
+	godot_audio_worklet_start_no_threads__sig: 'viiiiii',
+	godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) {
+		const out_callback = GodotRuntime.get_func(p_out_callback);
+		const in_callback = GodotRuntime.get_func(p_in_callback);
+		GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback);
+	},
+
 	godot_audio_worklet_state_wait__sig: 'iiii',
 	godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) {
 		Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
@@ -358,7 +462,15 @@ const GodotAudioScript = {
 
 	godot_audio_script_create__sig: 'iii',
 	godot_audio_script_create: function (buffer_length, channel_count) {
-		return GodotAudioScript.create(buffer_length, channel_count);
+		const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32');
+		try {
+			const out_len = GodotAudioScript.create(buf_len, channel_count);
+			GodotRuntime.setHeapValue(buffer_length, out_len, 'i32');
+		} catch (e) {
+			GodotRuntime.error('Error starting AudioDriverScriptProcessor', e);
+			return 1;
+		}
+		return 0;
 	},
 
 	godot_audio_script_start__sig: 'viiiii',

+ 16 - 10
platform/javascript/os_javascript.cpp

@@ -751,11 +751,14 @@ const char *OS_JavaScript::get_video_driver_name(int p_driver) const {
 // Audio
 
 int OS_JavaScript::get_audio_driver_count() const {
-	return 1;
+	return audio_drivers.size();
 }
 
 const char *OS_JavaScript::get_audio_driver_name(int p_driver) const {
-	return "JavaScript";
+	if (audio_drivers.size() <= p_driver) {
+		return "Unknown";
+	}
+	return audio_drivers[p_driver]->get_name();
 }
 
 // Clipboard
@@ -961,9 +964,7 @@ MainLoop *OS_JavaScript::get_main_loop() const {
 }
 
 void OS_JavaScript::resume_audio() {
-	if (audio_driver_javascript) {
-		audio_driver_javascript->resume();
-	}
+	AudioDriverJavaScript::resume();
 }
 
 void OS_JavaScript::fs_sync_callback() {
@@ -1021,9 +1022,10 @@ void OS_JavaScript::finalize() {
 	emscripten_webgl_commit_frame();
 	memdelete(visual_server);
 	emscripten_webgl_destroy_context(webgl_ctx);
-	if (audio_driver_javascript) {
-		memdelete(audio_driver_javascript);
+	for (int i = 0; i < audio_drivers.size(); i++) {
+		memdelete(audio_drivers[i]);
 	}
+	audio_drivers.clear();
 }
 
 // Miscellaneous
@@ -1217,7 +1219,6 @@ OS_JavaScript::OS_JavaScript() {
 
 	main_loop = NULL;
 	visual_server = NULL;
-	audio_driver_javascript = NULL;
 
 	swap_ok_cancel = false;
 	idb_available = godot_js_os_fs_is_persistent() != 0;
@@ -1225,8 +1226,13 @@ OS_JavaScript::OS_JavaScript() {
 	idb_is_syncing = false;
 
 	if (AudioDriverJavaScript::is_available()) {
-		audio_driver_javascript = memnew(AudioDriverJavaScript);
-		AudioDriverManager::add_driver(audio_driver_javascript);
+#ifdef NO_THREADS
+		audio_drivers.push_back(memnew(AudioDriverScriptProcessor));
+#endif
+		audio_drivers.push_back(memnew(AudioDriverWorklet));
+	}
+	for (int i = 0; i < audio_drivers.size(); i++) {
+		AudioDriverManager::add_driver(audio_drivers[i]);
 	}
 
 	Vector<Logger *> loggers;

+ 1 - 1
platform/javascript/os_javascript.h

@@ -62,7 +62,7 @@ private:
 
 	MainLoop *main_loop;
 	int video_driver_index;
-	AudioDriverJavaScript *audio_driver_javascript;
+	List<AudioDriverJavaScript *> audio_drivers;
 	VisualServer *visual_server;
 
 	bool swap_ok_cancel;