diff options
-rw-r--r-- | platform/javascript/SCsub | 3 | ||||
-rw-r--r-- | platform/javascript/audio_driver_javascript.cpp | 248 | ||||
-rw-r--r-- | platform/javascript/audio_driver_javascript.h | 96 | ||||
-rw-r--r-- | platform/javascript/engine/utils.js | 2 | ||||
-rw-r--r-- | platform/javascript/export/export.cpp | 7 | ||||
-rw-r--r-- | platform/javascript/godot_audio.h | 20 | ||||
-rw-r--r-- | platform/javascript/native/audio.worklet.js | 186 | ||||
-rw-r--r-- | platform/javascript/native/library_godot_audio.js | 357 |
8 files changed, 717 insertions, 202 deletions
diff --git a/platform/javascript/SCsub b/platform/javascript/SCsub index f26b314a51..a0e6fa0e18 100644 --- a/platform/javascript/SCsub +++ b/platform/javascript/SCsub @@ -56,9 +56,10 @@ out_files = [ zip_dir.File(binary_name + ".js"), zip_dir.File(binary_name + ".wasm"), zip_dir.File(binary_name + ".html"), + zip_dir.File(binary_name + ".audio.worklet.js"), ] html_file = "#misc/dist/html/editor.html" if env["tools"] else "#misc/dist/html/full-size.html" -in_files = [js_wrapped, build[1], html_file] +in_files = [js_wrapped, build[1], html_file, "#platform/javascript/native/audio.worklet.js"] if env["threads_enabled"]: in_files.append(build[2]) out_files.append(zip_dir.File(binary_name + ".worker.js")) diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp index 2535ad1812..dd982bc3a8 100644 --- a/platform/javascript/audio_driver_javascript.cpp +++ b/platform/javascript/audio_driver_javascript.cpp @@ -31,7 +31,6 @@ #include "audio_driver_javascript.h" #include "core/config/project_settings.h" -#include "godot_audio.h" #include <emscripten.h> @@ -45,92 +44,109 @@ const char *AudioDriverJavaScript::get_name() const { return "JavaScript"; } -#ifndef NO_THREADS -void AudioDriverJavaScript::_audio_thread_func(void *p_data) { - AudioDriverJavaScript *obj = static_cast<AudioDriverJavaScript *>(p_data); - while (!obj->quit) { - obj->lock(); - if (!obj->needs_process) { - obj->unlock(); - OS::get_singleton()->delay_usec(1000); // Give the browser some slack. - continue; - } - obj->_audio_driver_process(); - obj->needs_process = false; - obj->unlock(); - } +void AudioDriverJavaScript::_state_change_callback(int p_state) { + singleton->state = p_state; } -#endif -void AudioDriverJavaScript::_audio_driver_process_start() { -#ifndef NO_THREADS - singleton->lock(); -#else - singleton->_audio_driver_process(); -#endif +void AudioDriverJavaScript::_latency_update_callback(float p_latency) { + singleton->output_latency = p_latency; } -void AudioDriverJavaScript::_audio_driver_process_end() { -#ifndef NO_THREADS - singleton->needs_process = true; - singleton->unlock(); -#endif -} +void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) { + int32_t *stream_buffer = reinterpret_cast<int32_t *>(output_rb); + const int max_samples = memarr_len(output_rb); -void AudioDriverJavaScript::_audio_driver_process_capture(float p_sample) { - singleton->process_capture(p_sample); + int write_pos = p_from; + int to_write = p_samples; + if (to_write == 0) { + to_write = max_samples; + } + // High part + if (write_pos + to_write > max_samples) { + const int samples_high = max_samples - write_pos; + audio_server_process(samples_high / channel_count, &stream_buffer[write_pos]); + for (int i = write_pos; i < max_samples; i++) { + output_rb[i] = float(stream_buffer[i] >> 16) / 32768.f; + } + to_write -= samples_high; + write_pos = 0; + } + // Leftover + audio_server_process(to_write / channel_count, &stream_buffer[write_pos]); + for (int i = write_pos; i < write_pos + to_write; i++) { + output_rb[i] = float(stream_buffer[i] >> 16) / 32768.f; + } } -void AudioDriverJavaScript::_audio_driver_process() { - int sample_count = memarr_len(internal_buffer) / channel_count; - int32_t *stream_buffer = reinterpret_cast<int32_t *>(internal_buffer); - audio_server_process(sample_count, stream_buffer); - for (int i = 0; i < sample_count * channel_count; i++) { - internal_buffer[i] = float(stream_buffer[i] >> 16) / 32768.f; +void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) { + if (get_input_buffer().size() == 0) { + return; // Input capture stopped. } -} + const int max_samples = memarr_len(input_rb); -void AudioDriverJavaScript::process_capture(float sample) { - int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16); - input_buffer_write(sample32); + int read_pos = p_from; + int to_read = p_samples; + if (to_read == 0) { + to_read = max_samples; + } + // High part + if (read_pos + to_read > max_samples) { + const int samples_high = max_samples - read_pos; + for (int i = read_pos; i < max_samples; i++) { + input_buffer_write(int32_t(input_rb[i] * 32768.f) * (1U << 16)); + } + to_read -= samples_high; + read_pos = 0; + } + // Leftover + for (int i = read_pos; i < read_pos + to_read; i++) { + input_buffer_write(int32_t(input_rb[i] * 32768.f) * (1U << 16)); + } } 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); - buffer_length = closest_power_of_2(latency * mix_rate / 1000); - buffer_length = godot_audio_create_processor(buffer_length, channel_count); - if (!buffer_length) { - return FAILED; + channel_count = godot_audio_init(mix_rate, latency, &_state_change_callback, &_latency_update_callback); + 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); + if (output_rb) { + memdelete_arr(output_rb); } - - if (!internal_buffer || (int)memarr_len(internal_buffer) != buffer_length * channel_count) { - if (internal_buffer) - memdelete_arr(internal_buffer); - internal_buffer = memnew_arr(float, buffer_length *channel_count); + output_rb = memnew_arr(float, buffer_length *channel_count); + if (!output_rb) { + return ERR_OUT_OF_MEMORY; } - - if (!internal_buffer) { + if (input_rb) { + memdelete_arr(input_rb); + } + input_rb = memnew_arr(float, buffer_length *channel_count); + if (!input_rb) { return ERR_OUT_OF_MEMORY; } return OK; } void AudioDriverJavaScript::start() { -#ifndef NO_THREADS - thread = Thread::create(_audio_thread_func, this); -#endif - godot_audio_start(internal_buffer, &_audio_driver_process_start, &_audio_driver_process_end, &_audio_driver_process_capture); + if (node) { + node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb)); + } } void AudioDriverJavaScript::resume() { - godot_audio_resume(); + if (state == 0) { // 'suspended' + godot_audio_resume(); + } } float AudioDriverJavaScript::get_latency() { - return godot_audio_get_latency(); + return output_latency + (float(buffer_length) / mix_rate); } int AudioDriverJavaScript::get_mix_rate() const { @@ -142,42 +158,128 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const { } void AudioDriverJavaScript::lock() { -#ifndef NO_THREADS - mutex.lock(); -#endif + if (node) { + node->unlock(); + } } void AudioDriverJavaScript::unlock() { -#ifndef NO_THREADS - mutex.unlock(); -#endif + if (node) { + node->unlock(); + } } void AudioDriverJavaScript::finish() { -#ifndef NO_THREADS - quit = true; // Ask thread to quit. - Thread::wait_to_finish(thread); - memdelete(thread); - thread = nullptr; -#endif - if (internal_buffer) { - memdelete_arr(internal_buffer); - internal_buffer = nullptr; + if (node) { + node->finish(); + memdelete(node); + node = nullptr; + } + if (output_rb) { + memdelete_arr(output_rb); + output_rb = nullptr; + } + if (input_rb) { + memdelete_arr(input_rb); + input_rb = nullptr; } } Error AudioDriverJavaScript::capture_start() { + lock(); input_buffer_init(buffer_length); + unlock(); godot_audio_capture_start(); return OK; } Error AudioDriverJavaScript::capture_stop() { godot_audio_capture_stop(); + lock(); input_buffer.clear(); + unlock(); 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(); +} + +int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) { + return 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) { + godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback); +} +#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); + int wpos = 0; + int to_write = out_samples; + int rpos = 0; + int to_read = 0; + int32_t step = 0; + while (!obj->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); + driver->unlock(); + rpos += to_read; + if (rpos >= in_samples) { + rpos -= in_samples; + } + } + if (to_write) { + driver->lock(); + driver->_audio_driver_process(wpos, to_write); + godot_audio_worklet_state_add(obj->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); + } +} + +int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) { + godot_audio_worklet_create(p_channels); + return p_buffer_size; +} + +void AudioDriverJavaScript::WorkletNode::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 = Thread::create(_audio_thread_func, this); +} + +void AudioDriverJavaScript::WorkletNode::lock() { + mutex.lock(); +} + +void AudioDriverJavaScript::WorkletNode::unlock() { + mutex.unlock(); +} + +void AudioDriverJavaScript::WorkletNode::finish() { + quit = true; // Ask thread to quit. + Thread::wait_to_finish(thread); + memdelete(thread); + thread = nullptr; +} +#endif diff --git a/platform/javascript/audio_driver_javascript.h b/platform/javascript/audio_driver_javascript.h index 60ad6480c9..f112a1ede4 100644 --- a/platform/javascript/audio_driver_javascript.h +++ b/platform/javascript/audio_driver_javascript.h @@ -31,54 +31,96 @@ #ifndef AUDIO_DRIVER_JAVASCRIPT_H #define AUDIO_DRIVER_JAVASCRIPT_H -#include "servers/audio_server.h" - #include "core/os/mutex.h" #include "core/os/thread.h" +#include "servers/audio_server.h" + +#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 = nullptr; + 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: - float *internal_buffer = nullptr; + AudioNode *node = nullptr; + + float *output_rb = nullptr; + float *input_rb = nullptr; int buffer_length = 0; int mix_rate = 0; int channel_count = 0; + int state = 0; + float output_latency = 0.0; -#ifndef NO_THREADS - Mutex mutex; - Thread *thread = nullptr; - bool quit = false; - bool needs_process = true; + static void _state_change_callback(int p_state); + static void _latency_update_callback(float p_latency); - static void _audio_thread_func(void *p_data); -#endif - static void _audio_driver_process_start(); - static void _audio_driver_process_end(); - static void _audio_driver_process_capture(float p_sample); - void _audio_driver_process(); +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); public: static bool is_available(); - void process_capture(float sample); static AudioDriverJavaScript *singleton; - const char *get_name() const override; + virtual const char *get_name() const; - Error init() override; - void start() override; + virtual Error init(); + virtual void start(); void resume(); - float get_latency() override; - int get_mix_rate() const override; - SpeakerMode get_speaker_mode() const override; - void lock() override; - void unlock() override; - void finish() override; + 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(); - Error capture_start() override; - Error capture_stop() override; + virtual Error capture_start(); + virtual Error capture_stop(); AudioDriverJavaScript(); }; - #endif diff --git a/platform/javascript/engine/utils.js b/platform/javascript/engine/utils.js index 0c97b38199..10e3abe91e 100644 --- a/platform/javascript/engine/utils.js +++ b/platform/javascript/engine/utils.js @@ -4,6 +4,8 @@ var Utils = { function rw(path) { if (path.endsWith('.worker.js')) { return execName + '.worker.js'; + } else if (path.endsWith('.audio.worklet.js')) { + return execName + '.audio.worklet.js'; } else if (path.endsWith('.js')) { return execName + '.js'; } else if (path.endsWith('.wasm')) { diff --git a/platform/javascript/export/export.cpp b/platform/javascript/export/export.cpp index a83ff44d20..d520931067 100644 --- a/platform/javascript/export/export.cpp +++ b/platform/javascript/export/export.cpp @@ -94,6 +94,9 @@ public: } else if (req[1] == basereq + ".js") { filepath += ".js"; ctype = "application/javascript"; + } else if (req[1] == basereq + ".audio.worklet.js") { + filepath += ".audio.worklet.js"; + ctype = "application/javascript"; } else if (req[1] == basereq + ".worker.js") { filepath += ".worker.js"; ctype = "application/javascript"; @@ -440,6 +443,9 @@ Error EditorExportPlatformJavaScript::export_project(const Ref<EditorExportPrese } else if (file == "godot.worker.js") { file = p_path.get_file().get_basename() + ".worker.js"; + } else if (file == "godot.audio.worklet.js") { + file = p_path.get_file().get_basename() + ".audio.worklet.js"; + } else if (file == "godot.wasm") { file = p_path.get_file().get_basename() + ".wasm"; } @@ -566,6 +572,7 @@ Error EditorExportPlatformJavaScript::run(const Ref<EditorExportPreset> &p_prese DirAccess::remove_file_or_error(basepath + ".html"); 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 + ".pck"); DirAccess::remove_file_or_error(basepath + ".png"); DirAccess::remove_file_or_error(basepath + ".wasm"); diff --git a/platform/javascript/godot_audio.h b/platform/javascript/godot_audio.h index 120fdbcc59..7ebda3ad39 100644 --- a/platform/javascript/godot_audio.h +++ b/platform/javascript/godot_audio.h @@ -38,18 +38,24 @@ extern "C" { #include "stddef.h" extern int godot_audio_is_available(); - -extern int godot_audio_init(int p_mix_rate, int p_latency); -extern int godot_audio_create_processor(int p_buffer_length, int p_channel_count); - -extern void godot_audio_start(float *r_buffer_ptr, void (*p_start)(), void (*p_end)(), void (*p_input)(float p_sample)); +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(); -extern float godot_audio_get_latency(); - extern void godot_audio_capture_start(); extern void godot_audio_capture_stop(); +// Worklet +typedef int32_t GodotAudioState[4]; +extern void 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_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 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 } #endif diff --git a/platform/javascript/native/audio.worklet.js b/platform/javascript/native/audio.worklet.js new file mode 100644 index 0000000000..ad7957e45c --- /dev/null +++ b/platform/javascript/native/audio.worklet.js @@ -0,0 +1,186 @@ +/*************************************************************************/ +/* audio.worklet.js */ +/*************************************************************************/ +/* This file is part of: */ +/* GODOT ENGINE */ +/* https://godotengine.org */ +/*************************************************************************/ +/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */ +/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */ +/* */ +/* 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 RingBuffer { + + constructor(p_buffer, p_state) { + this.buffer = p_buffer; + this.avail = p_state; + this.rpos = 0; + this.wpos = 0; + } + + data_left() { + return Atomics.load(this.avail, 0); + } + + space_left() { + return this.buffer.length - this.data_left(); + } + + read(output) { + const size = this.buffer.length; + let from = 0 + let to_write = output.length; + if (this.rpos + to_write > size) { + const high = size - this.rpos; + output.set(this.buffer.subarray(this.rpos, size)); + from = high; + to_write -= high; + this.rpos = 0; + } + 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); + } + + write(p_buffer) { + const to_write = p_buffer.length; + const mw = this.buffer.length - this.wpos; + if (mw >= to_write) { + this.buffer.set(p_buffer, this.wpos); + } else { + const high = p_buffer.subarray(0, to_write - mw); + const low = p_buffer.subarray(to_write - mw); + this.buffer.set(high, this.wpos); + this.buffer.set(low); + } + let diff = to_write; + if (this.wpos + diff >= this.buffer.length) { + diff -= this.buffer.length; + } + this.wpos += diff; + Atomics.add(this.avail, 0, to_write); + Atomics.notify(this.avail, 0); + } +} + +class GodotProcessor extends AudioWorkletProcessor { + constructor() { + super(); + this.running = true; + this.lock = null; + this.notifier = null; + this.output = null; + this.output_buffer = new Float32Array(); + this.input = null; + this.input_buffer = new Float32Array(); + this.port.onmessage = (event) => { + const cmd = event.data['cmd']; + const data = event.data['data']; + this.parse_message(cmd, data); + }; + } + + process_notify() { + 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.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); + } else if (p_cmd == "stop") { + this.runing = false; + this.output = null; + this.input = null; + } + } + + array_has_data(arr) { + return arr.length && arr[0].length && arr[0][0].length; + } + + process(inputs, outputs, parameters) { + if (!this.running) { + return false; // Stop processing. + } + if (this.output === null) { + return true; // Not ready yet, keep processing. + } + const process_input = this.array_has_data(inputs); + if (process_input) { + const input = inputs[0]; + const chunk = input[0].length * input.length; + if (this.input_buffer.length != chunk) { + this.input_buffer = new Float32Array(chunk); + } + if (this.input.space_left() >= chunk) { + this.write_input(this.input_buffer, input); + this.input.write(this.input_buffer); + } else { + this.port.postMessage("Input buffer is full! Skipping input frame."); + } + } + const process_output = this.array_has_data(outputs); + if (process_output) { + const output = outputs[0]; + const chunk = output[0].length * output.length; + if (this.output_buffer.length != chunk) { + this.output_buffer = new Float32Array(chunk) + } + if (this.output.data_left() >= chunk) { + this.output.read(this.output_buffer); + this.write_output(output, this.output_buffer); + } else { + this.port.postMessage("Output buffer has not enough frames! Skipping output frame."); + } + } + this.process_notify(); + return true; + } + + write_output(dest, source) { + const channels = dest.length; + for (let ch = 0; ch < channels; ch++) { + for (let sample = 0; sample < dest[ch].length; sample++) { + dest[ch][sample] = source[sample * channels + ch]; + } + } + } + + write_input(dest, source) { + const channels = source.length; + for (let ch = 0; ch < channels; ch++) { + for (let sample = 0; sample < source[ch].length; sample++) { + dest[sample * channels + ch] = source[ch][sample]; + } + } + } +} + +registerProcessor('godot-processor', GodotProcessor); diff --git a/platform/javascript/native/library_godot_audio.js b/platform/javascript/native/library_godot_audio.js index 1e6f787657..846359b8b2 100644 --- a/platform/javascript/native/library_godot_audio.js +++ b/platform/javascript/native/library_godot_audio.js @@ -27,89 +27,123 @@ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /*************************************************************************/ -var GodotAudio = { + +const GodotAudio = { $GodotAudio__deps: ['$GodotOS'], $GodotAudio: { - ctx: null, input: null, - script: null, - }, + driver: null, + interval: 0, - godot_audio_is_available__proxy: 'sync', - godot_audio_is_available: function () { - if (!(window.AudioContext || window.webkitAudioContext)) { - return 0; - } - return 1; - }, + init: function(mix_rate, latency, onstatechange, onlatencyupdate) { + const ctx = new (window.AudioContext || window.webkitAudioContext)({ + sampleRate: mix_rate, + // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. + }); + GodotAudio.ctx = ctx; + onstatechange(ctx.state); // Immeditately notify state. + ctx.onstatechange = function() { + let state = 0; + switch (ctx.state) { + case 'suspended': + state = 0; + break; + case 'running': + state = 1; + break; + case 'closed': + state = 2; + break; + } + onstatechange(state); + } + // Update computed latency + GodotAudio.interval = setInterval(function() { + let latency = 0; + if (ctx.baseLatency) { + latency += GodotAudio.ctx.baseLatency; + } + if (ctx.outputLatency) { + latency += GodotAudio.ctx.outputLatency; + } + onlatencyupdate(latency); + }, 1000); + GodotOS.atexit(GodotAudio.close_async); + return ctx.destination.channelCount; + }, - godot_audio_init: function(mix_rate, latency) { - GodotAudio.ctx = new (window.AudioContext || window.webkitAudioContext)({ - sampleRate: mix_rate, - // latencyHint: latency / 1000 // Do not specify, leave 'interactive' for good performance. - }); - GodotOS.atexit(function(accept, reject) { - if (!GodotAudio.ctx) { - accept(); + create_input: function(callback) { + if (GodotAudio.input) { + return; // Already started. + } + function gotMediaInput(stream) { + GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); + callback(GodotAudio.input) + } + if (navigator.mediaDevices.getUserMedia) { + navigator.mediaDevices.getUserMedia({ + "audio": true + }).then(gotMediaInput, function(e) { out(e) }); + } else { + if (!navigator.getUserMedia) { + navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; + } + navigator.getUserMedia({ + "audio": true + }, gotMediaInput, function(e) { out(e) }); + } + }, + + close_async: function(resolve, reject) { + const ctx = GodotAudio.ctx; + GodotAudio.ctx = null; + // Audio was not initialized. + if (!ctx) { + resolve(); return; } - if (GodotAudio.script) { - GodotAudio.script.disconnect(); - GodotAudio.script.onaudioprocess = null; - GodotAudio.script = null; + // Remove latency callback + if (GodotAudio.interval) { + clearInterval(GodotAudio.interval); + GodotAudio.interval = 0; } + // Disconnect input, if it was started. if (GodotAudio.input) { GodotAudio.input.disconnect(); GodotAudio.input = null; } - GodotAudio.ctx.close().then(function() { - accept(); + // Disconnect output + let closed = Promise.resolve(); + if (GodotAudio.driver) { + closed = GodotAudio.driver.close(); + } + closed.then(function() { + return ctx.close(); + }).then(function() { + ctx.onstatechange = null; + resolve(); }).catch(function(e) { - accept(); + ctx.onstatechange = null; + console.error("Error closing AudioContext", e); + resolve(); }); - GodotAudio.ctx = null; - }); - return GodotAudio.ctx.destination.channelCount; + }, }, - godot_audio_create_processor: function(buffer_length, channel_count) { - GodotAudio.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); - GodotAudio.script.connect(GodotAudio.ctx.destination); - return GodotAudio.script.bufferSize; + godot_audio_is_available__proxy: 'sync', + godot_audio_is_available: function () { + if (!(window.AudioContext || window.webkitAudioContext)) { + return 0; + } + return 1; }, - godot_audio_start: function(buffer_ptr, p_process_start, p_process_end, p_process_capture) { - const audioDriverProcessStart = GodotOS.get_func(p_process_start); - const audioDriverProcessEnd = GodotOS.get_func(p_process_end); - const audioDriverProcessCapture = GodotOS.get_func(p_process_capture); - GodotAudio.script.onaudioprocess = function(audioProcessingEvent) { - audioDriverProcessStart(); - - var input = audioProcessingEvent.inputBuffer; - var output = audioProcessingEvent.outputBuffer; - var internalBuffer = HEAPF32.subarray( - buffer_ptr / HEAPF32.BYTES_PER_ELEMENT, - buffer_ptr / HEAPF32.BYTES_PER_ELEMENT + output.length * output.numberOfChannels); - for (var channel = 0; channel < output.numberOfChannels; channel++) { - var outputData = output.getChannelData(channel); - // Loop through samples. - for (var sample = 0; sample < outputData.length; sample++) { - outputData[sample] = internalBuffer[sample * output.numberOfChannels + channel]; - } - } - - if (GodotAudio.input) { - var inputDataL = input.getChannelData(0); - var inputDataR = input.getChannelData(1); - for (var i = 0; i < inputDataL.length; i++) { - audioDriverProcessCapture(inputDataL[i]); - audioDriverProcessCapture(inputDataR[i]); - } - } - audioDriverProcessEnd(); - }; + godot_audio_init: function(p_mix_rate, p_latency, p_state_change, p_latency_update) { + const statechange = GodotOS.get_func(p_state_change); + const latencyupdate = GodotOS.get_func(p_latency_update); + return GodotAudio.init(p_mix_rate, p_latency, statechange, latencyupdate); }, godot_audio_resume: function() { @@ -118,48 +152,21 @@ var GodotAudio = { } }, - godot_audio_get_latency__proxy: 'sync', - godot_audio_get_latency: function() { - var latency = 0; - if (GodotAudio.ctx) { - if (GodotAudio.ctx.baseLatency) { - latency += GodotAudio.ctx.baseLatency; - } - if (GodotAudio.ctx.outputLatency) { - latency += GodotAudio.ctx.outputLatency; - } - } - return latency; - }, - godot_audio_capture_start__proxy: 'sync', godot_audio_capture_start: function() { if (GodotAudio.input) { return; // Already started. } - function gotMediaInput(stream) { - GodotAudio.input = GodotAudio.ctx.createMediaStreamSource(stream); - GodotAudio.input.connect(GodotAudio.script); - } - - function gotMediaInputError(e) { - out(e); - } - - if (navigator.mediaDevices.getUserMedia) { - navigator.mediaDevices.getUserMedia({"audio": true}).then(gotMediaInput, gotMediaInputError); - } else { - if (!navigator.getUserMedia) - navigator.getUserMedia = navigator.webkitGetUserMedia || navigator.mozGetUserMedia; - navigator.getUserMedia({"audio": true}, gotMediaInput, gotMediaInputError); - } + GodotAudio.create_input(function(input) { + input.connect(GodotAudio.driver.get_node()); + }); }, godot_audio_capture_stop__proxy: 'sync', godot_audio_capture_stop: function() { if (GodotAudio.input) { const tracks = GodotAudio.input['mediaStream']['getTracks'](); - for (var i = 0; i < tracks.length; i++) { + for (let i = 0; i < tracks.length; i++) { tracks[i]['stop'](); } GodotAudio.input.disconnect(); @@ -170,3 +177,165 @@ var GodotAudio = { autoAddDeps(GodotAudio, "$GodotAudio"); mergeInto(LibraryManager.library, GodotAudio); + +/** + * The AudioWorklet API driver, used when threads are available. + */ +const GodotAudioWorklet = { + + $GodotAudioWorklet__deps: ['$GodotAudio'], + $GodotAudioWorklet: { + promise: null, + worklet: null, + + create: function(channels) { + const path = Module['locateFile']('godot.audio.worklet.js'); + GodotAudioWorklet.promise = GodotAudio.ctx.audioWorklet.addModule(path).then(function() { + GodotAudioWorklet.worklet = new AudioWorkletNode( + GodotAudio.ctx, + 'godot-processor', + { + 'outputChannelCount': [channels] + } + ); + return Promise.resolve(); + }); + GodotAudio.driver = GodotAudioWorklet; + }, + + start: function(in_buf, out_buf, state) { + GodotAudioWorklet.promise.then(function() { + const node = GodotAudioWorklet.worklet; + node.connect(GodotAudio.ctx.destination); + node.port.postMessage({ + 'cmd': 'start', + 'data': [state, in_buf, out_buf], + }); + node.port.onmessage = function(event) { + console.error(event.data); + }; + }); + }, + + get_node: function() { + return GodotAudioWorklet.worklet; + }, + + close: function() { + return new Promise(function(resolve, reject) { + GodotAudioWorklet.promise.then(function() { + GodotAudioWorklet.worklet.port.postMessage({ + 'cmd': 'stop', + 'data': null, + }); + GodotAudioWorklet.worklet.disconnect(); + GodotAudioWorklet.worklet = null; + GodotAudioWorklet.promise = null; + resolve(); + }); + }); + }, + }, + + godot_audio_worklet_create: function(channels) { + GodotAudioWorklet.create(channels); + }, + + godot_audio_worklet_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_state) { + const out_buffer = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); + const in_buffer = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); + const state = GodotOS.heapSub(HEAP32, p_state, 4); + GodotAudioWorklet.start(in_buffer, out_buffer, state); + }, + + 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); + return Atomics.load(HEAP32, (p_state >> 2) + p_idx); + }, + + godot_audio_worklet_state_add: function(p_state, p_idx, p_value) { + return Atomics.add(HEAP32, (p_state >> 2) + p_idx, p_value); + }, + + godot_audio_worklet_state_get: function(p_state, p_idx) { + return Atomics.load(HEAP32, (p_state >> 2) + p_idx); + }, +}; + +autoAddDeps(GodotAudioWorklet, "$GodotAudioWorklet"); +mergeInto(LibraryManager.library, GodotAudioWorklet); + +/* + * The deprecated ScriptProcessorNode API, used when threads are disabled. + */ +const GodotAudioScript = { + + $GodotAudioScript__deps: ['$GodotAudio'], + $GodotAudioScript: { + script: null, + + create: function(buffer_length, channel_count) { + GodotAudioScript.script = GodotAudio.ctx.createScriptProcessor(buffer_length, 2, channel_count); + GodotAudio.driver = GodotAudioScript; + return GodotAudioScript.script.bufferSize; + }, + + start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess) { + GodotAudioScript.script.onaudioprocess = function(event) { + // Read input + const inb = GodotOS.heapSub(HEAPF32, p_in_buf, p_in_size); + const input = event.inputBuffer; + if (GodotAudio.input) { + const inlen = input.getChannelData(0).length; + for (let ch = 0; ch < 2; ch++) { + const data = input.getChannelData(ch); + for (let s = 0; s < inlen; s++) { + inb[s * 2 + ch] = data[s]; + } + } + } + + // Let Godot process the input/output. + onprocess(); + + // Write the output. + const outb = GodotOS.heapSub(HEAPF32, p_out_buf, p_out_size); + const output = event.outputBuffer; + const channels = output.numberOfChannels; + for (let ch = 0; ch < channels; ch++) { + const data = output.getChannelData(ch); + // Loop through samples and assign computed values. + for (let sample = 0; sample < data.length; sample++) { + data[sample] = outb[sample * channels + ch]; + } + } + }; + GodotAudioScript.script.connect(GodotAudio.ctx.destination); + }, + + get_node: function() { + return GodotAudioScript.script; + }, + + close: function() { + return new Promise(function(resolve, reject) { + GodotAudioScript.script.disconnect(); + GodotAudioScript.script.onaudioprocess = null; + GodotAudioScript.script = null; + resolve(); + }); + }, + }, + + godot_audio_script_create: function(buffer_length, channel_count) { + return GodotAudioScript.create(buffer_length, channel_count); + }, + + godot_audio_script_start: function(p_in_buf, p_in_size, p_out_buf, p_out_size, p_cb) { + const onprocess = GodotOS.get_func(p_cb); + GodotAudioScript.start(p_in_buf, p_in_size, p_out_buf, p_out_size, onprocess); + }, +}; + +autoAddDeps(GodotAudioScript, "$GodotAudioScript"); +mergeInto(LibraryManager.library, GodotAudioScript); |