diff --git a/application.h b/application.h index 07eda9b..728bde7 100644 --- a/application.h +++ b/application.h @@ -17,7 +17,6 @@ #include "application/sound.h" #include "application/audio_device.h" #include "application/audio_music.h" -#include "application/audio_sfx.h" #include "application/music_config.h" #include "font/glfreetypefont.h" diff --git a/application/audio_music.h b/application/audio_music.h index 144618d..7d29531 100644 --- a/application/audio_music.h +++ b/application/audio_music.h @@ -9,13 +9,20 @@ typedef struct music_layer_t music_layer_t; typedef struct music_theme_t music_theme_t; typedef vec2f_t music_curve_point_t; +typedef struct { + const void *pcm_data; + u64 frame_count; + u32 channels; + u32 sample_rate; +} music_audio_info_t; + struct music_layer_t{ - str_t filepath; - ma_sound sound; + ma_audio_buffer buffer; + ma_sound sound; music_curve_point_t curve[8]; - u32 curve_count; - f32 current_db; - bool loaded; + u32 curve_count; + f32 current_db; + bool loaded; }; struct music_theme_t { @@ -30,7 +37,7 @@ struct music_theme_t { global music_theme_t global_music = {0}; -bool music_theme_load(str_t base_path, const str_t layer_files[], u32 count, +bool music_theme_load(const music_audio_info_t audio_info[], u32 count, const music_curve_point_t curves[][8], const u32 curve_counts[]); void music_theme_play(void); void music_theme_stop(f32 fade_sec); @@ -77,8 +84,7 @@ INTERNAL void music__internal__apply_intensity(music_theme_t *m) } bool music_theme_load( - const str_t base_path, - const str_t layer_files[], + const music_audio_info_t audio_info[], u32 count, const music_curve_point_t curves[][8], const u32 curve_counts[]) @@ -102,18 +108,35 @@ bool music_theme_load( for (u32 i = 0; i < count; i++) { music_layer_t *layer = &global_music.layers[i]; - char fullpath[512]; - snprintf(fullpath, sizeof(fullpath), "%.*s/%.*s", STR_ARG(base_path), STR_ARG(layer_files[i])); - layer->filepath = str(fullpath); - layer->curve_count = curve_counts[i]; memcpy(layer->curve, curves[i], sizeof(music_curve_point_t) * curve_counts[i]); layer->current_db = 0.0f; - ma_result result = ma_sound_init_from_file(engine, fullpath, - MA_SOUND_FLAG_STREAM | MA_SOUND_FLAG_DECODE, NULL, NULL, &layer->sound); + if (!audio_info[i].pcm_data || audio_info[i].frame_count == 0) { + eprint("[audio] music: layer %u has no audio data\n", i); + layer->loaded = false; + continue; + } + + ma_audio_buffer_config buf_cfg = ma_audio_buffer_config_init( + ma_format_f32, + audio_info[i].channels, + audio_info[i].frame_count, + (void *)audio_info[i].pcm_data, + NULL); + + ma_result result = ma_audio_buffer_init(&buf_cfg, &layer->buffer); + if (result != MA_SUCCESS) { + eprint("[audio] music: buffer init failed layer %u: %d\n", i, result); + layer->loaded = false; + continue; + } + + result = ma_sound_init_from_data_source(engine, + &layer->buffer, 0, NULL, &layer->sound); if (result != MA_SUCCESS) { - eprint("[audio] music: failed to load `%s`: %d\n", fullpath, result); + eprint("[audio] music: sound init failed layer %u: %d\n", i, result); + ma_audio_buffer_uninit(&layer->buffer); layer->loaded = false; continue; } @@ -188,6 +211,7 @@ void music_theme_unload(void) for (u32 i = 0; i < global_music.layer_count; i++) { if (global_music.layers[i].loaded) { ma_sound_uninit(&global_music.layers[i].sound); + ma_audio_buffer_uninit(&global_music.layers[i].buffer); global_music.layers[i].loaded = false; } } diff --git a/application/audio_sfx.h b/application/audio_sfx.h deleted file mode 100644 index 2c1216d..0000000 --- a/application/audio_sfx.h +++ /dev/null @@ -1,137 +0,0 @@ -#pragma once -#include -#include - -/* audio_sfx.h — One-shot SFX (impact stingers over music) - * - * Mirrors the Wwise Stinger_impacts trigger set from Music_Khushi. - * Non-looping ma_sound instances mixed over the layered music. - * Skips Wwise-only effects (pitch shifter) for v1. - */ - -#define SFX_MAX_SOUNDS 16 -#define SFX_MAX_SETS 4 -#define SFX_SET_MAX 8 - -typedef struct { - str_t name; - ma_sound sound; - bool loaded; -} sfx_sound_t; - -typedef struct { - str_t label; - u32 indices[SFX_SET_MAX]; - u32 count; - u32 next; -} sfx_set_t; - -typedef struct { - sfx_sound_t sounds[SFX_MAX_SOUNDS]; - u32 sound_count; - sfx_set_t sets[SFX_MAX_SETS]; - u32 set_count; - bool initialized; -} sfx_system_t; - -bool sfx_system_init(void); -u32 sfx_register(str_t name, str_t filepath, ma_engine *engine); -u32 sfx_set_create(str_t label); -void sfx_set_add(u32 set_id, u32 sound_id); -void sfx_play(u32 sound_id); -void sfx_play_from_set(u32 set_id); -void sfx_system_unload(void); - -#ifndef IGNORE_AUDIO_SFX_IMPLEMENTATION - -global sfx_system_t g_sfx = {0}; - -bool sfx_system_init(void) -{ - if (g_sfx.initialized) return true; - memset(&g_sfx, 0, sizeof(sfx_system_t)); - g_sfx.initialized = true; - return true; -} - -u32 sfx_register(str_t name, str_t filepath, ma_engine *engine) -{ - if (g_sfx.sound_count >= SFX_MAX_SOUNDS) { - eprint("[audio] sfx: max sounds reached\n"); - return (u32)-1; - } - - u32 id = g_sfx.sound_count++; - sfx_sound_t *s = &g_sfx.sounds[id]; - s->name = name; - - ma_result result = ma_sound_init_from_file(engine, filepath.data, - 0, NULL, NULL, &s->sound); - if (result != MA_SUCCESS) { - eprint("[audio] sfx: failed to load `%.*s`: %d\n", STR_ARG(filepath), result); - s->loaded = false; - return (u32)-1; - } - - s->loaded = true; - return id; -} - -u32 sfx_set_create(str_t label) -{ - if (g_sfx.set_count >= SFX_MAX_SETS) { - eprint("[audio] sfx: max sets reached\n"); - return (u32)-1; - } - u32 id = g_sfx.set_count++; - sfx_set_t *set = &g_sfx.sets[id]; - set->label = label; - set->count = 0; - set->next = 0; - return id; -} - -void sfx_set_add(u32 set_id, u32 sound_id) -{ - if (set_id >= g_sfx.set_count || sound_id >= g_sfx.sound_count) return; - sfx_set_t *set = &g_sfx.sets[set_id]; - if (set->count >= SFX_SET_MAX) return; - set->indices[set->count++] = sound_id; -} - -void sfx_play(u32 sound_id) -{ - if (sound_id >= g_sfx.sound_count) return; - sfx_sound_t *s = &g_sfx.sounds[sound_id]; - if (!s->loaded) return; - - ma_sound_set_volume(&s->sound, 1.0f); - ma_sound_seek_to_pcm_frame(&s->sound, 0); - ma_sound_start(&s->sound); -} - -void sfx_play_from_set(u32 set_id) -{ - if (set_id >= g_sfx.set_count) return; - sfx_set_t *set = &g_sfx.sets[set_id]; - if (set->count == 0) return; - - u32 idx = set->indices[set->next]; - set->next = (set->next + 1) % set->count; - sfx_play(idx); -} - -void sfx_system_unload(void) -{ - for (u32 i = 0; i < g_sfx.sound_count; i++) { - if (g_sfx.sounds[i].loaded) { - ma_sound_uninit(&g_sfx.sounds[i].sound); - g_sfx.sounds[i].loaded = false; - } - } - g_sfx.sound_count = 0; - g_sfx.set_count = 0; - g_sfx.initialized = false; -} - -#endif diff --git a/application/music_config.h b/application/music_config.h index ca2b13f..18c86ae 100644 --- a/application/music_config.h +++ b/application/music_config.h @@ -1,6 +1,6 @@ #pragma once #include "audio_music.h" -#include "audio_sfx.h" +#include "poglib/util/assetmanager.h" /* music_config.h — Wwise-project-derived music definitions (NO Wwise SDK) * @@ -10,7 +10,6 @@ * Maps 1:1 to Wwise authoring data: * - 4 layers (bed + 3 stems) at 80 BPM * - RTPC Parkour_intensity (0..100) → layer volume curves - * - Stinger_impacts set (Impact_KL_01..04) * * Edit this file when curves/assets change in Wwise authoring. * Do NOT parse .wwu / .wproj / SoundBanks at runtime. @@ -46,64 +45,56 @@ static const u32 MUSIC_LAYER_CURVE_COUNTS[] = { 2, 4, 4, 4, }; -static const str_t MUSIC_SFX_FILES[] = { - str_lit("sfx/Impact_KL_01.wav"), - str_lit("sfx/Impact_KL_02.wav"), - str_lit("sfx/Impact_KL_03.wav"), - str_lit("sfx/Impact_KL_04.wav"), -}; - -static const u32 MUSIC_SFX_COUNT = 4; - #ifndef IGNORE_MUSIC_CONFIG_IMPLEMENTATION -global u32 g_sfx_impact_set = (u32)-1; - -bool music_config_load(void) +bool music_config_load(assetmanager_t *assets) { ma_engine *engine = audio_device_get_engine(); if (!engine) return false; - if (!sfx_system_init()) return false; + music_audio_info_t audio_info[8] = {0}; + + for (u32 i = 0; i < MUSIC_LAYER_COUNT; i++) { + char fullpath[512]; + snprintf(fullpath, sizeof(fullpath), "%.*s/%.*s", + STR_ARG(MUSIC_BASE_PATH), STR_ARG(MUSIC_LAYER_FILES[i])); + + u32 asset_id = assetmanager_load_audio(assets, str(fullpath)); + if (asset_id == INVALID_ASSET_ID) { + eprint("[audio] music_config: failed to load `%s`\n", fullpath); + return false; + } + + const audio_asset_t *asset = assetmanager_get_assetresource( + assets, ASSET_TYPE_AUDIO, asset_id); + if (!asset) { + eprint("[audio] music_config: asset not ready `%s`\n", fullpath); + return false; + } + + audio_info[i] = (music_audio_info_t){ + .pcm_data = asset->pcm_data, + .frame_count = asset->frame_count, + .channels = asset->channels, + .sample_rate = asset->sample_rate, + }; + } - /* Load music layers */ bool ok = music_theme_load( - MUSIC_BASE_PATH, - MUSIC_LAYER_FILES, + audio_info, MUSIC_LAYER_COUNT, MUSIC_LAYER_CURVES, MUSIC_LAYER_CURVE_COUNTS ); if (!ok) return false; - /* Load impact SFX */ - g_sfx_impact_set = sfx_set_create(str("impacts")); - for (u32 i = 0; i < MUSIC_SFX_COUNT; i++) { - char fullpath[512]; - snprintf(fullpath, sizeof(fullpath), "%.*s/%.*s", - STR_ARG(MUSIC_BASE_PATH), STR_ARG(MUSIC_SFX_FILES[i])); - u32 id = sfx_register(MUSIC_SFX_FILES[i], str(fullpath), engine); - if (id != (u32)-1) { - sfx_set_add(g_sfx_impact_set, id); - } - } - - logging("[audio] music config loaded (Wwise hybrid)"); + logging("[audio] music config loaded (asset manager)"); return true; } -void music_config_play_impact(void) -{ - if (g_sfx_impact_set != (u32)-1) { - sfx_play_from_set(g_sfx_impact_set); - } -} - void music_config_unload(void) { music_theme_unload(); - sfx_system_unload(); - g_sfx_impact_set = (u32)-1; } #endif diff --git a/util/asset.h b/util/asset.h index 59403d3..de06828 100644 --- a/util/asset.h +++ b/util/asset.h @@ -7,6 +7,7 @@ typedef enum asset_type { ASSET_TYPE_GLSL_SHADER = 1, ASSET_TYPE_TEXTURE = 2, ASSET_TYPE_TEXTURE_SPRITE_ATLAS = 3, + ASSET_TYPE_AUDIO = 4, ASSET_TYPE_COUNT } asset_type; @@ -42,6 +43,14 @@ struct gpu_asset__internal_upload_task_t { void *processed_data; }; +typedef struct { + void *pcm_data; + u64 frame_count; + u32 format; + u32 channels; + u32 sample_rate; +} audio_asset_t; + #define INVALID_ASSET_ID 0 const bool ASSET_ASYNC_LOADING_SUPPORT[ASSET_TYPE_COUNT] = { @@ -49,5 +58,6 @@ const bool ASSET_ASYNC_LOADING_SUPPORT[ASSET_TYPE_COUNT] = { [ASSET_TYPE_GLSL_SHADER] = false, //Requires opengl to compile shaders [ASSET_TYPE_TEXTURE] = false, [ASSET_TYPE_TEXTURE_SPRITE_ATLAS] = false, + [ASSET_TYPE_AUDIO] = false, }; diff --git a/util/assetmanager.h b/util/assetmanager.h index 24e056b..d89041f 100644 --- a/util/assetmanager.h +++ b/util/assetmanager.h @@ -6,6 +6,7 @@ #include "poglib/basic/ds/hashtable.h" #include "poglib/basic/file.h" #include "poglib/basic/str.h" +#include "poglib/external/miniaudio.h" #include "poglib/gfx/gl/objects.h" #include "poglib/gfx/gl/renderconfig.h" #include "poglib/gfx/gl/shader.h" @@ -43,6 +44,7 @@ u32 assetmanager_load_model_async(assetmanager_t *self, const st u32 assetmanager_load_spriteatlas(assetmanager_t *const self, const str_t filepath, const u32 tile_count_width, const u32 tile_count_height); u32 assetmanager_load_texture(assetmanager_t *self, const str_t filepath); u32 assetmanager_load_glsl_shader(assetmanager_t *self, const str_t vtx_filepath, const str_t frag_filepath, const gluniform_registry_t registry); +u32 assetmanager_load_audio(assetmanager_t *self, const str_t filepath); queue_t * assetmanager_get_eventqueue(assetmanager_t *const self); const void * assetmanager_get_assetresource(const assetmanager_t *const self, const asset_type assettype, const u32 assetId); @@ -73,7 +75,7 @@ INTERNAL void assetmanager__internal_write_uniformlocs_to_file(const hashtable_e assetmanager_t assetmanager_init(bgtask_manager_t *const taskmanager) { ASSERT(taskmanager); - arena_t *arena = arena_init(NULL, 1 * MB); + arena_t *arena = arena_init(NULL, 64 * MB); assetmanager_t result = { .arena = arena, .assetmaps = { @@ -81,6 +83,7 @@ assetmanager_t assetmanager_init(bgtask_manager_t *const taskmanager) [ASSET_TYPE_GLSL_SHADER] = hashtable_init(MAX_ASSETS_ALLOWED_PER_TYPE, HT_KEY_TYPE_U32, (ht_value_type) { .size = sizeof(glshader_t), .type = HT_STORAGE_BY_REFERENCE }, arena), [ASSET_TYPE_TEXTURE] = hashtable_init(MAX_ASSETS_ALLOWED_PER_TYPE, HT_KEY_TYPE_U32, (ht_value_type) { .size = sizeof(gltexture2d_t), .type = HT_STORAGE_BY_REFERENCE }, arena), [ASSET_TYPE_TEXTURE_SPRITE_ATLAS] = hashtable_init(3, HT_KEY_TYPE_U32, (ht_value_type) { .size = sizeof(spriteatlas_t), .type = HT_STORAGE_BY_VALUE }, arena), + [ASSET_TYPE_AUDIO] = hashtable_init(MAX_ASSETS_ALLOWED_PER_TYPE, HT_KEY_TYPE_U32, (ht_value_type) { .size = sizeof(audio_asset_t), .type = HT_STORAGE_BY_VALUE }, arena), }, .bgtask_manager = taskmanager, .assetmeta_lookup = hashtable_init(MAX_ASSETS_ALLOWED_PER_TYPE * ASSET_TYPE_COUNT, HT_KEY_TYPE_U32, (ht_value_type){ .size = sizeof(asset_meta_t), .type = HT_STORAGE_BY_VALUE } , arena), @@ -115,6 +118,8 @@ void assetmanager__internal_assetmaps_destroy(assetmanager_t *const self) case ASSET_TYPE_TEXTURE_SPRITE_ATLAS: spriteatlas_destroy(entry->value); break; + case ASSET_TYPE_AUDIO: + break; default: eprint("asset type not implemented"); } } @@ -799,6 +804,63 @@ u32 assetmanager_load_texture(assetmanager_t *self, const str_t filepath) eprint("not implemented"); } +u32 assetmanager_load_audio(assetmanager_t *self, const str_t filepath) +{ + hashtable_iterator(&self->assetmaps[ASSET_TYPE_AUDIO], iter) + { + const hashtable_entry_t *const table_entry = iter; + const audio_asset_t *const asset = table_entry->value; + const asset_meta_t *const meta = (asset_meta_t *)hashtable_get_value( + &self->assetmeta_lookup, (hashtable_key_t){ .u32 = table_entry->key.u32 }); + if (str_cmp(meta->filepath1, filepath)) + return table_entry->key.u32; + } + + ma_decoder_config decoder_cfg = ma_decoder_config_init(ma_format_f32, 2, 48000); + + ma_decoder decoder; + char pathbuf[512]; + snprintf(pathbuf, sizeof(pathbuf), "%.*s", STR_ARG(filepath)); + ma_result result = ma_decoder_init_file(pathbuf, &decoder_cfg, &decoder); + if (result != MA_SUCCESS) { + eprint("[audio] assetmanager: failed to decode `%.*s`: %d\n", STR_ARG(filepath), result); + return INVALID_ASSET_ID; + } + + ma_uint64 total_frames = 0; + result = ma_decoder_get_length_in_pcm_frames(&decoder, &total_frames); + if (result != MA_SUCCESS || total_frames == 0) { + eprint("[audio] assetmanager: empty or unreadable `%.*s`\n", STR_ARG(filepath)); + ma_decoder_uninit(&decoder); + return INVALID_ASSET_ID; + } + + f32 *pcm = arena_reserve(self->arena, (size_t)(total_frames * decoder_cfg.channels * sizeof(f32))); + ma_uint64 frames_read = 0; + result = ma_decoder_read_pcm_frames(&decoder, pcm, total_frames, &frames_read); + ma_decoder_uninit(&decoder); + + if (result != MA_SUCCESS) { + eprint("[audio] assetmanager: pcm read failed `%.*s`: %d\n", STR_ARG(filepath), result); + return INVALID_ASSET_ID; + } + + const u32 asset_id = ++self->internal.asset_idx_generator; + audio_asset_t asset = { + .pcm_data = pcm, + .frame_count = frames_read, + .format = (u32)decoder_cfg.format, + .channels = decoder_cfg.channels, + .sample_rate = decoder_cfg.sampleRate, + }; + + hashtable_insert(&self->assetmaps[ASSET_TYPE_AUDIO], (hashtable_key_t){ .u32 = asset_id }, &asset); + assetmanager__internal_add_asset_meta_data(self, ASSET_TYPE_AUDIO, asset_id, filepath, STR_EMPTY, NULL); + + logging("[audio] assetmanager: loaded `%.*s` (%llu frames)", STR_ARG(filepath), frames_read); + return asset_id; +} + queue_t * assetmanager_get_eventqueue(assetmanager_t *const self) {