Audio¶
This page documents the audio system: SFX IDs, usage hotspots, data labels, and the runtime entry struct shared by SFX and music.
Playback rules¶
Recovered sfx_play and sfx_play_panned reject a sound while that native ID's
cooldown is positive: 0.05 seconds normally, 0.44 seconds for either flamer ID.
audio_update subtracts frame_dt_copy after playback requests, retaining a
negative zero-crossing remainder. Sound selection has already consumed RNG
before this admission check. There is no global four-event cutoff.
Panned playback computes ((camera_x + position_x) / screen_width - 0.5) * 1700,
truncates to an integer, and clamps to ±10000 DirectSound hundredths of a dB.
Positive values attenuate the left channel; negative values attenuate the right.
Demo playback multiplies the requested volume by 0.7. The volume setter maps
the scalar through (volume + 2) * (1 / 3) before converting it to DirectSound
attenuation. These operations use native float32 constants and x87 PC=24;
__ftol at 0x00461054 sets truncation mode before converting, including the
Reflex Boost playback frequency calculation.
The Python backend converts channel attenuation to raylib 5.5's pan law in
src/grim/audio_math.py. It retains the port's explicit zero-volume mute and
per-voice gain state. Decoded sample ownership is shared by asset, while
cooldowns remain separate for distinct native IDs. The port keeps its bounded
voice pool rather than reproducing DirectSound's 16 buffers per native entry.
Music keeps selection and mute/fade intent even at zero master volume. The
Python backend pauses streams at zero and resumes them without choosing a new
tune. Stopped unmuted streams are started explicitly. Game-over and quest-failed
screens repeatedly request shortie_monk while active; quest completion starts
crimsonquest with a fade-in. A previously muted track must finish fading before
an exclusive-play request can unmute it.
Python resource loading validates every decoded handle before publication and rolls back partial acquisition. An owned sample releases aliases before its source; a music entry retains encoded bytes until decoder teardown. Audio shutdown closes the device only when that state opened it.
See the deterministic session
for deferred sound requests and cooldown timing. The backend conversion follows
raylib 5.5 MixAudioFrames
and the DirectSound pan
and volume
contracts.
Entry struct (audio_entry_t)¶
SFX and music tracks share the same 0x84-byte entry layout. The runtime uses
this struct for both sfx_entry_table and music_entry_table.
Offsets are relative to the entry base and match the initialization logic in
wav_parse_into_entry, sfx_entry_load_ogg, and music_entry_load_ogg.
| Offset | Size | Field | Notes |
|---|---|---|---|
| 0x00 | u16 | format_tag | Set to 1 (PCM) for WAV and OGG decode. |
| 0x02 | u16 | channels | Written from WAV header / Vorbis info. |
| 0x04 | u32 | sample_rate | nSamplesPerSec. |
| 0x08 | u32 | avg_bytes_per_sec | nAvgBytesPerSec. |
| 0x0c | u16 | block_align | nBlockAlign. |
| 0x0e | u16 | bits_per_sample | wBitsPerSample (usually 16). |
| 0x10 | u16 | cb_size | Extra size (usually 0). |
| 0x12 | u16 | _pad | Alignment. |
| 0x14 | ptr | pcm_data | Heap buffer holding decoded PCM. |
| 0x18 | u32 | pcm_bytes | Size of pcm_data / stream buffer in bytes. |
| 0x1c | u32 | stream_cursor | Current cursor used by streaming refill logic. |
| 0x20 | f32 | volume | Cached volume scalar. |
| 0x24 | ptr[16] | buffers | DirectSound buffer pointers (primary + 15 duplicates). |
| 0x64 | u8[16] | buffer_in_use | Voice-active flags set during playback. |
| 0x74 | ptr | vorbis_stream | Non-null for streaming music entries. |
| 0x78 | u32 | stream_fill_bytes | Bytes remaining in the current stream chunk. |
| 0x7c | u32 | stream_total_bytes | Accumulated bytes written to the stream. |
| 0x80 | u32 | stream_cursor_bytes | Cursor used to trigger refill in music_stream_update. |
Notes:
-
Static/one-shot SFX entries leave
vorbis_streamnull and use the 16 voice buffers at0x24for overlapping playback. -
Music tracks set
vorbis_streamand use a single streaming buffer at0x24. -
The entry size is 0x84 bytes; table strides for
sfx_entry_tableandmusic_entry_tableuse this size. -
sfx_voice_table(0x004c3e80) is a 0x20-entry table of DirectSound buffer pointers; it is only zeroed duringsfx_system_init, so the role remains inferred.
Memory-backed Vorbis stream¶
The native vorbis_stream_t is now recovered through its complete leaf
surface. Its leading layout is an ov_callbacks record at +0x00, an
OggVorbis_File at +0x10, the current bitstream index at +0x2e0, total PCM
bytes at +0x2e4, source-data offset at +0x2e8, the owned source allocation
at +0x2ec, and a copied 32-byte vorbis_info at +0x2f0.
The owned allocation has an eight-byte prefix followed by inline OGG data:
| Offset | Type | Meaning |
|---|---|---|
| 0x00 | u32 | Source byte size. |
| 0x04 | long | Callback cursor. |
| 0x08 | u8[] | OGG bytes passed to ov_open_callbacks. |
The callback policy contains several native quirks that ports should retain:
- reads reset an exhausted cursor to zero, clamp the copy to remaining bytes,
but advance the cursor by the full requested
size * count; - seek returns
1for every origin, treats non-SEEK_SET/SEEK_ENDorigins as relative, and implements end-relative seek assize - offset; - the callback close is a no-op returning
1;vorbis_stream_t::closeowns the allocation and frees it beforeov_clear; - PCM reads request signed little-endian 16-bit output and clamp negative
ov_readresults to zero.
Opening copies vorbis_info, calculates PCM bytes as
ov_pcm_total * channels * 16 / 8, and records the callback cursor after the
Vorbis headers have been consumed.
SFX ID map¶
Derived from audio_init_sfx (0x0043caa0). sfx_load_sample (0x0043c740)
returns the SFX id for each .ogg file and stores it in a global variable.
Labels are mirrored into analysis/ghidra/maps/data_map.json and applied with
ApplyDataMap.java during headless analysis. Duplicate IDs that point at the
same .ogg file use _alt suffixes to keep names unique.
| Var | File | Label | Refs |
|---|---|---|---|
| 0x004c3f00 | trooper_inPain_01.ogg | sfx_trooper_inpain_01 | 6 |
| 0x004c3f04 | trooper_inPain_02.ogg | sfx_trooper_inpain_02 | |
| 0x004c3f08 | trooper_inPain_03.ogg | sfx_trooper_inpain_03 | |
| 0x004c3f0c | trooper_die_01.ogg | sfx_trooper_die_01 | 3 |
| 0x004c3f10 | trooper_die_02.ogg | sfx_trooper_die_02 | |
| 0x004c3f14 | trooper_die_03.ogg | sfx_trooper_die_03 | |
| 0x004c3f18 | zombie_die_01.ogg | sfx_zombie_die_01 | |
| 0x004c3f1c | zombie_die_02.ogg | sfx_zombie_die_02 | |
| 0x004c3f20 | zombie_die_03.ogg | sfx_zombie_die_03 | |
| 0x004c3f24 | zombie_die_04.ogg | sfx_zombie_die_04 | |
| 0x004c3f28 | zombie_attack_01.ogg | sfx_zombie_attack_01 | |
| 0x004c3f2c | zombie_attack_02.ogg | sfx_zombie_attack_02 | |
| 0x004c3f30 | alien_die_01.ogg | sfx_alien_die_01 | |
| 0x004c3f34 | alien_die_02.ogg | sfx_alien_die_02 | |
| 0x004c3f38 | alien_die_03.ogg | sfx_alien_die_03 | |
| 0x004c3f3c | alien_die_04.ogg | sfx_alien_die_04 | |
| 0x004c3f40 | alien_attack_01.ogg | sfx_alien_attack_01 | |
| 0x004c3f44 | alien_attack_02.ogg | sfx_alien_attack_02 | |
| 0x004c3f48 | lizard_die_01.ogg | sfx_lizard_die_01 | |
| 0x004c3f4c | lizard_die_02.ogg | sfx_lizard_die_02 | |
| 0x004c3f50 | lizard_die_03.ogg | sfx_lizard_die_03 | |
| 0x004c3f54 | lizard_die_04.ogg | sfx_lizard_die_04 | |
| 0x004c3f58 | lizard_attack_01.ogg | sfx_lizard_attack_01 | |
| 0x004c3f5c | lizard_attack_02.ogg | sfx_lizard_attack_02 | |
| 0x004c3f60 | spider_die_01.ogg | sfx_spider_die_01 | 3 |
| 0x004c3f64 | spider_die_02.ogg | sfx_spider_die_02 | 3 |
| 0x004c3f68 | spider_die_03.ogg | sfx_spider_die_03 | 3 |
| 0x004c3f6c | spider_die_04.ogg | sfx_spider_die_04 | 3 |
| 0x004c3f70 | spider_attack_01.ogg | sfx_spider_attack_01 | 3 |
| 0x004c3f74 | spider_attack_02.ogg | sfx_spider_attack_02 | 3 |
| 0x004c3f78 | pistol_fire.ogg | sfx_pistol_fire | |
| 0x004c3f7c | pistol_reload.ogg | sfx_pistol_reload | 3 |
| 0x004c3f80 | shotgun_fire.ogg | sfx_shotgun_fire | 3 |
| 0x004c3f84 | shotgun_reload.ogg | sfx_shotgun_reload | |
| 0x004c3f88 | autorifle_fire.ogg | sfx_autorifle_fire | 3 |
| 0x004c3f8c | autorifle_reload.ogg | sfx_autorifle_reload | 3 |
| 0x004c3f90 | gauss_fire.ogg | sfx_gauss_fire | 3 |
| 0x004c3f98 | hrpm_fire.ogg | sfx_hrpm_fire | |
| 0x004c3f9c | shock_fire.ogg | sfx_shock_fire | 6 |
| 0x004c3fa0 | plasmaMinigun_fire.ogg | sfx_plasmaminigun_fire | 3 |
| 0x004c3fa4 | plasmaShotgun_fire.ogg | sfx_plasmashotgun_fire | 2 |
| 0x004c3fa8 | pulse_fire.ogg | sfx_pulse_fire | 2 |
| 0x004c3fac | flamer_fire_01.ogg | sfx_flamer_fire_01 | 7 |
| 0x004c3fb0 | flamer_fire_02.ogg | sfx_flamer_fire_02 | 3 |
| 0x004c3fb4 | shock_fire.ogg | sfx_shock_fire_alt | 9 |
| 0x004c3fb8 | shockMinigun_fire.ogg | sfx_shockminigun_fire | 2 |
| 0x004c3fbc | shock_reload.ogg | sfx_shock_reload | 2 |
| 0x004c3fc0 | rocket_fire.ogg | sfx_rocket_fire | 4 |
| 0x004c3fc4 | rocketmini_fire.ogg | sfx_rocketmini_fire | 2 |
| 0x004c3fc8 | autorifle_reload.ogg | sfx_autorifle_reload_alt | 5 |
| 0x004c3fcc | bullet_hit_01.ogg | sfx_bullet_hit_01 | 2 |
| 0x004c3fd0 | bullet_hit_02.ogg | sfx_bullet_hit_02 | |
| 0x004c3fd4 | bullet_hit_03.ogg | sfx_bullet_hit_03 | |
| 0x004c3fd8 | bullet_hit_04.ogg | sfx_bullet_hit_04 | |
| 0x004c3fdc | bullet_hit_05.ogg | sfx_bullet_hit_05 | |
| 0x004c3fe0 | bullet_hit_06.ogg | sfx_bullet_hit_06 | |
| 0x004c3fe4 | shock_hit_01.ogg | sfx_shock_hit_01 | 5 |
| 0x004c3fe8 | explosion_small.ogg | sfx_explosion_small | |
| 0x004c3fec | explosion_medium.ogg | sfx_explosion_medium | 4 |
| 0x004c3ff0 | explosion_large.ogg | sfx_explosion_large | 4 |
| 0x004c3ff4 | shockwave.ogg | sfx_shockwave | 6 |
| 0x004c3ff8 | questHit.ogg | sfx_questhit | 2 |
| 0x004c3ffc | ui_bonus.ogg | sfx_ui_bonus | 4 |
| 0x004c400c | ui_buttonClick.ogg | sfx_ui_buttonclick | 5 |
| 0x004c4010 | ui_panelClick.ogg | sfx_ui_panelclick | 2 |
| 0x004c4014 | ui_levelUp.ogg | sfx_ui_levelup | 2 |
| 0x004c4018 | ui_typeClick_01.ogg | sfx_ui_typeclick_01 | 4 |
| 0x004c401c | ui_typeClick_02.ogg | sfx_ui_typeclick_02 | |
| 0x004c4020 | ui_typeEnter.ogg | sfx_ui_typeenter | 5 |
| 0x004c4024 | ui_clink_01.ogg | sfx_ui_clink_01 | 4 |
| 0x004c4028 | bloodSpill_01.ogg | sfx_bloodspill_01 | 3 |
| 0x004c402c | bloodSpill_02.ogg | sfx_bloodspill_02 |
Aliases¶
Alias entries are direct copies of another id in the init function.
| Var | Copies | Label |
|---|---|---|
| 0x004c4000 | sfx_trooper_inpain_01 | sfx_trooper_inpain_01_alias_0 |
| sfx_jinxed_kill | sfx_trooper_inpain_01 | sfx_trooper_inpain_01_alias_1 |
| 0x004c4008 | sfx_trooper_inpain_01 | sfx_trooper_inpain_01_alias_2 |
Playlist/runtime globals¶
Recovered data labels for exclusive music-play path:
music_playlist_entry_count(0x004cc8d0): number of queued entries inmusic_playlist(written bymusic_queue_track).music_playlist_randomized_latch(0x004cc8d4): latch used bysfx_play_exclusive(music_track_extra_0)to avoid re-randomizing every call.audio_assets_loaded_count(0x004cc8d8): increments onsfx_load_sample/music_load_track; displayed on startup loading UI ("Grim SFX: %d/%d"line).
Runtime callsite clusters (2026-02-06 capture)¶
From analysis/frida/gameplay_state_capture_summary.json (694s gameplay-heavy run):
quest_results_screen_update->sfx_play_exclusive(5)in 3989/3989 calls.game_over_screen_update->sfx_play_exclusive(1)in 714/716 calls.quest_failed_screen_update->sfx_play_exclusive(1)in 482/483 calls.ui_button_update->sfx_play(63)in 38/38 calls.ui_element_update-> mostlysfx_play(64)(95/108) with secondary63(13/108).
Gameplay-heavy panned clusters (same capture):
player_update: dominant IDs34and37(plus30), matching weapon-fire cadence.projectile_update: dominant IDs54/51/53/55/52/50(projectile hit/explosion family).player_take_damage: IDs0/2/1(player hurt variants).player_start_reload: mostly ID35(15/20).bonus_apply: dominantsfx_playID62; panned side effects include56/59/60.weapon_assign_player: panned cluster led by31with secondary35/49.
Unreferenced entries (in sfx.paq)¶
These files exist in sfx.paq, but are not loaded by audio_init_sfx in 1.9.93:
flamer_fire_start.oggtrooper_die_04.oggtrooper_inPain_04.ogg