add sound

This commit is contained in:
asrael 2026-01-07 17:45:46 -06:00
parent 99d9c43ea3
commit 01d6e09a91
22 changed files with 1825 additions and 39 deletions

View file

@ -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;