summaryrefslogtreecommitdiff
path: root/platform/javascript/audio_driver_javascript.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'platform/javascript/audio_driver_javascript.cpp')
-rw-r--r--platform/javascript/audio_driver_javascript.cpp368
1 files changed, 191 insertions, 177 deletions
diff --git a/platform/javascript/audio_driver_javascript.cpp b/platform/javascript/audio_driver_javascript.cpp
index 8f857478e5..478e848675 100644
--- a/platform/javascript/audio_driver_javascript.cpp
+++ b/platform/javascript/audio_driver_javascript.cpp
@@ -5,8 +5,8 @@
/* GODOT ENGINE */
/* https://godotengine.org */
/*************************************************************************/
-/* Copyright (c) 2007-2020 Juan Linietsky, Ariel Manzur. */
-/* Copyright (c) 2014-2020 Godot Engine contributors (cf. AUTHORS.md). */
+/* Copyright (c) 2007-2021 Juan Linietsky, Ariel Manzur. */
+/* Copyright (c) 2014-2021 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 */
@@ -30,242 +30,256 @@
#include "audio_driver_javascript.h"
+#include "core/config/project_settings.h"
+
#include <emscripten.h>
AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr;
-const char *AudioDriverJavaScript::get_name() const {
+bool AudioDriverJavaScript::is_available() {
+ return godot_audio_is_available() != 0;
+}
+const char *AudioDriverJavaScript::get_name() const {
return "JavaScript";
}
-extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_js_mix() {
-
- AudioDriverJavaScript::singleton->mix_to_js();
+void AudioDriverJavaScript::_state_change_callback(int p_state) {
+ singleton->state = p_state;
}
-extern "C" EMSCRIPTEN_KEEPALIVE void audio_driver_process_capture(float sample) {
-
- AudioDriverJavaScript::singleton->process_capture(sample);
+void AudioDriverJavaScript::_latency_update_callback(float p_latency) {
+ singleton->output_latency = p_latency;
}
-void AudioDriverJavaScript::mix_to_js() {
+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);
- int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode());
- 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;
+ 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::process_capture(float sample) {
+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);
- 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() {
-
- /* clang-format off */
- _driver_id = EM_ASM_INT({
- return Module.IDHandler.add({
- 'context': new (window.AudioContext || window.webkitAudioContext),
- 'input': null,
- 'stream': null,
- 'script': null
- });
- });
- /* clang-format on */
-
- int channel_count = get_total_channels_by_speaker_mode(get_speaker_mode());
- /* clang-format off */
- buffer_length = EM_ASM_INT({
- var ref = Module.IDHandler.get($0);
- var ctx = ref['context'];
- var CHANNEL_COUNT = $1;
-
- var channelCount = ctx.destination.channelCount;
- var script = null;
- try {
- // Try letting the browser recommend a buffer length.
- script = ctx.createScriptProcessor(0, 2, channelCount);
- } catch (e) {
- // ...otherwise, default to 4096.
- script = ctx.createScriptProcessor(4096, 2, channelCount);
- }
- script.connect(ctx.destination);
- ref['script'] = script;
-
- return script.bufferSize;
- }, _driver_id, channel_count);
- /* clang-format on */
- if (!buffer_length) {
- return FAILED;
+ mix_rate = GLOBAL_GET("audio/driver/mix_rate");
+ int latency = GLOBAL_GET("audio/driver/output_latency");
+
+ 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;
}
-
- return internal_buffer ? OK : ERR_OUT_OF_MEMORY;
+ 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() {
-
- /* clang-format off */
- EM_ASM({
- const ref = Module.IDHandler.get($0);
- var INTERNAL_BUFFER_PTR = $1;
-
- var audioDriverMixFunction = cwrap('audio_driver_js_mix');
- var audioDriverProcessCapture = cwrap('audio_driver_process_capture', null, ['number']);
- ref['script'].onaudioprocess = function(audioProcessingEvent) {
- audioDriverMixFunction();
-
- var input = audioProcessingEvent.inputBuffer;
- var output = audioProcessingEvent.outputBuffer;
- var internalBuffer = HEAPF32.subarray(
- INTERNAL_BUFFER_PTR / HEAPF32.BYTES_PER_ELEMENT,
- INTERNAL_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 (ref['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]);
- }
- }
- };
- }, _driver_id, internal_buffer);
- /* clang-format on */
+ if (node) {
+ node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb));
+ }
}
void AudioDriverJavaScript::resume() {
- /* clang-format off */
- EM_ASM({
- const ref = Module.IDHandler.get($0);
- if (ref && ref['context'] && ref['context'].resume)
- ref['context'].resume();
- }, _driver_id);
- /* clang-format on */
+ if (state == 0) { // 'suspended'
+ godot_audio_resume();
+ }
}
-int AudioDriverJavaScript::get_mix_rate() const {
+float AudioDriverJavaScript::get_latency() {
+ return output_latency + (float(buffer_length) / mix_rate);
+}
- /* clang-format off */
- return EM_ASM_INT({
- const ref = Module.IDHandler.get($0);
- return ref && ref['context'] ? ref['context'].sampleRate : 0;
- }, _driver_id);
- /* clang-format on */
+int AudioDriverJavaScript::get_mix_rate() const {
+ return mix_rate;
}
AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
-
- /* clang-format off */
- return get_speaker_mode_by_total_channels(EM_ASM_INT({
- const ref = Module.IDHandler.get($0);
- return ref && ref['context'] ? ref['context'].destination.channelCount : 0;
- }, _driver_id));
- /* clang-format on */
+ return get_speaker_mode_by_total_channels(channel_count);
}
-// No locking, as threads are not supported.
void AudioDriverJavaScript::lock() {
+ if (node) {
+ node->unlock();
+ }
}
void AudioDriverJavaScript::unlock() {
+ if (node) {
+ node->unlock();
+ }
}
void AudioDriverJavaScript::finish() {
-
- /* clang-format off */
- EM_ASM({
- Module.IDHandler.remove($0);
- }, _driver_id);
- /* clang-format on */
-
- 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;
}
- _driver_id = 0;
}
Error AudioDriverJavaScript::capture_start() {
-
+ lock();
input_buffer_init(buffer_length);
+ unlock();
+ if (godot_audio_capture_start()) {
+ return FAILED;
+ }
+ return OK;
+}
- /* clang-format off */
- EM_ASM({
- function gotMediaInput(stream) {
- var ref = Module.IDHandler.get($0);
- ref['stream'] = stream;
- ref['input'] = ref['context'].createMediaStreamSource(stream);
- ref['input'].connect(ref['script']);
- }
-
- function gotMediaInputError(e) {
- out(e);
- }
+Error AudioDriverJavaScript::capture_stop() {
+ godot_audio_capture_stop();
+ lock();
+ input_buffer.clear();
+ unlock();
+ return OK;
+}
- 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);
- }
- }, _driver_id);
- /* clang-format on */
+AudioDriverJavaScript::AudioDriverJavaScript() {
+ singleton = this;
+}
- return OK;
+#ifdef NO_THREADS
+/// ScriptProcessorNode implementation
+void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() {
+ AudioDriverJavaScript::singleton->_audio_driver_capture();
+ AudioDriverJavaScript::singleton->_audio_driver_process();
}
-Error AudioDriverJavaScript::capture_stop() {
+int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) {
+ return godot_audio_script_create(p_buffer_samples, p_channels);
+}
- /* clang-format off */
- EM_ASM({
- var ref = Module.IDHandler.get($0);
- if (ref['stream']) {
- const tracks = ref['stream'].getTracks();
- for (var i = 0; i < tracks.length; i++) {
- tracks[i].stop();
+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;
}
- ref['stream'] = null;
}
-
- if (ref['input']) {
- ref['input'].disconnect();
- ref['input'] = null;
+ 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);
+ }
+}
- }, _driver_id);
- /* clang-format on */
-
- input_buffer.clear();
+int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) {
+ godot_audio_worklet_create(p_channels);
+ return p_buffer_size;
+}
- return OK;
+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.start(_audio_thread_func, this);
}
-AudioDriverJavaScript::AudioDriverJavaScript() {
+void AudioDriverJavaScript::WorkletNode::lock() {
+ mutex.lock();
+}
- _driver_id = 0;
- internal_buffer = nullptr;
- buffer_length = 0;
+void AudioDriverJavaScript::WorkletNode::unlock() {
+ mutex.unlock();
+}
- singleton = this;
+void AudioDriverJavaScript::WorkletNode::finish() {
+ quit = true; // Ask thread to quit.
+ thread.wait_to_finish();
}
+#endif