mirror of
https://github.com/godotengine/godot.git
synced 2025-10-20 00:13:30 +00:00
[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:
parent
e52ed6d89e
commit
6d939b72f0
8 changed files with 722 additions and 220 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue