[HTML5] AudioWorklet API implementation.

Rewrote AudioDriverJavaScript to support multiple processor nodes.
The old (and deprecated) ScriptProcessorNode when threads are not
available, and the new AudioWorklet API when threads are enabled.

The new implementation uses two ring buffers and a shared state to
communicated with the AudioWorklet thread.

The audio.worklet.js JavaScript file is always added to the export
template, but only really used (and downloaded) in the thread build.
This commit is contained in:
Fabio Alessandrelli 2020-11-10 11:05:22 +01:00
parent e2083871eb
commit 179ec3ca0e
8 changed files with 726 additions and 211 deletions

View file

@ -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;
void AudioDriverJavaScript::_state_change_callback(int p_state) {
singleton->state = p_state;
}
void AudioDriverJavaScript::_latency_update_callback(float p_latency) {
singleton->output_latency = p_latency;
}
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 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;
}
obj->_audio_driver_process();
obj->needs_process = false;
obj->unlock();
to_write -= samples_high;
write_pos = 0;
}
}
#endif
void AudioDriverJavaScript::_audio_driver_process_start() {
#ifndef NO_THREADS
singleton->lock();
#else
singleton->_audio_driver_process();
#endif
}
void AudioDriverJavaScript::_audio_driver_process_end() {
#ifndef NO_THREADS
singleton->needs_process = true;
singleton->unlock();
#endif
}
void AudioDriverJavaScript::_audio_driver_process_capture(float p_sample) {
singleton->process_capture(p_sample);
}
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;
// 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) {
int32_t sample32 = int32_t(sample * 32768.f) * (1U << 16);
input_buffer_write(sample32);
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);
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