[HTML5] Refactor audio drivers. Implement AudioWorklet w/o threads.
Performances are not great in general, bad on Firefox, on Chrome, well, it's an improvement compared to the way they broke ScriptProcessorNode. I'm actually surprised this works, it involves so many allocations, but there's no way around it when SharedArrayBuffer is not available :(.
This commit is contained in:
parent
da8cd3d7a7
commit
2024200182
7 changed files with 345 additions and 157 deletions
|
@ -34,22 +34,18 @@
|
||||||
|
|
||||||
#include <emscripten.h>
|
#include <emscripten.h>
|
||||||
|
|
||||||
AudioDriverJavaScript *AudioDriverJavaScript::singleton = nullptr;
|
AudioDriverJavaScript::AudioContext AudioDriverJavaScript::audio_context;
|
||||||
|
|
||||||
bool AudioDriverJavaScript::is_available() {
|
bool AudioDriverJavaScript::is_available() {
|
||||||
return godot_audio_is_available() != 0;
|
return godot_audio_is_available() != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *AudioDriverJavaScript::get_name() const {
|
|
||||||
return "JavaScript";
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioDriverJavaScript::_state_change_callback(int p_state) {
|
void AudioDriverJavaScript::_state_change_callback(int p_state) {
|
||||||
singleton->state = p_state;
|
AudioDriverJavaScript::audio_context.state = p_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::_latency_update_callback(float p_latency) {
|
void AudioDriverJavaScript::_latency_update_callback(float p_latency) {
|
||||||
singleton->output_latency = p_latency;
|
AudioDriverJavaScript::audio_context.output_latency = p_latency;
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) {
|
void AudioDriverJavaScript::_audio_driver_process(int p_from, int p_samples) {
|
||||||
|
@ -105,17 +101,19 @@ void AudioDriverJavaScript::_audio_driver_capture(int p_from, int p_samples) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Error AudioDriverJavaScript::init() {
|
Error AudioDriverJavaScript::init() {
|
||||||
mix_rate = GLOBAL_GET("audio/mix_rate");
|
|
||||||
int latency = GLOBAL_GET("audio/output_latency");
|
int latency = GLOBAL_GET("audio/output_latency");
|
||||||
|
if (!audio_context.inited) {
|
||||||
channel_count = godot_audio_init(&mix_rate, latency, &_state_change_callback, &_latency_update_callback);
|
audio_context.mix_rate = GLOBAL_GET("audio/mix_rate");
|
||||||
|
audio_context.channel_count = godot_audio_init(&audio_context.mix_rate, latency, &_state_change_callback, &_latency_update_callback);
|
||||||
|
audio_context.inited = true;
|
||||||
|
}
|
||||||
|
mix_rate = audio_context.mix_rate;
|
||||||
|
channel_count = audio_context.channel_count;
|
||||||
buffer_length = closest_power_of_2((latency * mix_rate / 1000));
|
buffer_length = closest_power_of_2((latency * mix_rate / 1000));
|
||||||
#ifndef NO_THREADS
|
Error err = create(buffer_length, channel_count);
|
||||||
node = memnew(WorkletNode);
|
if (err != OK) {
|
||||||
#else
|
return err;
|
||||||
node = memnew(ScriptProcessorNode);
|
}
|
||||||
#endif
|
|
||||||
buffer_length = node->create(buffer_length, channel_count);
|
|
||||||
if (output_rb) {
|
if (output_rb) {
|
||||||
memdelete_arr(output_rb);
|
memdelete_arr(output_rb);
|
||||||
}
|
}
|
||||||
|
@ -134,19 +132,17 @@ Error AudioDriverJavaScript::init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::start() {
|
void AudioDriverJavaScript::start() {
|
||||||
if (node) {
|
start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb));
|
||||||
node->start(output_rb, memarr_len(output_rb), input_rb, memarr_len(input_rb));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::resume() {
|
void AudioDriverJavaScript::resume() {
|
||||||
if (state == 0) { // 'suspended'
|
if (audio_context.state == 0) { // 'suspended'
|
||||||
godot_audio_resume();
|
godot_audio_resume();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
float AudioDriverJavaScript::get_latency() {
|
float AudioDriverJavaScript::get_latency() {
|
||||||
return output_latency + (float(buffer_length) / mix_rate);
|
return audio_context.output_latency + (float(buffer_length) / mix_rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioDriverJavaScript::get_mix_rate() const {
|
int AudioDriverJavaScript::get_mix_rate() const {
|
||||||
|
@ -157,24 +153,8 @@ AudioDriver::SpeakerMode AudioDriverJavaScript::get_speaker_mode() const {
|
||||||
return get_speaker_mode_by_total_channels(channel_count);
|
return get_speaker_mode_by_total_channels(channel_count);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::lock() {
|
|
||||||
if (node) {
|
|
||||||
node->unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioDriverJavaScript::unlock() {
|
|
||||||
if (node) {
|
|
||||||
node->unlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void AudioDriverJavaScript::finish() {
|
void AudioDriverJavaScript::finish() {
|
||||||
if (node) {
|
finish_driver();
|
||||||
node->finish();
|
|
||||||
memdelete(node);
|
|
||||||
node = nullptr;
|
|
||||||
}
|
|
||||||
if (output_rb) {
|
if (output_rb) {
|
||||||
memdelete_arr(output_rb);
|
memdelete_arr(output_rb);
|
||||||
output_rb = nullptr;
|
output_rb = nullptr;
|
||||||
|
@ -203,41 +183,66 @@ Error AudioDriverJavaScript::capture_stop() {
|
||||||
return OK;
|
return OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
AudioDriverJavaScript::AudioDriverJavaScript() {
|
|
||||||
singleton = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef NO_THREADS
|
#ifdef NO_THREADS
|
||||||
/// ScriptProcessorNode implementation
|
/// ScriptProcessorNode implementation
|
||||||
void AudioDriverJavaScript::ScriptProcessorNode::_process_callback() {
|
AudioDriverScriptProcessor *AudioDriverScriptProcessor::singleton = nullptr;
|
||||||
AudioDriverJavaScript::singleton->_audio_driver_capture();
|
|
||||||
AudioDriverJavaScript::singleton->_audio_driver_process();
|
void AudioDriverScriptProcessor::_process_callback() {
|
||||||
|
AudioDriverScriptProcessor::singleton->_audio_driver_capture();
|
||||||
|
AudioDriverScriptProcessor::singleton->_audio_driver_process();
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioDriverJavaScript::ScriptProcessorNode::create(int p_buffer_samples, int p_channels) {
|
Error AudioDriverScriptProcessor::create(int &p_buffer_samples, int p_channels) {
|
||||||
return godot_audio_script_create(p_buffer_samples, p_channels);
|
if (!godot_audio_has_script_processor()) {
|
||||||
|
return ERR_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
return (Error)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) {
|
void AudioDriverScriptProcessor::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);
|
godot_audio_script_start(p_in_buf, p_in_buf_size, p_out_buf, p_out_buf_size, &_process_callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AudioWorkletNode implementation (no threads)
|
||||||
|
AudioDriverWorklet *AudioDriverWorklet::singleton = nullptr;
|
||||||
|
|
||||||
|
Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) {
|
||||||
|
if (!godot_audio_has_worklet()) {
|
||||||
|
return ERR_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
return (Error)godot_audio_worklet_create(p_channels);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioDriverWorklet::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
|
||||||
|
_audio_driver_process();
|
||||||
|
godot_audio_worklet_start_no_threads(p_out_buf, p_out_buf_size, &_process_callback, p_in_buf, p_in_buf_size, &_capture_callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioDriverWorklet::_process_callback(int p_pos, int p_samples) {
|
||||||
|
AudioDriverWorklet *driver = AudioDriverWorklet::singleton;
|
||||||
|
driver->_audio_driver_process(p_pos, p_samples);
|
||||||
|
}
|
||||||
|
|
||||||
|
void AudioDriverWorklet::_capture_callback(int p_pos, int p_samples) {
|
||||||
|
AudioDriverWorklet *driver = AudioDriverWorklet::singleton;
|
||||||
|
driver->_audio_driver_capture(p_pos, p_samples);
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
/// AudioWorkletNode implementation
|
/// AudioWorkletNode implementation (threads)
|
||||||
void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) {
|
void AudioDriverWorklet::_audio_thread_func(void *p_data) {
|
||||||
AudioDriverJavaScript::WorkletNode *obj = static_cast<AudioDriverJavaScript::WorkletNode *>(p_data);
|
AudioDriverWorklet *driver = static_cast<AudioDriverWorklet *>(p_data);
|
||||||
AudioDriverJavaScript *driver = AudioDriverJavaScript::singleton;
|
const int out_samples = memarr_len(driver->get_output_rb());
|
||||||
const int out_samples = memarr_len(driver->output_rb);
|
const int in_samples = memarr_len(driver->get_input_rb());
|
||||||
const int in_samples = memarr_len(driver->input_rb);
|
|
||||||
int wpos = 0;
|
int wpos = 0;
|
||||||
int to_write = out_samples;
|
int to_write = out_samples;
|
||||||
int rpos = 0;
|
int rpos = 0;
|
||||||
int to_read = 0;
|
int to_read = 0;
|
||||||
int32_t step = 0;
|
int32_t step = 0;
|
||||||
while (!obj->quit) {
|
while (!driver->quit) {
|
||||||
if (to_read) {
|
if (to_read) {
|
||||||
driver->lock();
|
driver->lock();
|
||||||
driver->_audio_driver_capture(rpos, to_read);
|
driver->_audio_driver_capture(rpos, to_read);
|
||||||
godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_IN, -to_read);
|
godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_IN, -to_read);
|
||||||
driver->unlock();
|
driver->unlock();
|
||||||
rpos += to_read;
|
rpos += to_read;
|
||||||
if (rpos >= in_samples) {
|
if (rpos >= in_samples) {
|
||||||
|
@ -247,38 +252,40 @@ void AudioDriverJavaScript::WorkletNode::_audio_thread_func(void *p_data) {
|
||||||
if (to_write) {
|
if (to_write) {
|
||||||
driver->lock();
|
driver->lock();
|
||||||
driver->_audio_driver_process(wpos, to_write);
|
driver->_audio_driver_process(wpos, to_write);
|
||||||
godot_audio_worklet_state_add(obj->state, STATE_SAMPLES_OUT, to_write);
|
godot_audio_worklet_state_add(driver->state, STATE_SAMPLES_OUT, to_write);
|
||||||
driver->unlock();
|
driver->unlock();
|
||||||
wpos += to_write;
|
wpos += to_write;
|
||||||
if (wpos >= out_samples) {
|
if (wpos >= out_samples) {
|
||||||
wpos -= out_samples;
|
wpos -= out_samples;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
step = godot_audio_worklet_state_wait(obj->state, STATE_PROCESS, step, 1);
|
step = godot_audio_worklet_state_wait(driver->state, STATE_PROCESS, step, 1);
|
||||||
to_write = out_samples - godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_OUT);
|
to_write = out_samples - godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_OUT);
|
||||||
to_read = godot_audio_worklet_state_get(obj->state, STATE_SAMPLES_IN);
|
to_read = godot_audio_worklet_state_get(driver->state, STATE_SAMPLES_IN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int AudioDriverJavaScript::WorkletNode::create(int p_buffer_size, int p_channels) {
|
Error AudioDriverWorklet::create(int &p_buffer_size, int p_channels) {
|
||||||
godot_audio_worklet_create(p_channels);
|
if (!godot_audio_has_worklet()) {
|
||||||
return p_buffer_size;
|
return ERR_UNAVAILABLE;
|
||||||
|
}
|
||||||
|
return (Error)godot_audio_worklet_create(p_channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::WorkletNode::start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) {
|
void AudioDriverWorklet::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);
|
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);
|
thread.start(_audio_thread_func, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::WorkletNode::lock() {
|
void AudioDriverWorklet::lock() {
|
||||||
mutex.lock();
|
mutex.lock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::WorkletNode::unlock() {
|
void AudioDriverWorklet::unlock() {
|
||||||
mutex.unlock();
|
mutex.unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AudioDriverJavaScript::WorkletNode::finish() {
|
void AudioDriverWorklet::finish_driver() {
|
||||||
quit = true; // Ask thread to quit.
|
quit = true; // Ask thread to quit.
|
||||||
thread.wait_to_finish();
|
thread.wait_to_finish();
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,52 +38,15 @@
|
||||||
#include "godot_audio.h"
|
#include "godot_audio.h"
|
||||||
|
|
||||||
class AudioDriverJavaScript : public AudioDriver {
|
class AudioDriverJavaScript : public AudioDriver {
|
||||||
public:
|
|
||||||
class AudioNode {
|
|
||||||
public:
|
|
||||||
virtual int create(int p_buffer_size, int p_output_channels) = 0;
|
|
||||||
virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0;
|
|
||||||
virtual void finish() {}
|
|
||||||
virtual void lock() {}
|
|
||||||
virtual void unlock() {}
|
|
||||||
virtual ~AudioNode() {}
|
|
||||||
};
|
|
||||||
|
|
||||||
class WorkletNode : public AudioNode {
|
|
||||||
private:
|
|
||||||
enum {
|
|
||||||
STATE_LOCK,
|
|
||||||
STATE_PROCESS,
|
|
||||||
STATE_SAMPLES_IN,
|
|
||||||
STATE_SAMPLES_OUT,
|
|
||||||
STATE_MAX,
|
|
||||||
};
|
|
||||||
Mutex mutex;
|
|
||||||
Thread thread;
|
|
||||||
bool quit = false;
|
|
||||||
int32_t state[STATE_MAX] = { 0 };
|
|
||||||
|
|
||||||
static void _audio_thread_func(void *p_data);
|
|
||||||
|
|
||||||
public:
|
|
||||||
int create(int p_buffer_size, int p_output_channels) override;
|
|
||||||
void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
|
|
||||||
void finish() override;
|
|
||||||
void lock() override;
|
|
||||||
void unlock() override;
|
|
||||||
};
|
|
||||||
|
|
||||||
class ScriptProcessorNode : public AudioNode {
|
|
||||||
private:
|
|
||||||
static void _process_callback();
|
|
||||||
|
|
||||||
public:
|
|
||||||
int create(int p_buffer_samples, int p_channels) override;
|
|
||||||
void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
AudioNode *node = nullptr;
|
struct AudioContext {
|
||||||
|
bool inited = false;
|
||||||
|
float output_latency = 0.0;
|
||||||
|
int state = -1;
|
||||||
|
int channel_count = 0;
|
||||||
|
int mix_rate = 0;
|
||||||
|
};
|
||||||
|
static AudioContext audio_context;
|
||||||
|
|
||||||
float *output_rb = nullptr;
|
float *output_rb = nullptr;
|
||||||
float *input_rb = nullptr;
|
float *input_rb = nullptr;
|
||||||
|
@ -91,36 +54,108 @@ private:
|
||||||
int buffer_length = 0;
|
int buffer_length = 0;
|
||||||
int mix_rate = 0;
|
int mix_rate = 0;
|
||||||
int channel_count = 0;
|
int channel_count = 0;
|
||||||
int state = 0;
|
|
||||||
float output_latency = 0.0;
|
|
||||||
|
|
||||||
static void _state_change_callback(int p_state);
|
static void _state_change_callback(int p_state);
|
||||||
static void _latency_update_callback(float p_latency);
|
static void _latency_update_callback(float p_latency);
|
||||||
|
|
||||||
|
static AudioDriverJavaScript *singleton;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void _audio_driver_process(int p_from = 0, int p_samples = 0);
|
void _audio_driver_process(int p_from = 0, int p_samples = 0);
|
||||||
void _audio_driver_capture(int p_from = 0, int p_samples = 0);
|
void _audio_driver_capture(int p_from = 0, int p_samples = 0);
|
||||||
|
float *get_output_rb() const { return output_rb; }
|
||||||
|
float *get_input_rb() const { return input_rb; }
|
||||||
|
|
||||||
|
virtual Error create(int &p_buffer_samples, int p_channels) = 0;
|
||||||
|
virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) = 0;
|
||||||
|
virtual void finish_driver() {}
|
||||||
|
|
||||||
public:
|
public:
|
||||||
static bool is_available();
|
static bool is_available();
|
||||||
|
|
||||||
static AudioDriverJavaScript *singleton;
|
virtual Error init() final;
|
||||||
|
virtual void start() final;
|
||||||
|
virtual void finish() final;
|
||||||
|
|
||||||
virtual const char *get_name() const;
|
virtual float get_latency() override;
|
||||||
|
virtual int get_mix_rate() const override;
|
||||||
|
virtual SpeakerMode get_speaker_mode() const override;
|
||||||
|
|
||||||
virtual Error init();
|
virtual Error capture_start() override;
|
||||||
virtual void start();
|
virtual Error capture_stop() override;
|
||||||
void resume();
|
|
||||||
virtual float get_latency();
|
|
||||||
virtual int get_mix_rate() const;
|
|
||||||
virtual SpeakerMode get_speaker_mode() const;
|
|
||||||
virtual void lock();
|
|
||||||
virtual void unlock();
|
|
||||||
virtual void finish();
|
|
||||||
|
|
||||||
virtual Error capture_start();
|
static void resume();
|
||||||
virtual Error capture_stop();
|
|
||||||
|
|
||||||
AudioDriverJavaScript();
|
AudioDriverJavaScript() {}
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef NO_THREADS
|
||||||
|
class AudioDriverScriptProcessor : public AudioDriverJavaScript {
|
||||||
|
private:
|
||||||
|
static void _process_callback();
|
||||||
|
|
||||||
|
static AudioDriverScriptProcessor *singleton;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
Error create(int &p_buffer_samples, int p_channels) override;
|
||||||
|
void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual const char *get_name() const override { return "ScriptProcessor"; }
|
||||||
|
|
||||||
|
virtual void lock() override {}
|
||||||
|
virtual void unlock() override {}
|
||||||
|
|
||||||
|
AudioDriverScriptProcessor() { singleton = this; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class AudioDriverWorklet : public AudioDriverJavaScript {
|
||||||
|
private:
|
||||||
|
static void _process_callback(int p_pos, int p_samples);
|
||||||
|
static void _capture_callback(int p_pos, int p_samples);
|
||||||
|
|
||||||
|
static AudioDriverWorklet *singleton;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual Error create(int &p_buffer_size, int p_output_channels) override;
|
||||||
|
virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual const char *get_name() const override { return "AudioWorklet"; }
|
||||||
|
|
||||||
|
virtual void lock() override {}
|
||||||
|
virtual void unlock() override {}
|
||||||
|
|
||||||
|
AudioDriverWorklet() { singleton = this; }
|
||||||
|
};
|
||||||
|
#else
|
||||||
|
class AudioDriverWorklet : public AudioDriverJavaScript {
|
||||||
|
private:
|
||||||
|
enum {
|
||||||
|
STATE_LOCK,
|
||||||
|
STATE_PROCESS,
|
||||||
|
STATE_SAMPLES_IN,
|
||||||
|
STATE_SAMPLES_OUT,
|
||||||
|
STATE_MAX,
|
||||||
|
};
|
||||||
|
Mutex mutex;
|
||||||
|
Thread thread;
|
||||||
|
bool quit = false;
|
||||||
|
int32_t state[STATE_MAX] = { 0 };
|
||||||
|
|
||||||
|
static void _audio_thread_func(void *p_data);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
virtual Error create(int &p_buffer_size, int p_output_channels) override;
|
||||||
|
virtual void start(float *p_out_buf, int p_out_buf_size, float *p_in_buf, int p_in_buf_size) override;
|
||||||
|
virtual void finish_driver() override;
|
||||||
|
|
||||||
|
public:
|
||||||
|
virtual const char *get_name() const override { return "AudioWorklet"; }
|
||||||
|
|
||||||
|
void lock() override;
|
||||||
|
void unlock() override;
|
||||||
};
|
};
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
|
@ -38,6 +38,8 @@ extern "C" {
|
||||||
#include "stddef.h"
|
#include "stddef.h"
|
||||||
|
|
||||||
extern int godot_audio_is_available();
|
extern int godot_audio_is_available();
|
||||||
|
extern int godot_audio_has_worklet();
|
||||||
|
extern int godot_audio_has_script_processor();
|
||||||
extern int godot_audio_init(int *p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float));
|
extern int godot_audio_init(int *p_mix_rate, int p_latency, void (*_state_cb)(int), void (*_latency_cb)(float));
|
||||||
extern void godot_audio_resume();
|
extern void godot_audio_resume();
|
||||||
|
|
||||||
|
@ -46,14 +48,15 @@ extern void godot_audio_capture_stop();
|
||||||
|
|
||||||
// Worklet
|
// Worklet
|
||||||
typedef int32_t GodotAudioState[4];
|
typedef int32_t GodotAudioState[4];
|
||||||
extern void godot_audio_worklet_create(int p_channels);
|
extern int godot_audio_worklet_create(int p_channels);
|
||||||
extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state);
|
extern void godot_audio_worklet_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, GodotAudioState p_state);
|
||||||
|
extern void godot_audio_worklet_start_no_threads(float *p_out_buf, int p_out_size, void (*p_out_cb)(int p_pos, int p_frames), float *p_in_buf, int p_in_size, void (*p_in_cb)(int p_pos, int p_frames));
|
||||||
extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
|
extern int godot_audio_worklet_state_add(GodotAudioState p_state, int p_idx, int p_value);
|
||||||
extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx);
|
extern int godot_audio_worklet_state_get(GodotAudioState p_state, int p_idx);
|
||||||
extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout);
|
extern int godot_audio_worklet_state_wait(int32_t *p_state, int p_idx, int32_t p_expected, int p_timeout);
|
||||||
|
|
||||||
// Script
|
// Script
|
||||||
extern int godot_audio_script_create(int p_buffer_size, int p_channels);
|
extern int godot_audio_script_create(int *p_buffer_size, int p_channels);
|
||||||
extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)());
|
extern void godot_audio_script_start(float *p_in_buf, int p_in_size, float *p_out_buf, int p_out_size, void (*p_cb)());
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
|
|
|
@ -29,15 +29,16 @@
|
||||||
/*************************************************************************/
|
/*************************************************************************/
|
||||||
|
|
||||||
class RingBuffer {
|
class RingBuffer {
|
||||||
constructor(p_buffer, p_state) {
|
constructor(p_buffer, p_state, p_threads) {
|
||||||
this.buffer = p_buffer;
|
this.buffer = p_buffer;
|
||||||
this.avail = p_state;
|
this.avail = p_state;
|
||||||
|
this.threads = p_threads;
|
||||||
this.rpos = 0;
|
this.rpos = 0;
|
||||||
this.wpos = 0;
|
this.wpos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
data_left() {
|
data_left() {
|
||||||
return Atomics.load(this.avail, 0);
|
return this.threads ? Atomics.load(this.avail, 0) : this.avail;
|
||||||
}
|
}
|
||||||
|
|
||||||
space_left() {
|
space_left() {
|
||||||
|
@ -55,10 +56,16 @@ class RingBuffer {
|
||||||
to_write -= high;
|
to_write -= high;
|
||||||
this.rpos = 0;
|
this.rpos = 0;
|
||||||
}
|
}
|
||||||
output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
|
if (to_write) {
|
||||||
|
output.set(this.buffer.subarray(this.rpos, this.rpos + to_write), from);
|
||||||
|
}
|
||||||
this.rpos += to_write;
|
this.rpos += to_write;
|
||||||
Atomics.add(this.avail, 0, -output.length);
|
if (this.threads) {
|
||||||
Atomics.notify(this.avail, 0);
|
Atomics.add(this.avail, 0, -output.length);
|
||||||
|
Atomics.notify(this.avail, 0);
|
||||||
|
} else {
|
||||||
|
this.avail -= output.length;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
write(p_buffer) {
|
write(p_buffer) {
|
||||||
|
@ -77,14 +84,19 @@ class RingBuffer {
|
||||||
this.buffer.set(low);
|
this.buffer.set(low);
|
||||||
this.wpos = low.length;
|
this.wpos = low.length;
|
||||||
}
|
}
|
||||||
Atomics.add(this.avail, 0, to_write);
|
if (this.threads) {
|
||||||
Atomics.notify(this.avail, 0);
|
Atomics.add(this.avail, 0, to_write);
|
||||||
|
Atomics.notify(this.avail, 0);
|
||||||
|
} else {
|
||||||
|
this.avail += to_write;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GodotProcessor extends AudioWorkletProcessor {
|
class GodotProcessor extends AudioWorkletProcessor {
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
this.threads = false;
|
||||||
this.running = true;
|
this.running = true;
|
||||||
this.lock = null;
|
this.lock = null;
|
||||||
this.notifier = null;
|
this.notifier = null;
|
||||||
|
@ -100,24 +112,31 @@ class GodotProcessor extends AudioWorkletProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
process_notify() {
|
process_notify() {
|
||||||
Atomics.add(this.notifier, 0, 1);
|
if (this.notifier) {
|
||||||
Atomics.notify(this.notifier, 0);
|
Atomics.add(this.notifier, 0, 1);
|
||||||
|
Atomics.notify(this.notifier, 0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_message(p_cmd, p_data) {
|
parse_message(p_cmd, p_data) {
|
||||||
if (p_cmd === 'start' && p_data) {
|
if (p_cmd === 'start' && p_data) {
|
||||||
const state = p_data[0];
|
const state = p_data[0];
|
||||||
let idx = 0;
|
let idx = 0;
|
||||||
|
this.threads = true;
|
||||||
this.lock = state.subarray(idx, ++idx);
|
this.lock = state.subarray(idx, ++idx);
|
||||||
this.notifier = state.subarray(idx, ++idx);
|
this.notifier = state.subarray(idx, ++idx);
|
||||||
const avail_in = state.subarray(idx, ++idx);
|
const avail_in = state.subarray(idx, ++idx);
|
||||||
const avail_out = state.subarray(idx, ++idx);
|
const avail_out = state.subarray(idx, ++idx);
|
||||||
this.input = new RingBuffer(p_data[1], avail_in);
|
this.input = new RingBuffer(p_data[1], avail_in, true);
|
||||||
this.output = new RingBuffer(p_data[2], avail_out);
|
this.output = new RingBuffer(p_data[2], avail_out, true);
|
||||||
} else if (p_cmd === 'stop') {
|
} else if (p_cmd === 'stop') {
|
||||||
this.runing = false;
|
this.running = false;
|
||||||
this.output = null;
|
this.output = null;
|
||||||
this.input = null;
|
this.input = null;
|
||||||
|
} else if (p_cmd === 'start_nothreads') {
|
||||||
|
this.output = new RingBuffer(p_data[0], p_data[0].length, false);
|
||||||
|
} else if (p_cmd === 'chunk') {
|
||||||
|
this.output.write(p_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -139,7 +158,10 @@ class GodotProcessor extends AudioWorkletProcessor {
|
||||||
if (this.input_buffer.length !== chunk) {
|
if (this.input_buffer.length !== chunk) {
|
||||||
this.input_buffer = new Float32Array(chunk);
|
this.input_buffer = new Float32Array(chunk);
|
||||||
}
|
}
|
||||||
if (this.input.space_left() >= chunk) {
|
if (!this.threads) {
|
||||||
|
GodotProcessor.write_input(this.input_buffer, input);
|
||||||
|
this.port.postMessage({ 'cmd': 'input', 'data': this.input_buffer });
|
||||||
|
} else if (this.input.space_left() >= chunk) {
|
||||||
GodotProcessor.write_input(this.input_buffer, input);
|
GodotProcessor.write_input(this.input_buffer, input);
|
||||||
this.input.write(this.input_buffer);
|
this.input.write(this.input_buffer);
|
||||||
} else {
|
} else {
|
||||||
|
@ -156,6 +178,9 @@ class GodotProcessor extends AudioWorkletProcessor {
|
||||||
if (this.output.data_left() >= chunk) {
|
if (this.output.data_left() >= chunk) {
|
||||||
this.output.read(this.output_buffer);
|
this.output.read(this.output_buffer);
|
||||||
GodotProcessor.write_output(output, this.output_buffer);
|
GodotProcessor.write_output(output, this.output_buffer);
|
||||||
|
if (!this.threads) {
|
||||||
|
this.port.postMessage({ 'cmd': 'read', 'data': chunk });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
|
this.port.postMessage('Output buffer has not enough frames! Skipping output frame.');
|
||||||
}
|
}
|
||||||
|
|
|
@ -159,6 +159,16 @@ const GodotAudio = {
|
||||||
return 1;
|
return 1;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
godot_audio_has_worklet__sig: 'i',
|
||||||
|
godot_audio_has_worklet: function () {
|
||||||
|
return (GodotAudio.ctx && GodotAudio.ctx.audioWorklet) ? 1 : 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
godot_audio_has_script_processor__sig: 'i',
|
||||||
|
godot_audio_has_script_processor: function () {
|
||||||
|
return (GodotAudio.ctx && GodotAudio.ctx.createScriptProcessor) ? 1 : 0;
|
||||||
|
},
|
||||||
|
|
||||||
godot_audio_init__sig: 'iiiii',
|
godot_audio_init__sig: 'iiiii',
|
||||||
godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) {
|
godot_audio_init: function (p_mix_rate, p_latency, p_state_change, p_latency_update) {
|
||||||
const statechange = GodotRuntime.get_func(p_state_change);
|
const statechange = GodotRuntime.get_func(p_state_change);
|
||||||
|
@ -209,6 +219,7 @@ const GodotAudioWorklet = {
|
||||||
$GodotAudioWorklet: {
|
$GodotAudioWorklet: {
|
||||||
promise: null,
|
promise: null,
|
||||||
worklet: null,
|
worklet: null,
|
||||||
|
ring_buffer: null,
|
||||||
|
|
||||||
create: function (channels) {
|
create: function (channels) {
|
||||||
const path = GodotConfig.locate_file('godot.audio.worklet.js');
|
const path = GodotConfig.locate_file('godot.audio.worklet.js');
|
||||||
|
@ -239,6 +250,86 @@ const GodotAudioWorklet = {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
start_no_threads: function (p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback) {
|
||||||
|
function RingBuffer() {
|
||||||
|
let wpos = 0;
|
||||||
|
let rpos = 0;
|
||||||
|
let pending_samples = 0;
|
||||||
|
const wbuf = new Float32Array(p_out_size);
|
||||||
|
|
||||||
|
function send(port) {
|
||||||
|
if (pending_samples === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const buffer = GodotRuntime.heapSub(HEAPF32, p_out_buf, p_out_size);
|
||||||
|
const size = buffer.length;
|
||||||
|
const tot_sent = pending_samples;
|
||||||
|
out_callback(wpos, pending_samples);
|
||||||
|
if (wpos + pending_samples >= size) {
|
||||||
|
const high = size - wpos;
|
||||||
|
wbuf.set(buffer.subarray(wpos, size));
|
||||||
|
pending_samples -= high;
|
||||||
|
wpos = 0;
|
||||||
|
}
|
||||||
|
if (pending_samples > 0) {
|
||||||
|
wbuf.set(buffer.subarray(wpos, wpos + pending_samples), tot_sent - pending_samples);
|
||||||
|
}
|
||||||
|
port.postMessage({ 'cmd': 'chunk', 'data': wbuf.subarray(0, tot_sent) });
|
||||||
|
wpos += pending_samples;
|
||||||
|
pending_samples = 0;
|
||||||
|
}
|
||||||
|
this.receive = function (recv_buf) {
|
||||||
|
const buffer = GodotRuntime.heapSub(HEAPF32, p_in_buf, p_in_size);
|
||||||
|
const from = rpos;
|
||||||
|
let to_write = recv_buf.length;
|
||||||
|
let high = 0;
|
||||||
|
if (rpos + to_write >= p_in_size) {
|
||||||
|
high = p_in_size - rpos;
|
||||||
|
buffer.set(recv_buf.subarray(0, high), rpos);
|
||||||
|
to_write -= high;
|
||||||
|
rpos = 0;
|
||||||
|
}
|
||||||
|
if (to_write) {
|
||||||
|
buffer.set(recv_buf.subarray(high, to_write), rpos);
|
||||||
|
}
|
||||||
|
in_callback(from, recv_buf.length);
|
||||||
|
rpos += to_write;
|
||||||
|
};
|
||||||
|
this.consumed = function (size, port) {
|
||||||
|
pending_samples += size;
|
||||||
|
send(port);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
GodotAudioWorklet.ring_buffer = new RingBuffer();
|
||||||
|
GodotAudioWorklet.promise.then(function () {
|
||||||
|
const node = GodotAudioWorklet.worklet;
|
||||||
|
const buffer = GodotRuntime.heapSlice(HEAPF32, p_out_buf, p_out_size);
|
||||||
|
node.connect(GodotAudio.ctx.destination);
|
||||||
|
node.port.postMessage({
|
||||||
|
'cmd': 'start_nothreads',
|
||||||
|
'data': [buffer, p_in_size],
|
||||||
|
});
|
||||||
|
node.port.onmessage = function (event) {
|
||||||
|
if (!GodotAudioWorklet.worklet) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (event.data['cmd'] === 'read') {
|
||||||
|
const read = event.data['data'];
|
||||||
|
GodotAudioWorklet.ring_buffer.consumed(read, GodotAudioWorklet.worklet.port);
|
||||||
|
} else if (event.data['cmd'] === 'input') {
|
||||||
|
const buf = event.data['data'];
|
||||||
|
if (buf.length > p_in_size) {
|
||||||
|
GodotRuntime.error('Input chunk is too big');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GodotAudioWorklet.ring_buffer.receive(buf);
|
||||||
|
} else {
|
||||||
|
GodotRuntime.error(event.data);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
get_node: function () {
|
get_node: function () {
|
||||||
return GodotAudioWorklet.worklet;
|
return GodotAudioWorklet.worklet;
|
||||||
},
|
},
|
||||||
|
@ -262,9 +353,15 @@ const GodotAudioWorklet = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
godot_audio_worklet_create__sig: 'vi',
|
godot_audio_worklet_create__sig: 'ii',
|
||||||
godot_audio_worklet_create: function (channels) {
|
godot_audio_worklet_create: function (channels) {
|
||||||
GodotAudioWorklet.create(channels);
|
try {
|
||||||
|
GodotAudioWorklet.create(channels);
|
||||||
|
} catch (e) {
|
||||||
|
GodotRuntime.error('Error starting AudioDriverWorklet', e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
godot_audio_worklet_start__sig: 'viiiii',
|
godot_audio_worklet_start__sig: 'viiiii',
|
||||||
|
@ -275,6 +372,13 @@ const GodotAudioWorklet = {
|
||||||
GodotAudioWorklet.start(in_buffer, out_buffer, state);
|
GodotAudioWorklet.start(in_buffer, out_buffer, state);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
godot_audio_worklet_start_no_threads__sig: 'viiiiii',
|
||||||
|
godot_audio_worklet_start_no_threads: function (p_out_buf, p_out_size, p_out_callback, p_in_buf, p_in_size, p_in_callback) {
|
||||||
|
const out_callback = GodotRuntime.get_func(p_out_callback);
|
||||||
|
const in_callback = GodotRuntime.get_func(p_in_callback);
|
||||||
|
GodotAudioWorklet.start_no_threads(p_out_buf, p_out_size, out_callback, p_in_buf, p_in_size, in_callback);
|
||||||
|
},
|
||||||
|
|
||||||
godot_audio_worklet_state_wait__sig: 'iiii',
|
godot_audio_worklet_state_wait__sig: 'iiii',
|
||||||
godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) {
|
godot_audio_worklet_state_wait: function (p_state, p_idx, p_expected, p_timeout) {
|
||||||
Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
|
Atomics.wait(HEAP32, (p_state >> 2) + p_idx, p_expected, p_timeout);
|
||||||
|
@ -358,7 +462,15 @@ const GodotAudioScript = {
|
||||||
|
|
||||||
godot_audio_script_create__sig: 'iii',
|
godot_audio_script_create__sig: 'iii',
|
||||||
godot_audio_script_create: function (buffer_length, channel_count) {
|
godot_audio_script_create: function (buffer_length, channel_count) {
|
||||||
return GodotAudioScript.create(buffer_length, channel_count);
|
const buf_len = GodotRuntime.getHeapValue(buffer_length, 'i32');
|
||||||
|
try {
|
||||||
|
const out_len = GodotAudioScript.create(buf_len, channel_count);
|
||||||
|
GodotRuntime.setHeapValue(buffer_length, out_len, 'i32');
|
||||||
|
} catch (e) {
|
||||||
|
GodotRuntime.error('Error starting AudioDriverScriptProcessor', e);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
},
|
},
|
||||||
|
|
||||||
godot_audio_script_start__sig: 'viiiii',
|
godot_audio_script_start__sig: 'viiiii',
|
||||||
|
|
|
@ -751,11 +751,14 @@ const char *OS_JavaScript::get_video_driver_name(int p_driver) const {
|
||||||
// Audio
|
// Audio
|
||||||
|
|
||||||
int OS_JavaScript::get_audio_driver_count() const {
|
int OS_JavaScript::get_audio_driver_count() const {
|
||||||
return 1;
|
return audio_drivers.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
const char *OS_JavaScript::get_audio_driver_name(int p_driver) const {
|
const char *OS_JavaScript::get_audio_driver_name(int p_driver) const {
|
||||||
return "JavaScript";
|
if (audio_drivers.size() <= p_driver) {
|
||||||
|
return "Unknown";
|
||||||
|
}
|
||||||
|
return audio_drivers[p_driver]->get_name();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clipboard
|
// Clipboard
|
||||||
|
@ -961,9 +964,7 @@ MainLoop *OS_JavaScript::get_main_loop() const {
|
||||||
}
|
}
|
||||||
|
|
||||||
void OS_JavaScript::resume_audio() {
|
void OS_JavaScript::resume_audio() {
|
||||||
if (audio_driver_javascript) {
|
AudioDriverJavaScript::resume();
|
||||||
audio_driver_javascript->resume();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OS_JavaScript::fs_sync_callback() {
|
void OS_JavaScript::fs_sync_callback() {
|
||||||
|
@ -1021,9 +1022,10 @@ void OS_JavaScript::finalize() {
|
||||||
emscripten_webgl_commit_frame();
|
emscripten_webgl_commit_frame();
|
||||||
memdelete(visual_server);
|
memdelete(visual_server);
|
||||||
emscripten_webgl_destroy_context(webgl_ctx);
|
emscripten_webgl_destroy_context(webgl_ctx);
|
||||||
if (audio_driver_javascript) {
|
for (int i = 0; i < audio_drivers.size(); i++) {
|
||||||
memdelete(audio_driver_javascript);
|
memdelete(audio_drivers[i]);
|
||||||
}
|
}
|
||||||
|
audio_drivers.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Miscellaneous
|
// Miscellaneous
|
||||||
|
@ -1217,7 +1219,6 @@ OS_JavaScript::OS_JavaScript() {
|
||||||
|
|
||||||
main_loop = NULL;
|
main_loop = NULL;
|
||||||
visual_server = NULL;
|
visual_server = NULL;
|
||||||
audio_driver_javascript = NULL;
|
|
||||||
|
|
||||||
swap_ok_cancel = false;
|
swap_ok_cancel = false;
|
||||||
idb_available = godot_js_os_fs_is_persistent() != 0;
|
idb_available = godot_js_os_fs_is_persistent() != 0;
|
||||||
|
@ -1225,8 +1226,13 @@ OS_JavaScript::OS_JavaScript() {
|
||||||
idb_is_syncing = false;
|
idb_is_syncing = false;
|
||||||
|
|
||||||
if (AudioDriverJavaScript::is_available()) {
|
if (AudioDriverJavaScript::is_available()) {
|
||||||
audio_driver_javascript = memnew(AudioDriverJavaScript);
|
#ifdef NO_THREADS
|
||||||
AudioDriverManager::add_driver(audio_driver_javascript);
|
audio_drivers.push_back(memnew(AudioDriverScriptProcessor));
|
||||||
|
#endif
|
||||||
|
audio_drivers.push_back(memnew(AudioDriverWorklet));
|
||||||
|
}
|
||||||
|
for (int i = 0; i < audio_drivers.size(); i++) {
|
||||||
|
AudioDriverManager::add_driver(audio_drivers[i]);
|
||||||
}
|
}
|
||||||
|
|
||||||
Vector<Logger *> loggers;
|
Vector<Logger *> loggers;
|
||||||
|
|
|
@ -62,7 +62,7 @@ private:
|
||||||
|
|
||||||
MainLoop *main_loop;
|
MainLoop *main_loop;
|
||||||
int video_driver_index;
|
int video_driver_index;
|
||||||
AudioDriverJavaScript *audio_driver_javascript;
|
List<AudioDriverJavaScript *> audio_drivers;
|
||||||
VisualServer *visual_server;
|
VisualServer *visual_server;
|
||||||
|
|
||||||
bool swap_ok_cancel;
|
bool swap_ok_cancel;
|
||||||
|
|
Loading…
Reference in a new issue