mirror of
https://github.com/godotengine/godot.git
synced 2025-10-21 08:53:35 +00:00
[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:
parent
e2083871eb
commit
179ec3ca0e
8 changed files with 726 additions and 211 deletions
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue