pxl8/src/sfx/pxl8_sfx.c

1127 lines
31 KiB
C
Raw Normal View History

2026-01-07 17:45:46 -06:00
#include "pxl8_sfx.h"
#include <stdlib.h>
#include <string.h>
2026-01-08 01:19:25 -06:00
#include "pxl8_hal.h"
2026-01-07 17:45:46 -06:00
#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;
}
2026-01-07 17:45:46 -06:00
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;
2026-01-08 01:19:25 -06:00
f32 duration;
bool released;
2026-01-07 17:45:46 -06:00
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;
2026-01-08 01:19:25 -06:00
pxl8_sfx_mixer* mixer;
2026-01-07 17:45:46 -06:00
f32 volume;
f32 current_time;
u16 next_voice_id;
2026-01-08 01:19:25 -06:00
u8 context_id;
2026-01-07 17:45:46 -06:00
};
struct pxl8_sfx_mixer {
2026-01-08 01:19:25 -06:00
const pxl8_hal* hal;
void* audio_handle;
2026-01-07 17:45:46 -06:00
pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS];
f32 master_volume;
f32* output_buffer;
2026-01-08 01:19:25 -06:00
pxl8_sfx_event_callback event_callback;
void* event_userdata;
2026-01-07 17:45:46 -06:00
};
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;
}
2026-01-08 01:19:25 -06:00
static void voice_trigger(voice* v, u8 note, const pxl8_sfx_voice_params* params, f32 volume, u16 id, f32 time, f32 duration) {
2026-01-07 17:45:46 -06:00
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;
2026-01-08 01:19:25 -06:00
v->duration = duration;
v->released = false;
2026-01-07 17:45:46 -06:00
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++) {
2026-01-08 01:19:25 -06:00
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;
}
2026-01-07 17:45:46 -06:00
f32 dl, dr, wl, wr;
2026-01-08 01:19:25 -06:00
voice_process(vp, &dl, &dr, &wl, &wr);
2026-01-07 17:45:46 -06:00
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;
}
2026-01-08 01:19:25 -06:00
pxl8_sfx_mixer* pxl8_sfx_mixer_create(const pxl8_hal* hal) {
if (!hal || !hal->audio_create) return NULL;
2026-01-07 17:45:46 -06:00
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;
}
2026-01-08 01:19:25 -06:00
mixer->hal = hal;
2026-01-07 17:45:46 -06:00
mixer->master_volume = 0.8f;
2026-01-08 01:19:25 -06:00
mixer->audio_handle = hal->audio_create(PXL8_SFX_SAMPLE_RATE, 2);
if (!mixer->audio_handle) {
2026-01-07 17:45:46 -06:00
free(mixer->output_buffer);
free(mixer);
return NULL;
}
2026-01-08 01:19:25 -06:00
hal->audio_start(mixer->audio_handle);
2026-01-07 17:45:46 -06:00
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;
2026-01-08 01:19:25 -06:00
if (mixer->hal && mixer->audio_handle) {
mixer->hal->audio_destroy(mixer->audio_handle);
2026-01-07 17:45:46 -06:00
}
free(mixer->output_buffer);
free(mixer);
}
2026-01-08 01:19:25 -06:00
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);
2026-01-08 01:19:25 -06:00
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;
}
}
2026-01-07 17:45:46 -06:00
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;
2026-01-08 01:19:25 -06:00
ctx->mixer = mixer;
ctx->context_id = (u8)i;
2026-01-07 17:45:46 -06:00
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));
}
2026-01-08 01:19:25 -06:00
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;
}
}
2026-01-07 17:45:46 -06:00
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;
}
2026-01-08 01:19:25 -06:00
u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume, f32 duration) {
2026-01-07 17:45:46 -06:00
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;
2026-01-08 01:19:25 -06:00
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);
}
2026-01-07 17:45:46 -06:00
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;
}
}