[HTML5] AudioWorkletAPI 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 download) in the thread build.
This commit is contained in:
Fabio Alessandrelli 2020-11-03 17:18:02 +01:00
parent e52ed6d89e
commit 6d939b72f0
8 changed files with 722 additions and 220 deletions

View file

@ -34,9 +34,7 @@
#include <emscripten.h>
#include "godot_audio.h"
AudioDriverJavaScript *AudioDriverJavaScript::singleton = NULL;
AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr;
bool AudioDriverJavaScript::is_available() {
return godot_audio_is_available() != 0;
@ -46,93 +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
mutex = Mutex::create();
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 {
@ -144,60 +158,135 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
}
void AudioDriverJavaScript::lock() {
#ifndef NO_THREADS
if (mutex) {
mutex->lock();
if (node) {
node->unlock();
}
#endif
}
void AudioDriverJavaScript::unlock() {
#ifndef NO_THREADS
if (mutex) {
mutex->unlock();
if (node) {
node->unlock();
}
#endif
}
void AudioDriverJavaScript::finish() {
#ifndef NO_THREADS
quit = true; // Ask thread to quit.
Thread::wait_to_finish(thread);
memdelete(thread);
thread = NULL;
memdelete(mutex);
mutex = NULL;
#endif
if (internal_buffer) {
memdelete_arr(internal_buffer);
internal_buffer = NULL;
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() {
internal_buffer = NULL;
buffer_length = 0;
mix_rate = 0;
channel_count = 0;
#ifndef NO_THREADS
mutex = NULL;
thread = NULL;
quit = false;
needs_process = true;
#endif
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);
mutex = Mutex::create();
thread = Thread::create(_audio_thread_func, this);
}
void AudioDriverJavaScript::WorkletNode::lock() {
if (mutex) {
mutex->lock();
}
}
void AudioDriverJavaScript::WorkletNode::unlock() {
if (mutex) {
mutex->unlock();
}
}
void AudioDriverJavaScript::WorkletNode::finish() {
quit = true; // Ask thread to quit.
Thread::wait_to_finish(thread);
memdelete(thread);
thread = nullptr;
memdelete(mutex);
mutex = nullptr;
}
#endif