#include "pxl8_sfx.h" #include #include #include "pxl8_hal.h" #include "pxl8_log.h" #include "pxl8_math.h" #define VOICE_SCALE 0.15f 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)); if (!isfinite(left)) left = 0.0f; if (!isfinite(right)) right = 0.0f; 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; } }