Merge pull request #52650 from Faless/js/3.x_audioworklet_nothreads_pr
[3.x] [HTML5] Refactor audio drivers. Implement AudioWorklet w/o threads.
This commit is contained in:
commit
8a48be6980
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