1126 lines
31 KiB
C
1126 lines
31 KiB
C
#include "pxl8_sfx.h"
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "pxl8_hal.h"
|
|
#include "pxl8_log.h"
|
|
#include "pxl8_math.h"
|
|
|
|
#define VOICE_SCALE 0.15f
|
|
|
|
static inline f32 sanitize_audio(f32 x) {
|
|
union { f32 f; u32 u; } conv = { .f = x };
|
|
return ((conv.u & 0x7F800000) == 0x7F800000) ? 0.0f : x;
|
|
}
|
|
|
|
typedef enum envelope_state {
|
|
ENV_ATTACK = 0,
|
|
ENV_DECAY,
|
|
ENV_IDLE,
|
|
ENV_RELEASE,
|
|
ENV_SUSTAIN
|
|
} envelope_state;
|
|
|
|
typedef struct envelope {
|
|
pxl8_sfx_adsr adsr;
|
|
envelope_state state;
|
|
f32 level;
|
|
f32 release_level;
|
|
f32 time;
|
|
} envelope;
|
|
|
|
typedef struct oscillator {
|
|
pxl8_sfx_waveform waveform;
|
|
f32 frequency;
|
|
f32 phase;
|
|
f32 pulse_width;
|
|
u32 noise_state;
|
|
} oscillator;
|
|
|
|
typedef struct svf {
|
|
pxl8_sfx_filter_type type;
|
|
f32 cutoff;
|
|
f32 resonance;
|
|
f32 low;
|
|
f32 band;
|
|
f32 high;
|
|
} svf;
|
|
|
|
typedef struct lfo {
|
|
pxl8_sfx_waveform waveform;
|
|
pxl8_sfx_lfo_target target;
|
|
f32 rate;
|
|
f32 depth;
|
|
f32 phase;
|
|
} lfo;
|
|
|
|
typedef struct voice {
|
|
bool active;
|
|
u16 id;
|
|
u8 note;
|
|
f32 volume;
|
|
f32 pan;
|
|
f32 base_frequency;
|
|
f32 base_cutoff;
|
|
f32 filter_env_depth;
|
|
f32 fx_send;
|
|
f32 start_time;
|
|
f32 duration;
|
|
bool released;
|
|
oscillator osc;
|
|
envelope amp_env;
|
|
envelope filter_env;
|
|
svf filter;
|
|
lfo mod_lfo;
|
|
bool has_lfo;
|
|
} voice;
|
|
|
|
typedef struct delay_state {
|
|
f32* buffer_l;
|
|
f32* buffer_r;
|
|
u32 delay_samples_l;
|
|
u32 delay_samples_r;
|
|
f32 feedback;
|
|
f32 mix;
|
|
u32 write_pos;
|
|
} delay_state;
|
|
|
|
typedef struct comb_filter {
|
|
f32* buffer;
|
|
u32 size;
|
|
f32 feedback;
|
|
f32 damping;
|
|
f32 filter_state;
|
|
u32 index;
|
|
} comb_filter;
|
|
|
|
typedef struct allpass_filter {
|
|
f32* buffer;
|
|
u32 size;
|
|
f32 feedback;
|
|
u32 index;
|
|
} allpass_filter;
|
|
|
|
typedef struct reverb_state {
|
|
comb_filter combs[4];
|
|
allpass_filter allpasses[2];
|
|
f32 damping;
|
|
f32 mix;
|
|
f32 room;
|
|
} reverb_state;
|
|
|
|
typedef struct compressor_state {
|
|
f32 threshold;
|
|
f32 ratio;
|
|
f32 attack_coeff;
|
|
f32 release_coeff;
|
|
f32 envelope;
|
|
f32 knee_width;
|
|
f32 makeup_gain;
|
|
} compressor_state;
|
|
|
|
struct pxl8_sfx_node {
|
|
pxl8_sfx_node_type type;
|
|
void* state;
|
|
void (*process)(void* state, f32* left, f32* right);
|
|
void (*destroy)(void* state);
|
|
struct pxl8_sfx_node* next;
|
|
};
|
|
|
|
struct pxl8_sfx_context {
|
|
voice voices[PXL8_SFX_MAX_VOICES];
|
|
pxl8_sfx_node* fx_head;
|
|
pxl8_sfx_mixer* mixer;
|
|
f32 volume;
|
|
f32 current_time;
|
|
u16 next_voice_id;
|
|
u8 context_id;
|
|
};
|
|
|
|
struct pxl8_sfx_mixer {
|
|
const pxl8_hal* hal;
|
|
void* audio_handle;
|
|
pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS];
|
|
f32 master_volume;
|
|
f32* output_buffer;
|
|
pxl8_sfx_event_callback event_callback;
|
|
void* event_userdata;
|
|
};
|
|
|
|
static void envelope_trigger(envelope* env) {
|
|
env->state = ENV_ATTACK;
|
|
env->time = 0.0f;
|
|
env->level = 0.0f;
|
|
}
|
|
|
|
static void envelope_release(envelope* env) {
|
|
if (env->state != ENV_IDLE && env->state != ENV_RELEASE) {
|
|
env->release_level = env->level;
|
|
env->state = ENV_RELEASE;
|
|
env->time = 0.0f;
|
|
}
|
|
}
|
|
|
|
static f32 envelope_process(envelope* env) {
|
|
const f32 dt = 1.0f / PXL8_SFX_SAMPLE_RATE;
|
|
|
|
switch (env->state) {
|
|
case ENV_IDLE:
|
|
return 0.0f;
|
|
|
|
case ENV_ATTACK: {
|
|
if (env->adsr.attack <= 0.0f) {
|
|
env->level = 1.0f;
|
|
env->state = ENV_DECAY;
|
|
env->time = 0.0f;
|
|
} else {
|
|
f32 t = env->time / env->adsr.attack;
|
|
if (t >= 1.0f) {
|
|
env->level = 1.0f;
|
|
env->state = ENV_DECAY;
|
|
env->time = 0.0f;
|
|
} else {
|
|
env->level = t;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ENV_DECAY: {
|
|
if (env->adsr.decay <= 0.0f) {
|
|
env->level = env->adsr.sustain;
|
|
env->state = ENV_SUSTAIN;
|
|
} else {
|
|
f32 t = env->time / env->adsr.decay;
|
|
if (t >= 1.0f) {
|
|
env->level = env->adsr.sustain;
|
|
env->state = ENV_SUSTAIN;
|
|
} else {
|
|
env->level = 1.0f - t * (1.0f - env->adsr.sustain);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case ENV_SUSTAIN:
|
|
env->level = env->adsr.sustain;
|
|
if (env->adsr.sustain < 0.001f) {
|
|
env->state = ENV_IDLE;
|
|
}
|
|
break;
|
|
|
|
case ENV_RELEASE: {
|
|
if (env->adsr.release <= 0.0f) {
|
|
env->level = 0.0f;
|
|
env->state = ENV_IDLE;
|
|
} else {
|
|
f32 t = env->time / env->adsr.release;
|
|
if (t >= 1.0f) {
|
|
env->level = 0.0f;
|
|
env->state = ENV_IDLE;
|
|
} else {
|
|
env->level = env->release_level * (1.0f - t);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
env->time += dt;
|
|
return env->level;
|
|
}
|
|
|
|
static void oscillator_init(oscillator* osc, pxl8_sfx_waveform waveform, f32 frequency) {
|
|
osc->waveform = waveform;
|
|
osc->frequency = frequency;
|
|
osc->phase = 0.0f;
|
|
osc->pulse_width = 0.5f;
|
|
osc->noise_state = 0xDEADBEEF;
|
|
}
|
|
|
|
static f32 oscillator_process(oscillator* osc) {
|
|
f32 sample = 0.0f;
|
|
|
|
switch (osc->waveform) {
|
|
case PXL8_SFX_WAVE_SINE:
|
|
sample = sinf(osc->phase * PXL8_TAU);
|
|
break;
|
|
|
|
case PXL8_SFX_WAVE_SQUARE:
|
|
sample = osc->phase < 0.5f ? 1.0f : -1.0f;
|
|
break;
|
|
|
|
case PXL8_SFX_WAVE_SAW:
|
|
sample = 2.0f * osc->phase - 1.0f;
|
|
break;
|
|
|
|
case PXL8_SFX_WAVE_TRIANGLE:
|
|
if (osc->phase < 0.25f) {
|
|
sample = 4.0f * osc->phase;
|
|
} else if (osc->phase < 0.75f) {
|
|
sample = 2.0f - 4.0f * osc->phase;
|
|
} else {
|
|
sample = 4.0f * osc->phase - 4.0f;
|
|
}
|
|
break;
|
|
|
|
case PXL8_SFX_WAVE_PULSE:
|
|
sample = osc->phase < osc->pulse_width ? 1.0f : -1.0f;
|
|
break;
|
|
|
|
case PXL8_SFX_WAVE_NOISE:
|
|
osc->noise_state = osc->noise_state * 1103515245 + 12345;
|
|
sample = (f32)((osc->noise_state >> 16) & 0x7FFF) / 16384.0f - 1.0f;
|
|
break;
|
|
}
|
|
|
|
osc->phase += osc->frequency / PXL8_SFX_SAMPLE_RATE;
|
|
while (osc->phase >= 1.0f) osc->phase -= 1.0f;
|
|
|
|
return sample;
|
|
}
|
|
|
|
static void svf_init(svf* f, pxl8_sfx_filter_type type, f32 cutoff, f32 resonance) {
|
|
f->type = type;
|
|
f->cutoff = fmaxf(20.0f, fminf(14000.0f, cutoff));
|
|
f->resonance = fmaxf(0.0f, fminf(0.99f, resonance));
|
|
f->low = 0.0f;
|
|
f->band = 0.0f;
|
|
f->high = 0.0f;
|
|
}
|
|
|
|
static f32 svf_process(svf* f, f32 input) {
|
|
if (f->type == PXL8_SFX_FILTER_NONE) return input;
|
|
|
|
f32 freq = 2.0f * sinf(PXL8_PI * f->cutoff / PXL8_SFX_SAMPLE_RATE);
|
|
f32 q = 1.0f - f->resonance;
|
|
|
|
f->low += freq * f->band;
|
|
f->high = input - f->low - q * f->band;
|
|
f->band += freq * f->high;
|
|
|
|
switch (f->type) {
|
|
case PXL8_SFX_FILTER_LOWPASS: return f->low;
|
|
case PXL8_SFX_FILTER_HIGHPASS: return f->high;
|
|
case PXL8_SFX_FILTER_BANDPASS: return f->band;
|
|
default: return input;
|
|
}
|
|
}
|
|
|
|
static void lfo_init(lfo* l, pxl8_sfx_waveform waveform, f32 rate, f32 depth, pxl8_sfx_lfo_target target) {
|
|
l->waveform = waveform;
|
|
l->rate = rate;
|
|
l->depth = depth;
|
|
l->target = target;
|
|
l->phase = 0.0f;
|
|
}
|
|
|
|
static f32 lfo_process(lfo* l) {
|
|
f32 sample = 0.0f;
|
|
|
|
switch (l->waveform) {
|
|
case PXL8_SFX_WAVE_SINE:
|
|
sample = sinf(l->phase * PXL8_TAU);
|
|
break;
|
|
case PXL8_SFX_WAVE_SQUARE:
|
|
sample = l->phase < 0.5f ? 1.0f : -1.0f;
|
|
break;
|
|
case PXL8_SFX_WAVE_SAW:
|
|
sample = 2.0f * l->phase - 1.0f;
|
|
break;
|
|
case PXL8_SFX_WAVE_TRIANGLE:
|
|
if (l->phase < 0.25f) sample = 4.0f * l->phase;
|
|
else if (l->phase < 0.75f) sample = 2.0f - 4.0f * l->phase;
|
|
else sample = 4.0f * l->phase - 4.0f;
|
|
break;
|
|
default:
|
|
sample = sinf(l->phase * PXL8_TAU);
|
|
break;
|
|
}
|
|
|
|
l->phase += l->rate / PXL8_SFX_SAMPLE_RATE;
|
|
while (l->phase >= 1.0f) l->phase -= 1.0f;
|
|
|
|
return sample * l->depth;
|
|
}
|
|
|
|
static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params, f32 volume, u16 id, f32 time, f32 duration) {
|
|
v->active = true;
|
|
v->id = id;
|
|
v->note = note;
|
|
v->volume = volume;
|
|
v->pan = 0.0f;
|
|
v->base_frequency = pxl8_sfx_note_to_freq(note);
|
|
v->base_cutoff = params->filter_cutoff;
|
|
v->filter_env_depth = params->filter_env_depth;
|
|
v->fx_send = params->fx_send;
|
|
v->start_time = time;
|
|
v->duration = duration;
|
|
v->released = false;
|
|
|
|
oscillator_init(&v->osc, params->waveform, v->base_frequency);
|
|
v->osc.pulse_width = params->pulse_width;
|
|
|
|
v->amp_env.adsr = params->amp_env;
|
|
envelope_trigger(&v->amp_env);
|
|
|
|
v->filter_env.adsr = params->filter_env;
|
|
envelope_trigger(&v->filter_env);
|
|
|
|
svf_init(&v->filter, params->filter_type, params->filter_cutoff, params->filter_resonance);
|
|
|
|
if (params->lfo_depth > 0.0f && params->lfo_rate > 0.0f) {
|
|
lfo_init(&v->mod_lfo, params->lfo_waveform, params->lfo_rate, params->lfo_depth, params->lfo_target);
|
|
v->has_lfo = true;
|
|
} else {
|
|
v->has_lfo = false;
|
|
}
|
|
}
|
|
|
|
static void voice_release(voice* v) {
|
|
envelope_release(&v->amp_env);
|
|
envelope_release(&v->filter_env);
|
|
}
|
|
|
|
static void voice_process(voice* v, f32* dry_l, f32* dry_r, f32* wet_l, f32* wet_r) {
|
|
if (!v->active) {
|
|
*dry_l = 0.0f;
|
|
*dry_r = 0.0f;
|
|
*wet_l = 0.0f;
|
|
*wet_r = 0.0f;
|
|
return;
|
|
}
|
|
|
|
f32 lfo_value = v->has_lfo ? lfo_process(&v->mod_lfo) : 0.0f;
|
|
f32 freq = v->base_frequency;
|
|
f32 cutoff = v->base_cutoff;
|
|
f32 amp_mod = 1.0f;
|
|
|
|
if (v->has_lfo) {
|
|
switch (v->mod_lfo.target) {
|
|
case PXL8_SFX_LFO_PITCH:
|
|
freq *= (1.0f + lfo_value);
|
|
break;
|
|
case PXL8_SFX_LFO_FILTER:
|
|
cutoff += lfo_value * 1000.0f;
|
|
break;
|
|
case PXL8_SFX_LFO_AMPLITUDE:
|
|
amp_mod = 1.0f + lfo_value;
|
|
break;
|
|
}
|
|
}
|
|
|
|
f32 filter_env_value = envelope_process(&v->filter_env);
|
|
cutoff += filter_env_value * v->filter_env_depth;
|
|
v->filter.cutoff = fmaxf(20.0f, fminf(14000.0f, cutoff));
|
|
|
|
v->osc.frequency = freq;
|
|
f32 sample = oscillator_process(&v->osc);
|
|
sample = svf_process(&v->filter, sample);
|
|
|
|
f32 amp_env_value = envelope_process(&v->amp_env);
|
|
sample *= amp_env_value * v->volume * amp_mod * VOICE_SCALE;
|
|
|
|
if (v->amp_env.state == ENV_IDLE) {
|
|
v->active = false;
|
|
}
|
|
|
|
f32 left_gain = v->pan < 0.0f ? 1.0f : 1.0f - v->pan;
|
|
f32 right_gain = v->pan > 0.0f ? 1.0f : 1.0f + v->pan;
|
|
|
|
f32 left = sample * left_gain;
|
|
f32 right = sample * right_gain;
|
|
|
|
f32 dry_amt = 1.0f - v->fx_send;
|
|
f32 wet_amt = v->fx_send;
|
|
|
|
*dry_l = left * dry_amt;
|
|
*dry_r = right * dry_amt;
|
|
*wet_l = left * wet_amt;
|
|
*wet_r = right * wet_amt;
|
|
}
|
|
|
|
static void delay_process_stereo(void* state, f32* left, f32* right) {
|
|
delay_state* d = (delay_state*)state;
|
|
if (!d->buffer_l || !d->buffer_r) return;
|
|
|
|
u32 read_pos_l = (d->write_pos >= d->delay_samples_l)
|
|
? d->write_pos - d->delay_samples_l
|
|
: PXL8_SFX_MAX_DELAY_SAMPLES - (d->delay_samples_l - d->write_pos);
|
|
u32 read_pos_r = (d->write_pos >= d->delay_samples_r)
|
|
? d->write_pos - d->delay_samples_r
|
|
: PXL8_SFX_MAX_DELAY_SAMPLES - (d->delay_samples_r - d->write_pos);
|
|
|
|
f32 delayed_l = d->buffer_l[read_pos_l];
|
|
f32 delayed_r = d->buffer_r[read_pos_r];
|
|
|
|
d->buffer_l[d->write_pos] = *left + delayed_l * d->feedback;
|
|
d->buffer_r[d->write_pos] = *right + delayed_r * d->feedback;
|
|
d->write_pos = (d->write_pos + 1) % PXL8_SFX_MAX_DELAY_SAMPLES;
|
|
|
|
*left = *left * (1.0f - d->mix) + delayed_l * d->mix;
|
|
*right = *right * (1.0f - d->mix) + delayed_r * d->mix;
|
|
}
|
|
|
|
static void delay_destroy_state(void* state) {
|
|
delay_state* d = (delay_state*)state;
|
|
if (d) {
|
|
free(d->buffer_l);
|
|
free(d->buffer_r);
|
|
free(d);
|
|
}
|
|
}
|
|
|
|
static void comb_init(comb_filter* c, u32 size, f32 feedback, f32 damping) {
|
|
c->buffer = (f32*)calloc(size, sizeof(f32));
|
|
c->size = size;
|
|
c->feedback = feedback;
|
|
c->damping = damping;
|
|
c->filter_state = 0.0f;
|
|
c->index = 0;
|
|
}
|
|
|
|
static void comb_destroy(comb_filter* c) {
|
|
free(c->buffer);
|
|
}
|
|
|
|
static f32 comb_process(comb_filter* c, f32 input) {
|
|
if (!c->buffer) return input;
|
|
|
|
f32 output = c->buffer[c->index];
|
|
c->filter_state = output * (1.0f - c->damping) + c->filter_state * c->damping;
|
|
c->buffer[c->index] = input + c->filter_state * c->feedback;
|
|
c->index = (c->index + 1) % c->size;
|
|
|
|
return output;
|
|
}
|
|
|
|
static void allpass_init(allpass_filter* a, u32 size) {
|
|
a->buffer = (f32*)calloc(size, sizeof(f32));
|
|
a->size = size;
|
|
a->feedback = 0.5f;
|
|
a->index = 0;
|
|
}
|
|
|
|
static void allpass_destroy(allpass_filter* a) {
|
|
free(a->buffer);
|
|
}
|
|
|
|
static f32 allpass_process(allpass_filter* a, f32 input) {
|
|
if (!a->buffer) return input;
|
|
|
|
f32 delayed = a->buffer[a->index];
|
|
f32 output = -input + delayed;
|
|
a->buffer[a->index] = input + delayed * a->feedback;
|
|
a->index = (a->index + 1) % a->size;
|
|
|
|
return output;
|
|
}
|
|
|
|
static void reverb_rebuild(reverb_state* r) {
|
|
u32 comb_sizes[] = {1116, 1188, 1277, 1356};
|
|
u32 allpass_sizes[] = {556, 441};
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
comb_destroy(&r->combs[i]);
|
|
u32 size = (u32)(comb_sizes[i] * r->room);
|
|
comb_init(&r->combs[i], size, 0.84f, r->damping);
|
|
}
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
allpass_destroy(&r->allpasses[i]);
|
|
u32 size = (u32)(allpass_sizes[i] * r->room);
|
|
allpass_init(&r->allpasses[i], size);
|
|
}
|
|
}
|
|
|
|
static void reverb_process_stereo(void* state, f32* left, f32* right) {
|
|
reverb_state* r = (reverb_state*)state;
|
|
|
|
f32 input = (*left + *right) * 0.5f;
|
|
f32 comb_sum = 0.0f;
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
comb_sum += comb_process(&r->combs[i], input);
|
|
}
|
|
comb_sum *= 0.25f;
|
|
|
|
f32 output = comb_sum;
|
|
for (int i = 0; i < 2; i++) {
|
|
output = allpass_process(&r->allpasses[i], output);
|
|
}
|
|
|
|
*left = *left * (1.0f - r->mix) + output * r->mix;
|
|
*right = *right * (1.0f - r->mix) + output * r->mix;
|
|
}
|
|
|
|
static void reverb_destroy_state(void* state) {
|
|
reverb_state* r = (reverb_state*)state;
|
|
if (r) {
|
|
for (int i = 0; i < 4; i++) comb_destroy(&r->combs[i]);
|
|
for (int i = 0; i < 2; i++) allpass_destroy(&r->allpasses[i]);
|
|
free(r);
|
|
}
|
|
}
|
|
|
|
static void compressor_recalc(compressor_state* c, f32 threshold_db, f32 ratio, f32 attack_ms, f32 release_ms) {
|
|
c->threshold = powf(10.0f, threshold_db / 20.0f);
|
|
c->ratio = ratio;
|
|
c->attack_coeff = expf(-1.0f / (attack_ms * 0.001f * PXL8_SFX_SAMPLE_RATE));
|
|
c->release_coeff = expf(-1.0f / (release_ms * 0.001f * PXL8_SFX_SAMPLE_RATE));
|
|
f32 makeup_db = (fabsf(threshold_db) / ratio) * 0.5f;
|
|
c->makeup_gain = powf(10.0f, makeup_db / 20.0f);
|
|
}
|
|
|
|
static void compressor_process_stereo(void* state, f32* left, f32* right) {
|
|
compressor_state* c = (compressor_state*)state;
|
|
|
|
f32 peak = fmaxf(fabsf(*left), fabsf(*right));
|
|
f32 coeff = (peak > c->envelope) ? c->attack_coeff : c->release_coeff;
|
|
c->envelope = coeff * c->envelope + (1.0f - coeff) * peak;
|
|
|
|
if (c->envelope < 0.0001f) return;
|
|
|
|
f32 input_db = 20.0f * log10f(c->envelope);
|
|
f32 threshold_db = 20.0f * log10f(c->threshold);
|
|
f32 knee_min = threshold_db - c->knee_width * 0.5f;
|
|
f32 knee_max = threshold_db + c->knee_width * 0.5f;
|
|
|
|
f32 output_db;
|
|
if (input_db < knee_min) {
|
|
output_db = input_db;
|
|
} else if (input_db > knee_max) {
|
|
output_db = threshold_db + (input_db - threshold_db) / c->ratio;
|
|
} else {
|
|
f32 t = (input_db - knee_min) / c->knee_width;
|
|
f32 soft_ratio = 1.0f + (c->ratio - 1.0f) * t * t;
|
|
output_db = knee_min + (input_db - knee_min) / soft_ratio;
|
|
}
|
|
|
|
f32 gain_db = output_db - input_db;
|
|
f32 gain = powf(10.0f, gain_db / 20.0f) * c->makeup_gain;
|
|
|
|
*left *= gain;
|
|
*right *= gain;
|
|
}
|
|
|
|
static void compressor_destroy_state(void* state) {
|
|
free(state);
|
|
}
|
|
|
|
static void context_process_sample(pxl8_sfx_context* ctx, f32* out_left, f32* out_right) {
|
|
const f32 dt = 1.0f / PXL8_SFX_SAMPLE_RATE;
|
|
|
|
f32 dry_left = 0.0f, dry_right = 0.0f;
|
|
f32 wet_left = 0.0f, wet_right = 0.0f;
|
|
|
|
for (int v = 0; v < PXL8_SFX_MAX_VOICES; v++) {
|
|
voice* vp = &ctx->voices[v];
|
|
if (!vp->active) continue;
|
|
|
|
if (vp->duration > 0.0f && !vp->released &&
|
|
ctx->current_time - vp->start_time >= vp->duration) {
|
|
voice_release(vp);
|
|
vp->released = true;
|
|
}
|
|
|
|
f32 dl, dr, wl, wr;
|
|
voice_process(vp, &dl, &dr, &wl, &wr);
|
|
|
|
dry_left += dl;
|
|
dry_right += dr;
|
|
wet_left += wl;
|
|
wet_right += wr;
|
|
}
|
|
|
|
pxl8_sfx_node* node = ctx->fx_head;
|
|
while (node) {
|
|
if (node->process && node->state) {
|
|
node->process(node->state, &wet_left, &wet_right);
|
|
}
|
|
node = node->next;
|
|
}
|
|
|
|
f32 left = (dry_left + wet_left) * ctx->volume;
|
|
f32 right = (dry_right + wet_right) * ctx->volume;
|
|
|
|
ctx->current_time += dt;
|
|
|
|
*out_left = left;
|
|
*out_right = right;
|
|
}
|
|
|
|
pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal) {
|
|
if (!hal || !hal->audio_create) return NULL;
|
|
|
|
pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)calloc(1, sizeof(pxl8_sfx_mixer));
|
|
if (!mixer) return NULL;
|
|
|
|
mixer->output_buffer = (f32*)calloc(PXL8_SFX_BUFFER_SIZE * 2, sizeof(f32));
|
|
if (!mixer->output_buffer) {
|
|
free(mixer);
|
|
return NULL;
|
|
}
|
|
|
|
mixer->hal = hal;
|
|
mixer->master_volume = 0.8f;
|
|
|
|
mixer->audio_handle = hal->audio_create(PXL8_SFX_SAMPLE_RATE, 2);
|
|
if (!mixer->audio_handle) {
|
|
free(mixer->output_buffer);
|
|
free(mixer);
|
|
return NULL;
|
|
}
|
|
|
|
hal->audio_start(mixer->audio_handle);
|
|
pxl8_info("Audio mixer initialized: %d Hz, stereo, %d context slots", PXL8_SFX_SAMPLE_RATE, PXL8_SFX_MAX_CONTEXTS);
|
|
|
|
return mixer;
|
|
}
|
|
|
|
void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer) {
|
|
if (!mixer) return;
|
|
|
|
if (mixer->hal && mixer->audio_handle) {
|
|
mixer->hal->audio_destroy(mixer->audio_handle);
|
|
}
|
|
|
|
free(mixer->output_buffer);
|
|
free(mixer);
|
|
}
|
|
|
|
void pxl8_sfx_mixer_process(pxl8_sfx_mixer* mixer) {
|
|
if (!mixer || !mixer->hal || !mixer->audio_handle || !mixer->output_buffer) return;
|
|
|
|
i32 queued = mixer->hal->audio_queued(mixer->audio_handle);
|
|
i32 target = PXL8_SFX_SAMPLE_RATE / 10;
|
|
|
|
while (queued < target) {
|
|
i32 samples_to_generate = PXL8_SFX_BUFFER_SIZE;
|
|
if (samples_to_generate > target - queued) {
|
|
samples_to_generate = target - queued;
|
|
}
|
|
|
|
for (i32 i = 0; i < samples_to_generate; i++) {
|
|
f32 left = 0.0f, right = 0.0f;
|
|
|
|
for (int c = 0; c < PXL8_SFX_MAX_CONTEXTS; c++) {
|
|
pxl8_sfx_context* ctx = mixer->contexts[c];
|
|
if (!ctx) continue;
|
|
|
|
f32 ctx_left, ctx_right;
|
|
context_process_sample(ctx, &ctx_left, &ctx_right);
|
|
left += ctx_left;
|
|
right += ctx_right;
|
|
}
|
|
|
|
left *= mixer->master_volume;
|
|
right *= mixer->master_volume;
|
|
|
|
left = fmaxf(-1.0f, fminf(1.0f, left));
|
|
right = fmaxf(-1.0f, fminf(1.0f, right));
|
|
|
|
left = sanitize_audio(left);
|
|
right = sanitize_audio(right);
|
|
|
|
mixer->output_buffer[i * 2] = left;
|
|
mixer->output_buffer[i * 2 + 1] = right;
|
|
}
|
|
|
|
mixer->hal->upload_audio(mixer->audio_handle, mixer->output_buffer, samples_to_generate);
|
|
queued += samples_to_generate;
|
|
}
|
|
}
|
|
|
|
void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) {
|
|
if (!mixer || !ctx) return;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) {
|
|
if (mixer->contexts[i] == ctx) return;
|
|
}
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) {
|
|
if (!mixer->contexts[i]) {
|
|
mixer->contexts[i] = ctx;
|
|
ctx->mixer = mixer;
|
|
ctx->context_id = (u8)i;
|
|
return;
|
|
}
|
|
}
|
|
|
|
pxl8_warn("No free context slots in mixer");
|
|
}
|
|
|
|
void pxl8_sfx_mixer_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx) {
|
|
if (!mixer || !ctx) return;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) {
|
|
if (mixer->contexts[i] == ctx) {
|
|
mixer->contexts[i] = NULL;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void pxl8_sfx_mixer_clear(pxl8_sfx_mixer* mixer) {
|
|
if (!mixer) return;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_CONTEXTS; i++) {
|
|
if (mixer->contexts[i]) {
|
|
pxl8_sfx_context_destroy(mixer->contexts[i]);
|
|
mixer->contexts[i] = NULL;
|
|
}
|
|
}
|
|
}
|
|
|
|
void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume) {
|
|
if (mixer) mixer->master_volume = fmaxf(0.0f, fminf(1.0f, volume));
|
|
}
|
|
|
|
void pxl8_sfx_mixer_set_event_callback(pxl8_sfx_mixer* mixer, pxl8_sfx_event_callback cb, void* userdata) {
|
|
if (mixer) {
|
|
mixer->event_callback = cb;
|
|
mixer->event_userdata = userdata;
|
|
}
|
|
}
|
|
|
|
f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer) {
|
|
return mixer ? mixer->master_volume : 0.0f;
|
|
}
|
|
|
|
pxl8_sfx_context* pxl8_sfx_context_create(void) {
|
|
pxl8_sfx_context* ctx = (pxl8_sfx_context*)calloc(1, sizeof(pxl8_sfx_context));
|
|
if (!ctx) return NULL;
|
|
|
|
ctx->volume = 1.0f;
|
|
ctx->next_voice_id = 1;
|
|
|
|
return ctx;
|
|
}
|
|
|
|
void pxl8_sfx_context_destroy(pxl8_sfx_context* ctx) {
|
|
if (!ctx) return;
|
|
|
|
pxl8_sfx_node* node = ctx->fx_head;
|
|
while (node) {
|
|
pxl8_sfx_node* next = node->next;
|
|
pxl8_sfx_node_destroy(node);
|
|
node = next;
|
|
}
|
|
|
|
free(ctx);
|
|
}
|
|
|
|
void pxl8_sfx_context_set_volume(pxl8_sfx_context* ctx, f32 volume) {
|
|
if (ctx) ctx->volume = fmaxf(0.0f, fminf(1.0f, volume));
|
|
}
|
|
|
|
f32 pxl8_sfx_context_get_volume(const pxl8_sfx_context* ctx) {
|
|
return ctx ? ctx->volume : 0.0f;
|
|
}
|
|
|
|
void pxl8_sfx_context_append_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node) {
|
|
if (!ctx || !node) return;
|
|
|
|
node->next = NULL;
|
|
|
|
if (!ctx->fx_head) {
|
|
ctx->fx_head = node;
|
|
return;
|
|
}
|
|
|
|
pxl8_sfx_node* tail = ctx->fx_head;
|
|
while (tail->next) tail = tail->next;
|
|
tail->next = node;
|
|
}
|
|
|
|
void pxl8_sfx_context_insert_node(pxl8_sfx_context* ctx, pxl8_sfx_node* after, pxl8_sfx_node* node) {
|
|
if (!ctx || !node) return;
|
|
|
|
if (!after) {
|
|
node->next = ctx->fx_head;
|
|
ctx->fx_head = node;
|
|
return;
|
|
}
|
|
|
|
node->next = after->next;
|
|
after->next = node;
|
|
}
|
|
|
|
void pxl8_sfx_context_remove_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node) {
|
|
if (!ctx || !node || !ctx->fx_head) return;
|
|
|
|
if (ctx->fx_head == node) {
|
|
ctx->fx_head = node->next;
|
|
node->next = NULL;
|
|
return;
|
|
}
|
|
|
|
pxl8_sfx_node* prev = ctx->fx_head;
|
|
while (prev->next && prev->next != node) prev = prev->next;
|
|
|
|
if (prev->next == node) {
|
|
prev->next = node->next;
|
|
node->next = NULL;
|
|
}
|
|
}
|
|
|
|
pxl8_sfx_node* pxl8_sfx_context_get_head(pxl8_sfx_context* ctx) {
|
|
return ctx ? ctx->fx_head : NULL;
|
|
}
|
|
|
|
void pxl8_sfx_node_destroy(pxl8_sfx_node* node) {
|
|
if (!node) return;
|
|
|
|
if (node->destroy && node->state) {
|
|
node->destroy(node->state);
|
|
}
|
|
free(node);
|
|
}
|
|
|
|
pxl8_sfx_node* pxl8_sfx_delay_create(pxl8_sfx_delay_config cfg) {
|
|
pxl8_sfx_node* node = (pxl8_sfx_node*)calloc(1, sizeof(pxl8_sfx_node));
|
|
if (!node) return NULL;
|
|
|
|
delay_state* d = (delay_state*)calloc(1, sizeof(delay_state));
|
|
if (!d) {
|
|
free(node);
|
|
return NULL;
|
|
}
|
|
|
|
d->buffer_l = (f32*)calloc(PXL8_SFX_MAX_DELAY_SAMPLES, sizeof(f32));
|
|
d->buffer_r = (f32*)calloc(PXL8_SFX_MAX_DELAY_SAMPLES, sizeof(f32));
|
|
if (!d->buffer_l || !d->buffer_r) {
|
|
free(d->buffer_l);
|
|
free(d->buffer_r);
|
|
free(d);
|
|
free(node);
|
|
return NULL;
|
|
}
|
|
|
|
d->delay_samples_l = (cfg.time_l * PXL8_SFX_SAMPLE_RATE) / 1000;
|
|
d->delay_samples_r = (cfg.time_r * PXL8_SFX_SAMPLE_RATE) / 1000;
|
|
if (d->delay_samples_l >= PXL8_SFX_MAX_DELAY_SAMPLES) d->delay_samples_l = PXL8_SFX_MAX_DELAY_SAMPLES - 1;
|
|
if (d->delay_samples_r >= PXL8_SFX_MAX_DELAY_SAMPLES) d->delay_samples_r = PXL8_SFX_MAX_DELAY_SAMPLES - 1;
|
|
d->feedback = fminf(0.95f, cfg.feedback);
|
|
d->mix = fminf(1.0f, cfg.mix);
|
|
d->write_pos = 0;
|
|
|
|
node->type = PXL8_SFX_NODE_DELAY;
|
|
node->state = d;
|
|
node->process = delay_process_stereo;
|
|
node->destroy = delay_destroy_state;
|
|
node->next = NULL;
|
|
|
|
return node;
|
|
}
|
|
|
|
void pxl8_sfx_delay_set_time(pxl8_sfx_node* node, u32 time_l, u32 time_r) {
|
|
if (!node || node->type != PXL8_SFX_NODE_DELAY) return;
|
|
delay_state* d = (delay_state*)node->state;
|
|
d->delay_samples_l = (time_l * PXL8_SFX_SAMPLE_RATE) / 1000;
|
|
d->delay_samples_r = (time_r * PXL8_SFX_SAMPLE_RATE) / 1000;
|
|
if (d->delay_samples_l >= PXL8_SFX_MAX_DELAY_SAMPLES) d->delay_samples_l = PXL8_SFX_MAX_DELAY_SAMPLES - 1;
|
|
if (d->delay_samples_r >= PXL8_SFX_MAX_DELAY_SAMPLES) d->delay_samples_r = PXL8_SFX_MAX_DELAY_SAMPLES - 1;
|
|
}
|
|
|
|
void pxl8_sfx_delay_set_feedback(pxl8_sfx_node* node, f32 feedback) {
|
|
if (!node || node->type != PXL8_SFX_NODE_DELAY) return;
|
|
delay_state* d = (delay_state*)node->state;
|
|
d->feedback = fminf(0.95f, feedback);
|
|
}
|
|
|
|
void pxl8_sfx_delay_set_mix(pxl8_sfx_node* node, f32 mix) {
|
|
if (!node || node->type != PXL8_SFX_NODE_DELAY) return;
|
|
delay_state* d = (delay_state*)node->state;
|
|
d->mix = fminf(1.0f, mix);
|
|
}
|
|
|
|
pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg) {
|
|
pxl8_sfx_node* node = (pxl8_sfx_node*)calloc(1, sizeof(pxl8_sfx_node));
|
|
if (!node) return NULL;
|
|
|
|
reverb_state* r = (reverb_state*)calloc(1, sizeof(reverb_state));
|
|
if (!r) {
|
|
free(node);
|
|
return NULL;
|
|
}
|
|
|
|
r->room = cfg.room;
|
|
r->damping = cfg.damping;
|
|
r->mix = cfg.mix;
|
|
|
|
u32 comb_sizes[] = {1116, 1188, 1277, 1356};
|
|
u32 allpass_sizes[] = {556, 441};
|
|
|
|
for (int i = 0; i < 4; i++) {
|
|
u32 size = (u32)(comb_sizes[i] * r->room);
|
|
comb_init(&r->combs[i], size, 0.84f, r->damping);
|
|
}
|
|
|
|
for (int i = 0; i < 2; i++) {
|
|
u32 size = (u32)(allpass_sizes[i] * r->room);
|
|
allpass_init(&r->allpasses[i], size);
|
|
}
|
|
|
|
node->type = PXL8_SFX_NODE_REVERB;
|
|
node->state = r;
|
|
node->process = reverb_process_stereo;
|
|
node->destroy = reverb_destroy_state;
|
|
node->next = NULL;
|
|
|
|
return node;
|
|
}
|
|
|
|
void pxl8_sfx_reverb_set_room(pxl8_sfx_node* node, f32 room) {
|
|
if (!node || node->type != PXL8_SFX_NODE_REVERB) return;
|
|
reverb_state* r = (reverb_state*)node->state;
|
|
r->room = room;
|
|
reverb_rebuild(r);
|
|
}
|
|
|
|
void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping) {
|
|
if (!node || node->type != PXL8_SFX_NODE_REVERB) return;
|
|
reverb_state* r = (reverb_state*)node->state;
|
|
r->damping = damping;
|
|
for (int i = 0; i < 4; i++) {
|
|
r->combs[i].damping = damping;
|
|
}
|
|
}
|
|
|
|
void pxl8_sfx_reverb_set_mix(pxl8_sfx_node* node, f32 mix) {
|
|
if (!node || node->type != PXL8_SFX_NODE_REVERB) return;
|
|
reverb_state* r = (reverb_state*)node->state;
|
|
r->mix = mix;
|
|
}
|
|
|
|
pxl8_sfx_node* pxl8_sfx_compressor_create(pxl8_sfx_compressor_config cfg) {
|
|
pxl8_sfx_node* node = (pxl8_sfx_node*)calloc(1, sizeof(pxl8_sfx_node));
|
|
if (!node) return NULL;
|
|
|
|
compressor_state* c = (compressor_state*)calloc(1, sizeof(compressor_state));
|
|
if (!c) {
|
|
free(node);
|
|
return NULL;
|
|
}
|
|
|
|
c->envelope = 0.0f;
|
|
c->knee_width = 6.0f;
|
|
compressor_recalc(c, cfg.threshold, cfg.ratio, cfg.attack, cfg.release);
|
|
|
|
node->type = PXL8_SFX_NODE_COMPRESSOR;
|
|
node->state = c;
|
|
node->process = compressor_process_stereo;
|
|
node->destroy = compressor_destroy_state;
|
|
node->next = NULL;
|
|
|
|
return node;
|
|
}
|
|
|
|
void pxl8_sfx_compressor_set_threshold(pxl8_sfx_node* node, f32 threshold) {
|
|
if (!node || node->type != PXL8_SFX_NODE_COMPRESSOR) return;
|
|
compressor_state* c = (compressor_state*)node->state;
|
|
f32 attack_ms = -1.0f / (logf(c->attack_coeff) * PXL8_SFX_SAMPLE_RATE * 0.001f);
|
|
f32 release_ms = -1.0f / (logf(c->release_coeff) * PXL8_SFX_SAMPLE_RATE * 0.001f);
|
|
compressor_recalc(c, threshold, c->ratio, attack_ms, release_ms);
|
|
}
|
|
|
|
void pxl8_sfx_compressor_set_ratio(pxl8_sfx_node* node, f32 ratio) {
|
|
if (!node || node->type != PXL8_SFX_NODE_COMPRESSOR) return;
|
|
compressor_state* c = (compressor_state*)node->state;
|
|
f32 threshold_db = 20.0f * log10f(c->threshold);
|
|
f32 attack_ms = -1.0f / (logf(c->attack_coeff) * PXL8_SFX_SAMPLE_RATE * 0.001f);
|
|
f32 release_ms = -1.0f / (logf(c->release_coeff) * PXL8_SFX_SAMPLE_RATE * 0.001f);
|
|
compressor_recalc(c, threshold_db, ratio, attack_ms, release_ms);
|
|
}
|
|
|
|
void pxl8_sfx_compressor_set_attack(pxl8_sfx_node* node, f32 attack) {
|
|
if (!node || node->type != PXL8_SFX_NODE_COMPRESSOR) return;
|
|
compressor_state* c = (compressor_state*)node->state;
|
|
c->attack_coeff = expf(-1.0f / (attack * 0.001f * PXL8_SFX_SAMPLE_RATE));
|
|
}
|
|
|
|
void pxl8_sfx_compressor_set_release(pxl8_sfx_node* node, f32 release) {
|
|
if (!node || node->type != PXL8_SFX_NODE_COMPRESSOR) return;
|
|
compressor_state* c = (compressor_state*)node->state;
|
|
c->release_coeff = expf(-1.0f / (release * 0.001f * PXL8_SFX_SAMPLE_RATE));
|
|
}
|
|
|
|
f32 pxl8_sfx_note_to_freq(u8 note) {
|
|
return 440.0f * powf(2.0f, (note - 69) / 12.0f);
|
|
}
|
|
|
|
static i32 find_free_voice(pxl8_sfx_context* ctx) {
|
|
for (int i = 0; i < PXL8_SFX_MAX_VOICES; i++) {
|
|
if (!ctx->voices[i].active) return i;
|
|
}
|
|
|
|
i32 oldest_releasing = -1;
|
|
f32 oldest_releasing_time = ctx->current_time;
|
|
i32 oldest_active = -1;
|
|
f32 oldest_active_time = ctx->current_time;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_VOICES; i++) {
|
|
if (ctx->voices[i].amp_env.state == ENV_RELEASE) {
|
|
if (ctx->voices[i].start_time < oldest_releasing_time) {
|
|
oldest_releasing_time = ctx->voices[i].start_time;
|
|
oldest_releasing = i;
|
|
}
|
|
} else if (ctx->voices[i].start_time < oldest_active_time) {
|
|
oldest_active_time = ctx->voices[i].start_time;
|
|
oldest_active = i;
|
|
}
|
|
}
|
|
|
|
if (oldest_releasing >= 0) return oldest_releasing;
|
|
return oldest_active;
|
|
}
|
|
|
|
u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration) {
|
|
if (!ctx || !params) return 0;
|
|
|
|
i32 slot = find_free_voice(ctx);
|
|
if (slot < 0) return 0;
|
|
|
|
u16 id = ctx->next_voice_id++;
|
|
if (ctx->next_voice_id == 0) ctx->next_voice_id = 1;
|
|
|
|
voice_trigger(&ctx->voices[slot], note, params, volume, id, ctx->current_time, duration);
|
|
|
|
if (ctx->mixer && ctx->mixer->event_callback) {
|
|
ctx->mixer->event_callback(PXL8_SFX_EVENT_NOTE_ON, ctx->context_id, note, volume, ctx->mixer->event_userdata);
|
|
}
|
|
|
|
return id;
|
|
}
|
|
|
|
void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id) {
|
|
if (!ctx || voice_id == 0) return;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_VOICES; i++) {
|
|
if (ctx->voices[i].active && ctx->voices[i].id == voice_id) {
|
|
voice_release(&ctx->voices[i]);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id) {
|
|
if (!ctx || voice_id == 0) return;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_VOICES; i++) {
|
|
if (ctx->voices[i].active && ctx->voices[i].id == voice_id) {
|
|
ctx->voices[i].active = false;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void pxl8_sfx_stop_all(pxl8_sfx_context* ctx) {
|
|
if (!ctx) return;
|
|
|
|
for (int i = 0; i < PXL8_SFX_MAX_VOICES; i++) {
|
|
ctx->voices[i].active = false;
|
|
}
|
|
}
|