diff --git a/demo/main.fnl b/demo/main.fnl index 0eda6af..7c8d6cd 100644 --- a/demo/main.fnl +++ b/demo/main.fnl @@ -1,5 +1,6 @@ (local pxl8 (require :pxl8)) (local menu (require :mod.menu)) +(local music (require :mod.music)) (local worldgen (require :mod.worldgen)) (var time 0) @@ -29,6 +30,8 @@ (pxl8.load_palette "res/sprites/pxl8_logo.ase") (set logo-sprite (pxl8.load_sprite "res/sprites/pxl8_logo.ase")) (set particles (pxl8.particles_new 1000)) + (music.init) + (music.start) (worldgen.init))) (global update (fn [dt] @@ -79,6 +82,8 @@ (set logo-dy (- logo-dy)))) :worldgen (worldgen.update dt)) + (music.update dt) + (when particles (pxl8.particles_update particles dt))) diff --git a/demo/mod/music.fnl b/demo/mod/music.fnl new file mode 100644 index 0000000..2cb1e1e --- /dev/null +++ b/demo/mod/music.fnl @@ -0,0 +1,68 @@ +(local pxl8 (require :pxl8)) + +(var time 0) +(var step 0) +(var ctx nil) +(var params nil) +(var bass-params nil) +(var playing false) + +(local melody [60 64 67 72 67 64 60 64 67 72 76 72 67 64 62 66 69 74 69 66 62 66 69 74 78 74 69 66]) +(local bass [36 50 40 36 38 38 50 38]) +(local step-duration 0.15) + +(fn init [] + (set ctx (pxl8.sfx_context_create)) + + (local delay (pxl8.sfx_delay_create {:time_l 350 :time_r 500 :feedback 0.4 :mix 0.25})) + (local reverb (pxl8.sfx_reverb_create {:room 0.5 :damping 0.5 :mix 0.3})) + (local compressor (pxl8.sfx_compressor_create {:threshold -12 :ratio 4 :attack 10 :release 100})) + + (pxl8.sfx_context_append_node ctx delay) + (pxl8.sfx_context_append_node ctx reverb) + (pxl8.sfx_context_append_node ctx compressor) + + (pxl8.sfx_mixer_attach ctx) + + (set params (pxl8.sfx_voice_params + {:waveform pxl8.SFX_WAVE_TRIANGLE + :attack 0.005 :decay 0.08 :sustain 0.0 :release 0.05 + :filter_type pxl8.SFX_FILTER_LOWPASS + :filter_cutoff 2500 :filter_resonance 0.15 + :filter_attack 0.005 :filter_decay 0.1 :filter_sustain 0.1 :filter_release 0.05 + :filter_env_depth 1500 + :fx_send 0.3})) + + (set bass-params (pxl8.sfx_voice_params + {:waveform pxl8.SFX_WAVE_SQUARE + :attack 0.005 :decay 0.1 :sustain 0.0 :release 0.05 + :filter_type pxl8.SFX_FILTER_LOWPASS + :filter_cutoff 600 :filter_resonance 0.2 + :fx_send 0.2}))) + +(fn start [] + (set playing true) + (set time 0) + (set step 0)) + +(fn stop [] + (set playing false) + (pxl8.sfx_stop_all ctx)) + +(fn update [dt] + (when playing + (set time (+ time dt)) + (when (>= time step-duration) + (set time (- time step-duration)) + (local note (. melody (+ 1 (% step (length melody))))) + (pxl8.sfx_play_note ctx note params 0.4) + (when (= (% step 4) 0) + (local bass-note (. bass (+ 1 (% (math.floor (/ step 4)) (length bass))))) + (pxl8.sfx_play_note ctx bass-note bass-params 0.35)) + (set step (+ step 1))))) + +{:init init + :start start + :stop stop + :update update + :is-playing (fn [] playing)} diff --git a/demo/res/tiles/tilesheet-dungeon.ase b/demo/res/tiles/tilesheet-dungeon.ase new file mode 100644 index 0000000..573598a Binary files /dev/null and b/demo/res/tiles/tilesheet-dungeon.ase differ diff --git a/demo/res/tiles/tilesheet-world.ase b/demo/res/tiles/tilesheet-world.ase new file mode 100644 index 0000000..e28fab2 Binary files /dev/null and b/demo/res/tiles/tilesheet-world.ase differ diff --git a/pxl8.sh b/pxl8.sh index f6a82c1..b3a6d3e 100755 --- a/pxl8.sh +++ b/pxl8.sh @@ -347,6 +347,7 @@ case "$COMMAND" in src/pxl8_save.c src/pxl8_script.c src/pxl8_sdl3.c + src/pxl8_sfx.c src/pxl8_tilemap.c src/pxl8_tilesheet.c src/pxl8_transition.c diff --git a/src/lua/pxl8.lua b/src/lua/pxl8.lua index 750a499..8963408 100644 --- a/src/lua/pxl8.lua +++ b/src/lua/pxl8.lua @@ -12,8 +12,9 @@ local gui = require("pxl8.gui") local world = require("pxl8.world") local transition = require("pxl8.transition") local anim = require("pxl8.anim") +local sfx = require("pxl8.sfx") -core.init(_pxl8_gfx, _pxl8_input, _pxl8_sys) +core.init(_pxl8_gfx, _pxl8_input, _pxl8_sfx_mixer, _pxl8_sys) local pxl8 = {} @@ -189,4 +190,57 @@ pxl8.anim_set_state = anim.set_state pxl8.anim_stop = anim.stop pxl8.anim_update = anim.update +pxl8.sfx_compressor_create = sfx.compressor_create +pxl8.sfx_compressor_set_attack = sfx.compressor_set_attack +pxl8.sfx_compressor_set_ratio = sfx.compressor_set_ratio +pxl8.sfx_compressor_set_release = sfx.compressor_set_release +pxl8.sfx_compressor_set_threshold = sfx.compressor_set_threshold +pxl8.sfx_context_append_node = sfx.context_append_node +pxl8.sfx_context_create = sfx.context_create +pxl8.sfx_context_destroy = sfx.context_destroy +pxl8.sfx_context_get_head = sfx.context_get_head +pxl8.sfx_context_get_volume = sfx.context_get_volume +pxl8.sfx_context_insert_node = sfx.context_insert_node +pxl8.sfx_context_remove_node = sfx.context_remove_node +pxl8.sfx_context_set_volume = sfx.context_set_volume +pxl8.sfx_delay_create = sfx.delay_create +pxl8.sfx_delay_set_feedback = sfx.delay_set_feedback +pxl8.sfx_delay_set_mix = sfx.delay_set_mix +pxl8.sfx_delay_set_time = sfx.delay_set_time +pxl8.sfx_get_master_volume = sfx.get_master_volume +pxl8.sfx_mixer_attach = sfx.mixer_attach +pxl8.sfx_mixer_detach = sfx.mixer_detach +pxl8.sfx_node_destroy = sfx.node_destroy +pxl8.sfx_note_to_freq = sfx.note_to_freq +pxl8.sfx_play_note = sfx.play_note +pxl8.sfx_release_voice = sfx.release_voice +pxl8.sfx_reverb_create = sfx.reverb_create +pxl8.sfx_reverb_set_damping = sfx.reverb_set_damping +pxl8.sfx_reverb_set_mix = sfx.reverb_set_mix +pxl8.sfx_reverb_set_room = sfx.reverb_set_room +pxl8.sfx_set_master_volume = sfx.set_master_volume +pxl8.sfx_stop_all = sfx.stop_all +pxl8.sfx_stop_voice = sfx.stop_voice +pxl8.sfx_voice_params = sfx.voice_params + +pxl8.SFX_FILTER_BANDPASS = sfx.FILTER_BANDPASS +pxl8.SFX_FILTER_HIGHPASS = sfx.FILTER_HIGHPASS +pxl8.SFX_FILTER_LOWPASS = sfx.FILTER_LOWPASS +pxl8.SFX_FILTER_NONE = sfx.FILTER_NONE + +pxl8.SFX_LFO_AMPLITUDE = sfx.LFO_AMPLITUDE +pxl8.SFX_LFO_FILTER = sfx.LFO_FILTER +pxl8.SFX_LFO_PITCH = sfx.LFO_PITCH + +pxl8.SFX_NODE_COMPRESSOR = sfx.NODE_COMPRESSOR +pxl8.SFX_NODE_DELAY = sfx.NODE_DELAY +pxl8.SFX_NODE_REVERB = sfx.NODE_REVERB + +pxl8.SFX_WAVE_NOISE = sfx.WAVE_NOISE +pxl8.SFX_WAVE_PULSE = sfx.WAVE_PULSE +pxl8.SFX_WAVE_SAW = sfx.WAVE_SAW +pxl8.SFX_WAVE_SINE = sfx.WAVE_SINE +pxl8.SFX_WAVE_SQUARE = sfx.WAVE_SQUARE +pxl8.SFX_WAVE_TRIANGLE = sfx.WAVE_TRIANGLE + return pxl8 diff --git a/src/lua/pxl8/core.lua b/src/lua/pxl8/core.lua index 531e3da..6a6f6b1 100644 --- a/src/lua/pxl8/core.lua +++ b/src/lua/pxl8/core.lua @@ -3,10 +3,11 @@ local C = ffi.C local core = {} -function core.init(gfx_ptr, input_ptr, sys_ptr) - core.gfx = gfx_ptr - core.input = input_ptr - core.sys = sys_ptr +function core.init(gfx, input, sfx_mixer, sys) + core.gfx = gfx + core.input = input + core.sfx_mixer = sfx_mixer + core.sys = sys end function core.get_fps() diff --git a/src/lua/pxl8/sfx.lua b/src/lua/pxl8/sfx.lua new file mode 100644 index 0000000..aeed71e --- /dev/null +++ b/src/lua/pxl8/sfx.lua @@ -0,0 +1,198 @@ +local ffi = require("ffi") +local C = ffi.C +local core = require("pxl8.core") + +local sfx = {} + +sfx.FILTER_BANDPASS = C.PXL8_SFX_FILTER_BANDPASS +sfx.FILTER_HIGHPASS = C.PXL8_SFX_FILTER_HIGHPASS +sfx.FILTER_LOWPASS = C.PXL8_SFX_FILTER_LOWPASS +sfx.FILTER_NONE = C.PXL8_SFX_FILTER_NONE + +sfx.LFO_AMPLITUDE = C.PXL8_SFX_LFO_AMPLITUDE +sfx.LFO_FILTER = C.PXL8_SFX_LFO_FILTER +sfx.LFO_PITCH = C.PXL8_SFX_LFO_PITCH + +sfx.NODE_COMPRESSOR = C.PXL8_SFX_NODE_COMPRESSOR +sfx.NODE_DELAY = C.PXL8_SFX_NODE_DELAY +sfx.NODE_REVERB = C.PXL8_SFX_NODE_REVERB + +sfx.WAVE_NOISE = C.PXL8_SFX_WAVE_NOISE +sfx.WAVE_PULSE = C.PXL8_SFX_WAVE_PULSE +sfx.WAVE_SAW = C.PXL8_SFX_WAVE_SAW +sfx.WAVE_SINE = C.PXL8_SFX_WAVE_SINE +sfx.WAVE_SQUARE = C.PXL8_SFX_WAVE_SQUARE +sfx.WAVE_TRIANGLE = C.PXL8_SFX_WAVE_TRIANGLE + +function sfx.compressor_create(opts) + opts = opts or {} + local cfg = ffi.new("pxl8_sfx_compressor_config") + cfg.attack = opts.attack or 10 + cfg.ratio = opts.ratio or 4 + cfg.release = opts.release or 100 + cfg.threshold = opts.threshold or -12 + return C.pxl8_sfx_compressor_create(cfg) +end + +function sfx.compressor_set_attack(node, attack) + C.pxl8_sfx_compressor_set_attack(node, attack) +end + +function sfx.compressor_set_ratio(node, ratio) + C.pxl8_sfx_compressor_set_ratio(node, ratio) +end + +function sfx.compressor_set_release(node, release) + C.pxl8_sfx_compressor_set_release(node, release) +end + +function sfx.compressor_set_threshold(node, threshold) + C.pxl8_sfx_compressor_set_threshold(node, threshold) +end + +function sfx.context_append_node(ctx, node) + C.pxl8_sfx_context_append_node(ctx, node) +end + +function sfx.context_create() + return C.pxl8_sfx_context_create() +end + +function sfx.context_destroy(ctx) + C.pxl8_sfx_context_destroy(ctx) +end + +function sfx.context_get_head(ctx) + return C.pxl8_sfx_context_get_head(ctx) +end + +function sfx.context_get_volume(ctx) + return C.pxl8_sfx_context_get_volume(ctx) +end + +function sfx.context_insert_node(ctx, after, node) + C.pxl8_sfx_context_insert_node(ctx, after, node) +end + +function sfx.context_remove_node(ctx, node) + C.pxl8_sfx_context_remove_node(ctx, node) +end + +function sfx.context_set_volume(ctx, volume) + C.pxl8_sfx_context_set_volume(ctx, volume) +end + +function sfx.delay_create(opts) + opts = opts or {} + local cfg = ffi.new("pxl8_sfx_delay_config") + cfg.feedback = opts.feedback or 0.4 + cfg.mix = opts.mix or 0.25 + cfg.time_l = opts.time_l or 350 + cfg.time_r = opts.time_r or 500 + return C.pxl8_sfx_delay_create(cfg) +end + +function sfx.delay_set_feedback(node, feedback) + C.pxl8_sfx_delay_set_feedback(node, feedback) +end + +function sfx.delay_set_mix(node, mix) + C.pxl8_sfx_delay_set_mix(node, mix) +end + +function sfx.delay_set_time(node, time_l, time_r) + C.pxl8_sfx_delay_set_time(node, time_l, time_r) +end + +function sfx.get_master_volume() + return C.pxl8_sfx_mixer_get_master_volume(core.sfx_mixer) +end + +function sfx.mixer_attach(ctx) + C.pxl8_sfx_mixer_attach(core.sfx_mixer, ctx) +end + +function sfx.mixer_detach(ctx) + C.pxl8_sfx_mixer_detach(core.sfx_mixer, ctx) +end + +function sfx.node_destroy(node) + C.pxl8_sfx_node_destroy(node) +end + +function sfx.note_to_freq(note) + return C.pxl8_sfx_note_to_freq(note) +end + +function sfx.play_note(ctx, note, params, volume) + return C.pxl8_sfx_play_note(ctx, note, params, volume or 0.8) +end + +function sfx.release_voice(ctx, voice_id) + C.pxl8_sfx_release_voice(ctx, voice_id) +end + +function sfx.reverb_create(opts) + opts = opts or {} + local cfg = ffi.new("pxl8_sfx_reverb_config") + cfg.damping = opts.damping or 0.5 + cfg.mix = opts.mix or 0.3 + cfg.room = opts.room or 0.5 + return C.pxl8_sfx_reverb_create(cfg) +end + +function sfx.reverb_set_damping(node, damping) + C.pxl8_sfx_reverb_set_damping(node, damping) +end + +function sfx.reverb_set_mix(node, mix) + C.pxl8_sfx_reverb_set_mix(node, mix) +end + +function sfx.reverb_set_room(node, room) + C.pxl8_sfx_reverb_set_room(node, room) +end + +function sfx.set_master_volume(volume) + C.pxl8_sfx_mixer_set_master_volume(core.sfx_mixer, volume) +end + +function sfx.stop_all(ctx) + C.pxl8_sfx_stop_all(ctx) +end + +function sfx.stop_voice(ctx, voice_id) + C.pxl8_sfx_stop_voice(ctx, voice_id) +end + +function sfx.voice_params(opts) + opts = opts or {} + local params = ffi.new("pxl8_sfx_voice_params") + + params.amp_env.attack = opts.attack or 0.01 + params.amp_env.decay = opts.decay or 0.1 + params.amp_env.sustain = opts.sustain or 0.5 + params.amp_env.release = opts.release or 0.2 + + params.filter_env.attack = opts.filter_attack or 0.01 + params.filter_env.decay = opts.filter_decay or 0.1 + params.filter_env.sustain = opts.filter_sustain or 0.3 + params.filter_env.release = opts.filter_release or 0.1 + + params.filter_type = opts.filter_type or C.PXL8_SFX_FILTER_NONE + params.lfo_target = opts.lfo_target or C.PXL8_SFX_LFO_PITCH + params.lfo_waveform = opts.lfo_waveform or C.PXL8_SFX_WAVE_SINE + params.waveform = opts.waveform or C.PXL8_SFX_WAVE_SINE + + params.filter_cutoff = opts.filter_cutoff or 4000 + params.filter_env_depth = opts.filter_env_depth or 0 + params.filter_resonance = opts.filter_resonance or 0 + params.fx_send = opts.fx_send or 0 + params.lfo_depth = opts.lfo_depth or 0 + params.lfo_rate = opts.lfo_rate or 0 + params.pulse_width = opts.pulse_width or 0.5 + + return params +end + +return sfx diff --git a/src/pxl8.c b/src/pxl8.c index 9f5ea5c..2177e11 100644 --- a/src/pxl8.c +++ b/src/pxl8.c @@ -218,12 +218,19 @@ pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]) { return PXL8_ERROR_INITIALIZATION_FAILED; } + game->mixer = pxl8_sfx_mixer_create(); + if (!game->mixer) { + pxl8_error("failed to create audio mixer"); + return PXL8_ERROR_INITIALIZATION_FAILED; + } + if (game->repl_mode) { pxl8_info("starting in REPL mode with script: %s", game->script_path); } pxl8_script_set_gfx(game->script, game->gfx); pxl8_script_set_input(game->script, &game->input); + pxl8_script_set_sfx(game->script, game->mixer); pxl8_script_set_sys(game->script, sys); if (game->script_path[0] != '\0') { @@ -364,6 +371,7 @@ void pxl8_quit(pxl8* sys) { pxl8_cart_unmount(sys->cart); } + pxl8_sfx_mixer_destroy(game->mixer); pxl8_gfx_destroy(game->gfx); pxl8_script_destroy(game->script); } @@ -394,6 +402,10 @@ pxl8_input_state* pxl8_get_input(const pxl8* sys) { return (sys && sys->game) ? &sys->game->input : NULL; } +pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys) { + return (sys && sys->game) ? sys->game->mixer : NULL; +} + void pxl8_center_cursor(pxl8* sys) { if (!sys || !sys->hal || !sys->hal->center_cursor) return; sys->hal->center_cursor(sys->platform_data); diff --git a/src/pxl8_atlas.c b/src/pxl8_atlas.c index 5653399..f752145 100644 --- a/src/pxl8_atlas.c +++ b/src/pxl8_atlas.c @@ -199,6 +199,22 @@ void pxl8_atlas_destroy(pxl8_atlas* atlas) { free(atlas); } +void pxl8_atlas_clear(pxl8_atlas* atlas, u32 preserve_count) { + if (!atlas) return; + + for (u32 i = preserve_count; i < atlas->entry_count; i++) { + atlas->entries[i].active = false; + } + + atlas->entry_count = preserve_count; + atlas->free_count = 0; + + atlas->skyline.nodes[0] = (pxl8_skyline_node){0, 0, (i32)atlas->width}; + atlas->skyline.count = 1; + + atlas->dirty = true; +} + bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode) { if (!atlas || atlas->width >= 4096) return false; diff --git a/src/pxl8_atlas.h b/src/pxl8_atlas.h index 7097901..2f0313b 100644 --- a/src/pxl8_atlas.h +++ b/src/pxl8_atlas.h @@ -27,6 +27,7 @@ bool pxl8_atlas_is_dirty(const pxl8_atlas* atlas); void pxl8_atlas_mark_clean(pxl8_atlas* atlas); u32 pxl8_atlas_add_texture(pxl8_atlas* atlas, const u8* pixels, u32 w, u32 h, pxl8_pixel_mode pixel_mode); +void pxl8_atlas_clear(pxl8_atlas* atlas, u32 preserve_count); bool pxl8_atlas_expand(pxl8_atlas* atlas, pxl8_pixel_mode pixel_mode); #ifdef __cplusplus diff --git a/src/pxl8_embed.h b/src/pxl8_embed.h index 0bc95c2..02810b9 100644 --- a/src/pxl8_embed.h +++ b/src/pxl8_embed.h @@ -43,6 +43,10 @@ static const char embed_pxl8_particles[] = { #embed "src/lua/pxl8/particles.lua" , 0 }; +static const char embed_pxl8_sfx[] = { +#embed "src/lua/pxl8/sfx.lua" +, 0 }; + static const char embed_pxl8_tilemap[] = { #embed "src/lua/pxl8/tilemap.lua" , 0 }; @@ -74,6 +78,7 @@ static const pxl8_embed pxl8_embeds[] = { PXL8_EMBED_ENTRY(embed_pxl8_input, "pxl8.input"), PXL8_EMBED_ENTRY(embed_pxl8_math, "pxl8.math"), PXL8_EMBED_ENTRY(embed_pxl8_particles, "pxl8.particles"), + PXL8_EMBED_ENTRY(embed_pxl8_sfx, "pxl8.sfx"), PXL8_EMBED_ENTRY(embed_pxl8_tilemap, "pxl8.tilemap"), PXL8_EMBED_ENTRY(embed_pxl8_transition, "pxl8.transition"), PXL8_EMBED_ENTRY(embed_pxl8_vfx, "pxl8.vfx"), diff --git a/src/pxl8_game.h b/src/pxl8_game.h index ab7f3ee..61fe3cd 100644 --- a/src/pxl8_game.h +++ b/src/pxl8_game.h @@ -2,11 +2,13 @@ #include "pxl8_gfx.h" #include "pxl8_script.h" +#include "pxl8_sfx.h" #include "pxl8_types.h" typedef struct pxl8_game { pxl8_gfx* gfx; pxl8_script* script; + pxl8_sfx_mixer* mixer; i32 frame_count; u64 last_time; diff --git a/src/pxl8_gfx.c b/src/pxl8_gfx.c index 027142f..ab7f015 100644 --- a/src/pxl8_gfx.c +++ b/src/pxl8_gfx.c @@ -197,6 +197,18 @@ static pxl8_result pxl8_gfx_ensure_atlas(pxl8_gfx* gfx) { return gfx->atlas ? PXL8_OK : PXL8_ERROR_OUT_OF_MEMORY; } +void pxl8_gfx_clear_textures(pxl8_gfx* gfx) { + if (!gfx) return; + + if (gfx->atlas) { + pxl8_atlas_clear(gfx->atlas, 0); + } + + if (gfx->sprite_cache) { + gfx->sprite_cache_count = 0; + } +} + pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height) { if (!gfx || !gfx->initialized || !pixels) return PXL8_ERROR_INVALID_ARGUMENT; diff --git a/src/pxl8_gfx.h b/src/pxl8_gfx.h index e83f0a5..5efd3ac 100644 --- a/src/pxl8_gfx.h +++ b/src/pxl8_gfx.h @@ -25,36 +25,37 @@ extern "C" { pxl8_gfx* pxl8_gfx_create(const pxl8_hal* hal, void* platform_data, pxl8_pixel_mode mode, pxl8_resolution resolution); void pxl8_gfx_destroy(pxl8_gfx* gfx); +void pxl8_gfx_present(pxl8_gfx* gfx); +void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt); +void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx); + pxl8_bounds pxl8_gfx_get_bounds(pxl8_gfx* gfx); -pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx); u8* pxl8_gfx_get_framebuffer_indexed(pxl8_gfx* gfx); u16* pxl8_gfx_get_framebuffer_hicolor(pxl8_gfx* gfx); i32 pxl8_gfx_get_height(const pxl8_gfx* gfx); u32 pxl8_gfx_get_palette_size(const pxl8_gfx* gfx); +pxl8_pixel_mode pxl8_gfx_get_pixel_mode(pxl8_gfx* gfx); i32 pxl8_gfx_get_width(const pxl8_gfx* gfx); +void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom); void pxl8_gfx_set_viewport(pxl8_gfx* gfx, pxl8_viewport vp); +pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height); +void pxl8_gfx_clear_textures(pxl8_gfx* gfx); pxl8_result pxl8_gfx_create_texture(pxl8_gfx* gfx, const u8* pixels, u32 width, u32 height); pxl8_result pxl8_gfx_load_font_atlas(pxl8_gfx* gfx); pxl8_result pxl8_gfx_load_palette(pxl8_gfx* gfx, const char* path); pxl8_result pxl8_gfx_load_sprite(pxl8_gfx* gfx, const char* path); -void pxl8_gfx_present(pxl8_gfx* gfx); -void pxl8_gfx_project(pxl8_gfx* gfx, f32 left, f32 right, f32 top, f32 bottom); -void pxl8_gfx_upload_framebuffer(pxl8_gfx* gfx); -pxl8_viewport pxl8_gfx_viewport(pxl8_bounds bounds, i32 width, i32 height); +i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed); +void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx); void pxl8_gfx_color_ramp(pxl8_gfx* gfx, u8 start, u8 count, u32 from_color, u32 to_color); void pxl8_gfx_cycle_palette(pxl8_gfx* gfx, u8 start, u8 count, i32 step); void pxl8_gfx_fade_palette(pxl8_gfx* gfx, u8 start, u8 count, f32 amount, u32 target_color); void pxl8_gfx_interpolate_palettes(pxl8_gfx* gfx, u32* palette1, u32* palette2, u8 start, u8 count, f32 t); -void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors); - -i32 pxl8_gfx_add_palette_cycle(pxl8_gfx* gfx, u8 start_index, u8 end_index, f32 speed); void pxl8_gfx_remove_palette_cycle(pxl8_gfx* gfx, i32 cycle_id); void pxl8_gfx_set_palette_cycle_speed(pxl8_gfx* gfx, i32 cycle_id, f32 speed); -void pxl8_gfx_clear_palette_cycles(pxl8_gfx* gfx); -void pxl8_gfx_update(pxl8_gfx* gfx, f32 dt); +void pxl8_gfx_swap_palette(pxl8_gfx* gfx, u8 start, u8 count, u32* new_colors); void pxl8_circle(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color); void pxl8_circle_fill(pxl8_gfx* gfx, i32 cx, i32 cy, i32 radius, u32 color); @@ -68,6 +69,10 @@ void pxl8_sprite(pxl8_gfx* gfx, u32 sprite_id, i32 x, i32 y, i32 w, i32 h); void pxl8_text(pxl8_gfx* gfx, const char* text, i32 x, i32 y, u32 color); void pxl8_3d_clear_zbuffer(pxl8_gfx* gfx); +void pxl8_3d_draw_line_3d(pxl8_gfx* gfx, pxl8_vec3 p0, pxl8_vec3 p1, u32 color); +void pxl8_3d_draw_triangle(pxl8_gfx* gfx, pxl8_triangle tri); +void pxl8_3d_draw_triangle_raw(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, u32 color); +void pxl8_3d_draw_triangle_textured(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, f32 u0, f32 v0f, f32 u1, f32 v1f, f32 u2, f32 v2f, u32 texture_id); const pxl8_frustum* pxl8_3d_get_frustum(pxl8_gfx* gfx); void pxl8_3d_set_affine_textures(pxl8_gfx* gfx, bool affine); void pxl8_3d_set_backface_culling(pxl8_gfx* gfx, bool culling); @@ -75,10 +80,6 @@ void pxl8_3d_set_model(pxl8_gfx* gfx, pxl8_mat4 mat); void pxl8_3d_set_projection(pxl8_gfx* gfx, pxl8_mat4 mat); void pxl8_3d_set_view(pxl8_gfx* gfx, pxl8_mat4 mat); void pxl8_3d_set_wireframe(pxl8_gfx* gfx, bool wireframe); -void pxl8_3d_draw_line_3d(pxl8_gfx* gfx, pxl8_vec3 p0, pxl8_vec3 p1, u32 color); -void pxl8_3d_draw_triangle(pxl8_gfx* gfx, pxl8_triangle tri); -void pxl8_3d_draw_triangle_raw(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, u32 color); -void pxl8_3d_draw_triangle_textured(pxl8_gfx* gfx, pxl8_vec3 v0, pxl8_vec3 v1, pxl8_vec3 v2, f32 u0, f32 v0f, f32 u1, f32 v1f, f32 u2, f32 v2f, u32 texture_id); #ifdef __cplusplus } diff --git a/src/pxl8_math.h b/src/pxl8_math.h index 956f3df..0d941c1 100644 --- a/src/pxl8_math.h +++ b/src/pxl8_math.h @@ -4,6 +4,9 @@ #include "pxl8_types.h" +#define PXL8_PI 3.14159265358979323846f +#define PXL8_TAU (PXL8_PI * 2.0f) + typedef struct pxl8_vec2 { f32 x, y; } pxl8_vec2; diff --git a/src/pxl8_script.c b/src/pxl8_script.c index c5972b1..67ab32d 100644 --- a/src/pxl8_script.c +++ b/src/pxl8_script.c @@ -22,6 +22,7 @@ struct pxl8_script { lua_State* L; pxl8_gfx* gfx; pxl8_input_state* input; + pxl8_sfx_mixer* mixer; char last_error[PXL8_MAX_ERROR_SIZE]; char main_path[PXL8_MAX_PATH]; char watch_dir[PXL8_MAX_PATH]; @@ -420,7 +421,53 @@ static const char* pxl8_ffi_cdefs = "void pxl8_save_free(u8* data);\n" "bool pxl8_save_exists(pxl8_save* save, u8 slot);\n" "i32 pxl8_save_delete(pxl8_save* save, u8 slot);\n" -"const char* pxl8_save_get_directory(pxl8_save* save);\n"; +"const char* pxl8_save_get_directory(pxl8_save* save);\n" +"\n" +"typedef struct pxl8_sfx_context pxl8_sfx_context;\n" +"typedef struct pxl8_sfx_mixer pxl8_sfx_mixer;\n" +"typedef struct pxl8_sfx_node pxl8_sfx_node;\n" +"typedef enum pxl8_sfx_filter_type { PXL8_SFX_FILTER_BANDPASS = 0, PXL8_SFX_FILTER_HIGHPASS, PXL8_SFX_FILTER_LOWPASS, PXL8_SFX_FILTER_NONE } pxl8_sfx_filter_type;\n" +"typedef enum pxl8_sfx_lfo_target { PXL8_SFX_LFO_AMPLITUDE = 0, PXL8_SFX_LFO_FILTER, PXL8_SFX_LFO_PITCH } pxl8_sfx_lfo_target;\n" +"typedef enum pxl8_sfx_node_type { PXL8_SFX_NODE_COMPRESSOR, PXL8_SFX_NODE_DELAY, PXL8_SFX_NODE_REVERB } pxl8_sfx_node_type;\n" +"typedef enum pxl8_sfx_waveform { PXL8_SFX_WAVE_NOISE = 0, PXL8_SFX_WAVE_PULSE, PXL8_SFX_WAVE_SAW, PXL8_SFX_WAVE_SINE, PXL8_SFX_WAVE_SQUARE, PXL8_SFX_WAVE_TRIANGLE } pxl8_sfx_waveform;\n" +"typedef struct pxl8_sfx_adsr { f32 attack; f32 decay; f32 sustain; f32 release; } pxl8_sfx_adsr;\n" +"typedef struct pxl8_sfx_compressor_config { f32 attack; f32 ratio; f32 release; f32 threshold; } pxl8_sfx_compressor_config;\n" +"typedef struct pxl8_sfx_delay_config { f32 feedback; f32 mix; u32 time_l; u32 time_r; } pxl8_sfx_delay_config;\n" +"typedef struct pxl8_sfx_reverb_config { f32 damping; f32 mix; f32 room; } pxl8_sfx_reverb_config;\n" +"typedef struct pxl8_sfx_voice_params { pxl8_sfx_adsr amp_env; pxl8_sfx_adsr filter_env; pxl8_sfx_filter_type filter_type; pxl8_sfx_lfo_target lfo_target; pxl8_sfx_waveform lfo_waveform; pxl8_sfx_waveform waveform; f32 filter_cutoff; f32 filter_env_depth; f32 filter_resonance; f32 fx_send; f32 lfo_depth; f32 lfo_rate; f32 pulse_width; } pxl8_sfx_voice_params;\n" +"pxl8_sfx_node* pxl8_sfx_compressor_create(pxl8_sfx_compressor_config cfg);\n" +"void pxl8_sfx_compressor_set_attack(pxl8_sfx_node* node, f32 attack);\n" +"void pxl8_sfx_compressor_set_ratio(pxl8_sfx_node* node, f32 ratio);\n" +"void pxl8_sfx_compressor_set_release(pxl8_sfx_node* node, f32 release);\n" +"void pxl8_sfx_compressor_set_threshold(pxl8_sfx_node* node, f32 threshold);\n" +"void pxl8_sfx_context_append_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node);\n" +"pxl8_sfx_context* pxl8_sfx_context_create(void);\n" +"void pxl8_sfx_context_destroy(pxl8_sfx_context* ctx);\n" +"pxl8_sfx_node* pxl8_sfx_context_get_head(pxl8_sfx_context* ctx);\n" +"f32 pxl8_sfx_context_get_volume(const pxl8_sfx_context* ctx);\n" +"void pxl8_sfx_context_insert_node(pxl8_sfx_context* ctx, pxl8_sfx_node* after, pxl8_sfx_node* node);\n" +"void pxl8_sfx_context_remove_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node);\n" +"void pxl8_sfx_context_set_volume(pxl8_sfx_context* ctx, f32 volume);\n" +"pxl8_sfx_node* pxl8_sfx_delay_create(pxl8_sfx_delay_config cfg);\n" +"void pxl8_sfx_delay_set_feedback(pxl8_sfx_node* node, f32 feedback);\n" +"void pxl8_sfx_delay_set_mix(pxl8_sfx_node* node, f32 mix);\n" +"void pxl8_sfx_delay_set_time(pxl8_sfx_node* node, u32 time_l, u32 time_r);\n" +"void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);\n" +"pxl8_sfx_mixer* pxl8_sfx_mixer_create(void);\n" +"void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer);\n" +"void pxl8_sfx_mixer_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx);\n" +"f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer);\n" +"void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume);\n" +"void pxl8_sfx_node_destroy(pxl8_sfx_node* node);\n" +"f32 pxl8_sfx_note_to_freq(u8 note);\n" +"u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume);\n" +"void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id);\n" +"pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg);\n" +"void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping);\n" +"void pxl8_sfx_reverb_set_mix(pxl8_sfx_node* node, f32 mix);\n" +"void pxl8_sfx_reverb_set_room(pxl8_sfx_node* node, f32 room);\n" +"void pxl8_sfx_stop_all(pxl8_sfx_context* ctx);\n" +"void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id);\n"; void pxl8_lua_log(int level, const char* file, int line, const char* msg) { if (file && (file[0] == '?' || file[0] == '\0')) file = NULL; @@ -637,6 +684,14 @@ void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input) { } } +void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer) { + if (!script) return; + script->mixer = mixer; + if (script->L && mixer) { + lua_pushlightuserdata(script->L, mixer); + lua_setglobal(script->L, "_pxl8_sfx_mixer"); + } +} void pxl8_script_set_sys(pxl8_script* script, void* sys) { if (!script) return; @@ -920,6 +975,90 @@ pxl8_result pxl8_script_call_function_f32(pxl8_script* script, const char* name, return PXL8_OK; } +static bool pxl8_script_is_builtin_global(const char* name) { + static const char* builtins[] = { + "_G", "_VERSION", "arg", "assert", "bit", "collectgarbage", + "coroutine", "debug", "dofile", "error", "fennel", "ffi", + "gcinfo", "getfenv", "getmetatable", "init", "io", "ipairs", + "jit", "load", "loadfile", "loadstring", "math", "module", + "newproxy", "next", "os", "package", "pairs", "pcall", + "print", "pxl8", "rawequal", "rawget", "rawset", "require", + "select", "setfenv", "setmetatable", "string", "table", + "tonumber", "tostring", "type", "unpack", "update", "frame", + "xpcall", "_pxl8_gfx", "_pxl8_input", "_pxl8_sfx_mixer", "_pxl8_sys", + NULL + }; + for (int i = 0; builtins[i]; i++) { + if (strcmp(name, builtins[i]) == 0) return true; + } + return name[0] == '_' && name[1] == '_'; +} + +static void pxl8_script_cleanup(pxl8_script* script) { + if (script->gfx) { + pxl8_gfx_clear_textures(script->gfx); + } + + if (script->mixer) { + pxl8_sfx_mixer_clear(script->mixer); + } +} + +static void pxl8_script_save_globals(pxl8_script* script) { + lua_State* L = script->L; + + lua_newtable(L); + int saved_table = lua_gettop(L); + + lua_pushvalue(L, LUA_GLOBALSINDEX); + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + if (lua_type(L, -2) == LUA_TSTRING) { + const char* name = lua_tostring(L, -2); + int vtype = lua_type(L, -1); + + if (!pxl8_script_is_builtin_global(name) && + vtype != LUA_TFUNCTION && + vtype != LUA_TUSERDATA && + vtype != LUA_TLIGHTUSERDATA && + vtype != LUA_TTHREAD && + vtype != 10) { + lua_pushvalue(L, -2); + lua_pushvalue(L, -2); + lua_settable(L, saved_table); + } + } + lua_pop(L, 1); + } + lua_pop(L, 1); + + lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); + pxl8_debug("Hot reload state saved"); +} + +static void pxl8_script_restore_globals(pxl8_script* script) { + lua_State* L = script->L; + + lua_getfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); + if (lua_isnil(L, -1)) { + lua_pop(L, 1); + return; + } + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + lua_pushvalue(L, -2); + lua_pushvalue(L, -2); + lua_settable(L, LUA_GLOBALSINDEX); + lua_pop(L, 1); + } + lua_pop(L, 1); + + lua_pushnil(L); + lua_setfield(L, LUA_REGISTRYINDEX, "_pxl8_hotreload_state"); + pxl8_debug("Hot reload state restored"); +} + static time_t get_file_mod_time(const char* path) { struct stat file_stat; if (stat(path, &file_stat) == 0) { @@ -972,15 +1111,8 @@ pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) { pxl8_strncpy(script->main_path, path, sizeof(script->main_path)); - char* last_slash = strrchr(script->main_path, '/'); - if (last_slash) { - size_t dir_len = last_slash - script->main_path; - strncpy(script->watch_dir, script->main_path, dir_len); - script->watch_dir[dir_len] = '\0'; - } else { - script->watch_dir[0] = '.'; - script->watch_dir[1] = '\0'; - } + script->watch_dir[0] = '.'; + script->watch_dir[1] = '\0'; script->latest_mod_time = get_latest_script_mod_time(script->watch_dir); @@ -1005,26 +1137,72 @@ pxl8_result pxl8_script_load_main(pxl8_script* script, const char* path) { return result; } +static void pxl8_script_clear_cart_modules(pxl8_script* script) { + lua_State* L = script->L; + + lua_getglobal(L, "package"); + lua_getfield(L, -1, "loaded"); + + lua_pushnil(L); + while (lua_next(L, -2) != 0) { + lua_pop(L, 1); + const char* name = lua_tostring(L, -1); + if (name && strncmp(name, "pxl8", 4) != 0 && + strcmp(name, "fennel") != 0 && + strcmp(name, "ffi") != 0 && + strcmp(name, "bit") != 0 && + strcmp(name, "jit") != 0 && + strcmp(name, "string") != 0 && + strcmp(name, "table") != 0 && + strcmp(name, "math") != 0 && + strcmp(name, "io") != 0 && + strcmp(name, "os") != 0 && + strcmp(name, "debug") != 0 && + strcmp(name, "coroutine") != 0 && + strcmp(name, "package") != 0) { + lua_pushvalue(L, -1); + lua_pushnil(L); + lua_settable(L, -4); + } + } + + lua_pop(L, 2); +} + bool pxl8_script_check_reload(pxl8_script* script) { - if (!script || script->main_path[0] == '\0') return false; + if (!script || script->main_path[0] == '\0') { + return false; + } time_t current_mod_time = get_latest_script_mod_time(script->watch_dir); if (current_mod_time > script->latest_mod_time && current_mod_time != 0) { pxl8_info("Script files modified, reloading: %s", script->main_path); script->latest_mod_time = current_mod_time; + pxl8_script_cleanup(script); + pxl8_script_save_globals(script); + pxl8_script_clear_cart_modules(script); + const char* ext = strrchr(script->main_path, '.'); + bool reloaded = false; + if (ext && strcmp(ext, ".fnl") == 0) { if (pxl8_script_run_fennel_file(script, script->main_path) == PXL8_OK) { pxl8_script_call_function(script, "init"); - return true; + reloaded = true; } } else if (ext && strcmp(ext, ".lua") == 0) { if (pxl8_script_run_file(script, script->main_path) == PXL8_OK) { pxl8_script_call_function(script, "init"); - return true; + reloaded = true; } } + + if (reloaded) { + pxl8_script_restore_globals(script); + } + + return reloaded; } return false; diff --git a/src/pxl8_script.h b/src/pxl8_script.h index 3aefaf1..0281bb1 100644 --- a/src/pxl8_script.h +++ b/src/pxl8_script.h @@ -2,6 +2,7 @@ #include "pxl8_cart.h" #include "pxl8_gfx.h" +#include "pxl8_sfx.h" #include "pxl8_types.h" typedef struct pxl8_script pxl8_script; @@ -19,6 +20,7 @@ bool pxl8_script_is_incomplete_input(pxl8_script* script); void pxl8_script_set_cart_path(pxl8_script* script, const char* cart_path, const char* original_cwd); void pxl8_script_set_gfx(pxl8_script* script, pxl8_gfx* gfx); void pxl8_script_set_input(pxl8_script* script, pxl8_input_state* input); +void pxl8_script_set_sfx(pxl8_script* script, pxl8_sfx_mixer* mixer); void pxl8_script_set_sys(pxl8_script* script, void* sys); pxl8_result pxl8_script_call_function(pxl8_script* script, const char* name); diff --git a/src/pxl8_sdl3.c b/src/pxl8_sdl3.c index de85f73..1a6c1ed 100644 --- a/src/pxl8_sdl3.c +++ b/src/pxl8_sdl3.c @@ -126,7 +126,7 @@ static void sdl3_upload_texture(void* platform_data, const u8* pixels, u32 w, u3 } SDL_AppResult SDL_AppInit(void** appstate, int argc, char* argv[]) { - if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS)) { + if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS | SDL_INIT_AUDIO)) { pxl8_error("SDL_Init failed: %s", SDL_GetError()); return SDL_APP_FAILURE; } diff --git a/src/pxl8_sfx.c b/src/pxl8_sfx.c new file mode 100644 index 0000000..90a4edf --- /dev/null +++ b/src/pxl8_sfx.c @@ -0,0 +1,1092 @@ +#include "pxl8_sfx.h" + +#include +#include + +#include + +#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; + 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; + f32 volume; + f32 current_time; + u16 next_voice_id; +}; + +struct pxl8_sfx_mixer { + SDL_AudioStream* stream; + pxl8_sfx_context* contexts[PXL8_SFX_MAX_CONTEXTS]; + f32 master_volume; + f32* output_buffer; +}; + +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) { + 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; + + 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++) { + if (!ctx->voices[v].active) continue; + + f32 dl, dr, wl, wr; + voice_process(&ctx->voices[v], &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; +} + +static void SDLCALL audio_callback(void* userdata, SDL_AudioStream* stream, int additional_amount, int total_amount) { + (void)total_amount; + + pxl8_sfx_mixer* mixer = (pxl8_sfx_mixer*)userdata; + if (!mixer || !mixer->output_buffer) return; + + u32 samples_needed = additional_amount / (sizeof(f32) * 2); + if (samples_needed > PXL8_SFX_BUFFER_SIZE) samples_needed = PXL8_SFX_BUFFER_SIZE; + + for (u32 i = 0; i < samples_needed; 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; + } + + SDL_PutAudioStreamData(stream, mixer->output_buffer, samples_needed * sizeof(f32) * 2); +} + +pxl8_sfx_mixer* pxl8_sfx_mixer_create(void) { + 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->master_volume = 0.8f; + + SDL_AudioSpec spec = { + .channels = 2, + .format = SDL_AUDIO_F32, + .freq = PXL8_SFX_SAMPLE_RATE + }; + + mixer->stream = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &spec, audio_callback, mixer); + if (!mixer->stream) { + pxl8_error("Failed to open audio device: %s", SDL_GetError()); + free(mixer->output_buffer); + free(mixer); + return NULL; + } + + SDL_ResumeAudioStreamDevice(mixer->stream); + 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->stream) { + SDL_DestroyAudioStream(mixer->stream); + } + + free(mixer->output_buffer); + free(mixer); +} + +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; + 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)); +} + +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) { + 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); + + 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; + } +} diff --git a/src/pxl8_sfx.h b/src/pxl8_sfx.h new file mode 100644 index 0000000..780604d --- /dev/null +++ b/src/pxl8_sfx.h @@ -0,0 +1,133 @@ +#pragma once + +#include "pxl8_types.h" + +#define PXL8_SFX_BUFFER_SIZE 1024 +#define PXL8_SFX_MAX_CONTEXTS 8 +#define PXL8_SFX_MAX_DELAY_SAMPLES 44100 +#define PXL8_SFX_MAX_VOICES 16 +#define PXL8_SFX_SAMPLE_RATE 44100 + +typedef struct pxl8_sfx_context pxl8_sfx_context; +typedef struct pxl8_sfx_mixer pxl8_sfx_mixer; +typedef struct pxl8_sfx_node pxl8_sfx_node; + +typedef enum pxl8_sfx_filter_type { + PXL8_SFX_FILTER_BANDPASS = 0, + PXL8_SFX_FILTER_HIGHPASS, + PXL8_SFX_FILTER_LOWPASS, + PXL8_SFX_FILTER_NONE +} pxl8_sfx_filter_type; + +typedef enum pxl8_sfx_lfo_target { + PXL8_SFX_LFO_AMPLITUDE = 0, + PXL8_SFX_LFO_FILTER, + PXL8_SFX_LFO_PITCH +} pxl8_sfx_lfo_target; + +typedef enum pxl8_sfx_node_type { + PXL8_SFX_NODE_COMPRESSOR, + PXL8_SFX_NODE_DELAY, + PXL8_SFX_NODE_REVERB +} pxl8_sfx_node_type; + +typedef enum pxl8_sfx_waveform { + PXL8_SFX_WAVE_NOISE = 0, + PXL8_SFX_WAVE_PULSE, + PXL8_SFX_WAVE_SAW, + PXL8_SFX_WAVE_SINE, + PXL8_SFX_WAVE_SQUARE, + PXL8_SFX_WAVE_TRIANGLE +} pxl8_sfx_waveform; + +typedef struct pxl8_sfx_adsr { + f32 attack; + f32 decay; + f32 sustain; + f32 release; +} pxl8_sfx_adsr; + +typedef struct pxl8_sfx_compressor_config { + f32 attack; + f32 ratio; + f32 release; + f32 threshold; +} pxl8_sfx_compressor_config; + +typedef struct pxl8_sfx_delay_config { + f32 feedback; + f32 mix; + u32 time_l; + u32 time_r; +} pxl8_sfx_delay_config; + +typedef struct pxl8_sfx_reverb_config { + f32 damping; + f32 mix; + f32 room; +} pxl8_sfx_reverb_config; + +typedef struct pxl8_sfx_voice_params { + pxl8_sfx_adsr amp_env; + pxl8_sfx_adsr filter_env; + pxl8_sfx_filter_type filter_type; + pxl8_sfx_lfo_target lfo_target; + pxl8_sfx_waveform lfo_waveform; + pxl8_sfx_waveform waveform; + f32 filter_cutoff; + f32 filter_env_depth; + f32 filter_resonance; + f32 fx_send; + f32 lfo_depth; + f32 lfo_rate; + f32 pulse_width; +} pxl8_sfx_voice_params; + +#ifdef __cplusplus +extern "C" { +#endif + +pxl8_sfx_node* pxl8_sfx_compressor_create(pxl8_sfx_compressor_config cfg); +void pxl8_sfx_compressor_set_attack(pxl8_sfx_node* node, f32 attack); +void pxl8_sfx_compressor_set_ratio(pxl8_sfx_node* node, f32 ratio); +void pxl8_sfx_compressor_set_release(pxl8_sfx_node* node, f32 release); +void pxl8_sfx_compressor_set_threshold(pxl8_sfx_node* node, f32 threshold); + +void pxl8_sfx_context_append_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node); +pxl8_sfx_context* pxl8_sfx_context_create(void); +void pxl8_sfx_context_destroy(pxl8_sfx_context* ctx); +pxl8_sfx_node* pxl8_sfx_context_get_head(pxl8_sfx_context* ctx); +f32 pxl8_sfx_context_get_volume(const pxl8_sfx_context* ctx); +void pxl8_sfx_context_insert_node(pxl8_sfx_context* ctx, pxl8_sfx_node* after, pxl8_sfx_node* node); +void pxl8_sfx_context_remove_node(pxl8_sfx_context* ctx, pxl8_sfx_node* node); +void pxl8_sfx_context_set_volume(pxl8_sfx_context* ctx, f32 volume); + +pxl8_sfx_node* pxl8_sfx_delay_create(pxl8_sfx_delay_config cfg); +void pxl8_sfx_delay_set_feedback(pxl8_sfx_node* node, f32 feedback); +void pxl8_sfx_delay_set_mix(pxl8_sfx_node* node, f32 mix); +void pxl8_sfx_delay_set_time(pxl8_sfx_node* node, u32 time_l, u32 time_r); + +void pxl8_sfx_mixer_attach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx); +void pxl8_sfx_mixer_clear(pxl8_sfx_mixer* mixer); +pxl8_sfx_mixer* pxl8_sfx_mixer_create(void); +void pxl8_sfx_mixer_destroy(pxl8_sfx_mixer* mixer); +void pxl8_sfx_mixer_detach(pxl8_sfx_mixer* mixer, pxl8_sfx_context* ctx); +f32 pxl8_sfx_mixer_get_master_volume(const pxl8_sfx_mixer* mixer); +void pxl8_sfx_mixer_set_master_volume(pxl8_sfx_mixer* mixer, f32 volume); + +void pxl8_sfx_node_destroy(pxl8_sfx_node* node); +f32 pxl8_sfx_note_to_freq(u8 note); +u16 pxl8_sfx_play_note(pxl8_sfx_context* ctx, u8 note, const pxl8_sfx_voice_params* params, f32 volume); +void pxl8_sfx_release_voice(pxl8_sfx_context* ctx, u16 voice_id); + +pxl8_sfx_node* pxl8_sfx_reverb_create(pxl8_sfx_reverb_config cfg); +void pxl8_sfx_reverb_set_damping(pxl8_sfx_node* node, f32 damping); +void pxl8_sfx_reverb_set_mix(pxl8_sfx_node* node, f32 mix); +void pxl8_sfx_reverb_set_room(pxl8_sfx_node* node, f32 room); + +void pxl8_sfx_stop_all(pxl8_sfx_context* ctx); +void pxl8_sfx_stop_voice(pxl8_sfx_context* ctx, u16 voice_id); + +#ifdef __cplusplus +} +#endif diff --git a/src/pxl8_sys.h b/src/pxl8_sys.h index c534389..f5e59eb 100644 --- a/src/pxl8_sys.h +++ b/src/pxl8_sys.h @@ -3,6 +3,7 @@ #include "pxl8_gfx.h" #include "pxl8_hal.h" #include "pxl8_io.h" +#include "pxl8_sfx.h" #include "pxl8_types.h" typedef struct pxl8 pxl8; @@ -14,22 +15,23 @@ extern "C" { pxl8* pxl8_create(const pxl8_hal* hal); void pxl8_destroy(pxl8* sys); +pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]); +pxl8_result pxl8_update(pxl8* sys); +pxl8_result pxl8_frame(pxl8* sys); +void pxl8_quit(pxl8* sys); + f32 pxl8_get_fps(const pxl8* sys); pxl8_gfx* pxl8_get_gfx(const pxl8* sys); pxl8_input_state* pxl8_get_input(const pxl8* sys); pxl8_size pxl8_get_resolution_dimensions(pxl8_resolution resolution); +pxl8_sfx_mixer* pxl8_get_sfx_mixer(const pxl8* sys); bool pxl8_is_running(const pxl8* sys); +void pxl8_center_cursor(pxl8* sys); void pxl8_set_cursor(pxl8* sys, pxl8_cursor cursor); void pxl8_set_relative_mouse_mode(pxl8* sys, bool enabled); void pxl8_set_running(pxl8* sys, bool running); -void pxl8_center_cursor(pxl8* sys); -pxl8_result pxl8_frame(pxl8* sys); -pxl8_result pxl8_init(pxl8* sys, i32 argc, char* argv[]); -void pxl8_quit(pxl8* sys); -pxl8_result pxl8_update(pxl8* sys); - #ifdef __cplusplus } #endif