"Deep audit of the M44 audio subsystem — kira backend, spatial sub-tracks, listener/unit seam, reverb send, underwater filter, streaming music — plus its engine callers (footsteps, water audio, reverb zones, REGN ambient music)"
Install
npx skillscat add matiaszanolli/byroredux/claude-commands-audit-audio Install via the SkillsCat registry.
Audio Subsystem Audit (M44)
Read .claude/commands/_audit-common.md (delta-first scoping, dedup, finding format) and _audit-severity.md for shared protocol.
Audits crates/audio/ (kira 0.12, default-features = false with an explicit feature list in the workspace Cargo.toml — cpal, wav+pcm, ogg+vorbis, mp3, flac; 0.12 split container from codec features, so dropping a codec silently stops decoding; src/lib.rs + src/tests.rs) and the engine systems that are the ONLY callers of its API. Orchestrator; one Task agent per dimension (max 3 concurrent). Verify the shipped surface against the lib.rs module docstring and docs/feature-matrix.md (Audio); if lib.rs splits, re-derive each dimension's paths first.
Scope
- Crate:
AudioWorld(Option<AudioManager>resource;headless()is the no-device constructor),AudioListener/AudioEmitter/OneShotSoundcomponents,Attenuation,audio_system,SoundCache,VoiceSoundCache,load_*sound*,with_start_delay. - Engine side:
byroredux/src/systems/audio.rs(footstep_system,impact_audio_system,reverb_zone_system,water_audio_system), the combat-feedback sound half ofbyroredux/src/systems/combat_anim.rs(play_oneshot_cached; take/animation half is/audit-gameplay),byroredux/src/systems/dialogue_voice.rs(voice consumer),SoundCache/VoiceSoundCacheboot inserts (boot/world.rs) + telemetry (ownership_sample.rs), the footstep / impact / water-audio /RegionAmbientResresources inbyroredux/src/components.rs(FootstepConfig,ImpactConfig,WaterAudioConfig),byroredux/src/asset_provider/{audio,texture}.rs(dispatch_region_ambient_music,SoundArchiveProvider,sound_is_folder,try_load_default_footstep), wired fromcell_loader/load.rsandscene::apply_cell_region_ambient. - Handoffs: water state (
SubmersionState, splash/ripple production) is/audit-physicsDim 5; stage/lock shape is/audit-ecs//audit-concurrency(report audio-specific ordering only).
Not shipped as of 2026-10-12 (re-verify against docs/feature-matrix.md; audit the mechanism that will carry them): FOOT/IPDS/IPCT records → per-material footstep and clutter-impact content (the Wave-4 mechanism is live — Havok surface_material u32 → FootstepConfig/ImpactConfig surface_sounds — but both maps and ImpactConfig::default_sound ship empty, so impact_audio_system is a production no-op and footsteps always take the default), CELL-level MUSC routing (default_music/music_type_form are parsed; every construction site still hardcodes music_type_form: None), per-cell acoustics beyond binary interior/exterior reverb, occlusion, REGN incidental/ambient-loop selection (#3301 open), Skyrim+ .fuz dialogue voice and Oblivion voice (#5464, #5526).
Known-open (verified 2026-10-12): #3816 (MUSC/MUST/MSET/RDMD decode: REGN music_form never resolves as a SOUN on any supported game, so the dispatch mechanism is live and tested but plays nothing in production; audit the mechanism, cite #3816 for the gap). #3086 (entity-path spatial position frozen at dispatch) is closed (b4f08089b): sync_emitter_positions now follows the source GlobalTransform (Dim 1) — a frozen position is a regression. #4146 (reverb-zone ordering comment drift) is closed — a third drift is a regression. #5432 is closed (40b77828b): AudioWorld::stop_sounds_for (#5410) now has production callers through dialogue_voice::stop_line_voice — topic change, conversation close and speaker death cut the line (Dim 1/5); a new line-ending path that skips it is the regression. #5148 is closed (7208af67c): combat.status reports per-kind request counts (CombatSoundRequests: swing / impact / death_voice, counted only when play_oneshot_cached actually requested a one-shot) and the Skyrim P2 gate requires >= 1 impact + >= 1 death-voice, so oneshots_requested is no longer the gate's only signal. #4743 (combat-sound keying to take-state transitions) is still open.
Parameters
--focus <dims> (default all 5) · --depth shallow|deep (deep traces per-frame data flow + lifecycle).
Extra field: Dimension: Spatial Dispatch & Listener | Send Graph (Reverb + Underwater) | Music & SoundCache | Manager & ECS Lifecycle | Engine Consumers.
Phase 1: Setup
mkdir -p /tmp/audit/audio; dedup per_audit-common.md; read the latestdocs/audits/AUDIT_AUDIO_*.md(date D = delta baseline). A re-flag of a closed finding is a regression claim: check its guard is gone before reporting.cargo test -p byroredux-audioandcargo test -p byroredux -- audio dialogue_voice; record counts. The lifecycle guards are#[ignore]d (need an audio device, some FNV data):looping_emitter_survives_natural_duration_and_stops_on_emitter_remove,non_looping_emitter_stops_on_emitter_remove_regression_858,play_music_looping_survives_track_end,play_music_drives_streaming_playback_on_real_ogg,emitter_position_follows_the_source_entity_regression_3086,anchored_voice_follows_survives_prune_and_stops, plus the playback/decode trioplay_oneshot_queue_drives_real_playback,audio_system_full_lifecycle_on_real_fnv_sound,real_fnv_sounds_decode_through_kira. Despawn truncation, follow, anchored-voice stop and music looping are unguarded in the default lane: run-- --ignoredor say so.
Phase 2: Dimensions
Dim 1: Spatial Dispatch, Listener & the Unit Seam
Paths: crates/audio/src/lib.rs (dispatch_new_oneshots, drain_pending_oneshots, prune_stopped_sounds, sync_listener_pose, sync_emitter_positions, bu_to_audio_space, linear_volume_to_db, ActiveSound, SoundSource, play_oneshot_following, stop_sounds_for)
First step: git log --since=D -- crates/audio/src/lib.rs
Guards (default lane): tests.rs::{every_kira_position_site_goes_through_the_unit_seam, emitter_at_max_distance_in_bu_lands_on_max_distance_in_audio_space, drain_pending_oneshots_drains_in_place_instead_of_stranding_capacity, play_oneshot_queue_caps_at_max_pending_when_active, oneshot_marker_is_consumed_on_both_dispatch_failure_arms, with_start_delay_sets_the_start_time_and_shares_frames, play_oneshot_following_drops_on_inactive_audio, stop_sounds_for_is_a_no_op_on_a_headless_world}; #[ignore]d device-gated emitter_position_follows_the_source_entity_regression_3086 (#3086) and anchored_voice_follows_survives_prune_and_stops (#5410).
- Two dispatch paths stay observably equivalent: entity path (
OneShotSound+AudioEmitter→ per-emitterSpatialTrackHandle,SoundSource::Emitter, marker removed,AudioEmitterkept) and queue path (PendingOneShot, never loops:play_oneshot→SoundSource::Detached, orplay_oneshot_following→SoundSource::Anchored(entity), #5410 d1a7ee8c6). Both early-return without a listener id.stop_sounds_for(entity, fade_ms)fades every playingEmitter/Anchoredsound of the entity and drops its queued segments; its production caller isdialogue_voice::stop_line_voice(150 ms fade;apply_selection,end_open_conversation,reconcile_dead_actor; #5432). The entity path has NO engine producer —spawn_oneshot_atis test/tooling-only, so its dispatch is exercised bytests.rsalone (#5612 81207d34d). ActiveSound::trackis held for its Drop side effect AND carries the #3086 position pushes (renamed from_track; the track must still be kept alive): it must land inactive_soundsbefore the helper returns.Arc<StaticSoundData>is shared, never deep-cloned; volume applies via(*sound).clone().volume(db).oneshots_requestedis a monotonic count ofplay_oneshot/play_oneshot_followingcalls including headless discards (incremented before the manager gate; read bycombat.statusincommands/view.rsfor device-less smoke gates) — it proves a request, never audible playback.- Producer queue caps at 256 (in both
play_oneshot*entry points, each with its ownMAX_PENDING) with O(1)pop_frontand drops up front when the manager isNone; the manager-active gate precedes the drain; a drain of more than 32 warns once per tick (footstep tempo gone wrong).linear_volume_to_dbis the single dB conversion (floorSILENCE_DB-60). - Entity path reads post-propagation
GlobalTransform, notTransform. World is Bethesda units (70/m), kira is metres:bu_to_audio_spaceis the only seam, applied to the listener pose and both dispatch paths;Attenuationis authored in metres (min <= max,RangeInclusive) and callers must never pre-scale by 70. A missed site is silent (inaudible past ~43 cm, no log);every_kira_position_site_goes_through_the_unit_seamscans the listener sites, both dispatch paths and (since #5411, bb31c5fb3) the follow site, with a negative on rawset_position. - Emitter follow (#3086, b4f08089b):
sync_emitter_positions(pass 2, after the listener sync) pushes the sub-track of everyActiveSoundwhoseSoundSource::followed_entity()is set (EmitterandAnchored) to its source's currentGlobalTransformthrough the samebu_to_audio_space; change-gated onActiveSound::last_position(seeded at dispatch on both paths) so a stationary emitter costs one compare and zero kira commands;Detachedqueue sounds and music are never touched; a despawned/transform-less entity is skipped (prune ownsEmitter,stop_sounds_forownsAnchored).AudioWorld::emitter_position_updates()is the observable (kira has no position readback). The behaviour guards are#[ignore]d (device) — trace the code in the default lane. - Listener: created lazily on the first frame an
AudioListenerhas a resolvedGlobalTransform; the handle is sticky (never cleared on despawn: kira listener capacity is 8, a clear/re-add loop exhausts it); a failedadd_listenerretries next frame; multi-listener warns once and uses the first. Pose usesTween::default()(smooth follow); orientation is already renderer-space (no second Z-up conversion).
Output:/tmp/audit/audio/dim_1.md
Dim 2: Send Graph: Reverb Send & Underwater Filter
Paths: crates/audio/src/lib.rs (AudioWorld::new, apply_reverb_send, reverb_send_gate_open, set_reverb_send_db, apply_underwater_filter, update_underwater_filters, set_underwater)
First step: git log --since=D -- crates/audio/src/lib.rs (send/filter regions)
Guards: tests.rs::{reverb_send_defaults_to_silent, reverb_send_gate_matches_silence_db_boundary, above_water_filter_state_is_a_dry_bypass, above_water_cutoff_never_reaches_the_kira_clamp_ceiling, both_dispatch_paths_build_the_filter_through_one_call_site}.
- One global reverb send is created in
AudioWorld::newfromREVERB_FEEDBACK/REVERB_DAMPING/REVERB_STEREO_WIDTH(0.85/0.6/1.0). They have no recorded provenance and are not kira's defaults: flag the doc gap, never invent a rationale.reverb_send: Nonenever cascades into anunwrap(). - Send opt-in is build-time only (
with_sendat track construction, gatedis_finite() && > -60 dB) and both paths use the shared helper. Default levelNEG_INFINITY(dry). Playing sounds keep their construction-time level: the documented "next-dispatch knob" contract, not a bug. - Every spatial sub-track carries a low-pass built by ONE shared
apply_underwater_filter;update_underwater_filtersretargets live tracks when the listener's submersion changes. Above water is a genuine bypass viaMix::DRY(a defaultFilterBuilderis fully wet and colours the signal); the parked 20 kHz cutoff stays below kira's Nyquist clamp (beyond it the filter degenerates rather than turning transparent).set_underwateris written each frame bywater_audio_system(Dim 5), beforeaudio_system.
Output:/tmp/audit/audio/dim_2.md
Dim 3: Streaming Music & SoundCache
Paths: crates/audio/src/lib.rs (play_music, stop_music, is_music_active, load_streaming_sound_from_*, SoundCache, VoiceSoundCache)
First step: git log --since=D -- crates/audio/src/lib.rs
Guards: tests.rs::{play_music_no_op_when_inactive, sound_cache_hits_are_case_insensitive_and_share_arc, sound_cache_get_or_load_invokes_loader_only_on_miss, sound_cache_clear_drops_entries_and_bytes_estimate_tracks_pcm_size, voice_cache_evicts_lru_under_byte_budget, voice_cache_negatives_are_capped_and_counted}.
- Single music slot:
play_music(streaming_sound, volume, fade_in_secs, looping)(4 parameters,loopingsince #3775; re-verify the arity before citing) crossfades any existing handle. Music routes through the main track, never a spatial sub-track. It usesStreamingSoundData(from_cursor/from_file), neverStaticSoundData(full PCM decode = OOM on long tracks). stop_musicfades forfade_out_secs.max(0.0)then drops the handle (an instant stop clicks);is_music_activegoes false the momentstop_musicreturns (the slot is cleared while kira renders the fade tail): documented, so a caller must not read it as "audibly occupied".SoundCachekeys lowercase once per entry point;clear()never invalidatesArcs held by liveActiveSounds. It is live since 2026-09-27: inserted at boot (boot/world.rs), sampled byownership_sample.rs(len()+bytes_estimate()), and its only consumer iscombat_anim.rs::play_oneshot_cached(three fixed combat paths; voice moved toVoiceSoundCache). Footstep/splash loaders still bypass it intoFootstepConfig/WaterAudioConfig. The map isOption<Arc<_>>:Noneis a negative-cache entry (loader returned no bytes, or decode failed) thatget_or_loadnever re-probes andlen()/bytes_estimate()exclude — so a sound missing at first use stays missing untilclear(), which nothing in production calls (check whether the--sounds-bsaset can change at runtime). Still no eviction: a consumer keyed on open-ended paths (FOOT, REGN, per-NPC SFX) that lands onSoundCacheis the finding.VoiceSoundCache(#5382, 8410ad1e5) is the open-ended-key cache, used only by dialogue voice: sameget_or_loadcontract, LRU-evicted pastDEFAULT_BUDGET_BYTES(64 MiB; the just-inserted entry is never its own victim), negatives capped atVOICE_NEGATIVE_CAP(4096; the set clears and re-probes past it) and counted (negative_len). Telemetry reportsvoice_cache_{entries,bytes,negatives}throughownership_sample.rs; the save registry allowlists it. A new open-ended consumer belongs here (or in its own bounded cache), not inSoundCache.
Output:/tmp/audit/audio/dim_3.md
Dim 4: Manager, ECS Lifecycle & Schedule
Paths: crates/audio/src/lib.rs (struct, AudioWorld::new/headless, audio_system, prune_stopped_sounds), byroredux/src/boot/schedule/late.rs, byroredux/src/cell_loader/{load,unload}.rs
First step: git log --since=D -- crates/audio/src/lib.rs byroredux/src/boot/schedule/late.rs
Guards: tests.rs::{audio_world_constructs_without_panic_on_any_environment, explicit_headless_world_discards_playback_without_retaining_sound, manager_capacities_exceed_kira_defaults}; scheduler_access_tests.rs::{footstep_runs_after_camera_follow_in_late, reverb_zone_is_parallel_and_audio_is_exclusive_in_late}.
- Graceful degradation:
AudioManager::newfailure leavesmanager = None; every public API gates on it with nounwrap(); headless/CI boot must succeed (report as PASS/FAIL in the summary).AudioWorld::headless()never contacts a device; tests use it, not the device-dependentdefault(). SUB_TRACK_CAPACITY(512) andSEND_TRACK_CAPACITY(32) exceed kira's defaults and are applied innew().- Field-drop order (single source of truth):
active_sounds→pending_oneshots→ (oneshots_requested) →music→reverb_send→reverb_send_db→listener→manager, then the trailingmulti_listener_warned/underwaterflags and theemitter_position_updatescounter. A reorder that movesmanagerup asserts in kira's Drop.AudioWorld::new()is boot-only, never on cell transition or resize. - Schedule (all
Stage::Late):audio_systemis an exclusive registered afterfootstep_system(exclusive, pinned to Late by #4185's test) andwater_audio_system(exclusive, aftersubmersion_system);reverb_zone_systemis a parallel registration, and the parallel batch finishing before any exclusive (not registration order) is what sets the send level before new tracks are built.audio_systembody order:sync_listener_pose→sync_emitter_positions→update_underwater_filters→drain_pending_oneshots→dispatch_new_oneshots→prune_stopped_sounds. The voice producer (Latenpc_dialogue_selection_system) registers AFTERaudio_system, so a line's segments dispatch on the next frame — one frame of latency, not a loss. - Despawn truncation: only
SoundSource::Emitterentries are emitter-coupled (emitter_coupled()): an entity that losesAudioEmittergets a tweenedstop()overActiveSound.unload_fade_ms(10 ms default, looping and not);stop_issueddebounces (and is set bystop_sounds_fortoo); retain drops only onStopped.Detachedruns to natural end;Anchoredsurvives SFX emitter churn on the same NPC by design and stops only viastop_sounds_for— death goes throughreconcile_dead_actor, but an entity that is merely despawned (cell unload mid-line) is not routed there, so its voice keeps playing at its last position until the segment ends (check before filing: #5432 covers topic change, close and death only). Guards are#[ignore]d (Phase 1): trace the code. - Cross-cell continuity:
dispatch_region_ambient_musicruns only whenmusic_formchanged (bothcell_loader/load.rspaths andscene::apply_cell_region_ambientcompare against the priorRegionAmbientRes); an unconditional redispatch audibly restarts the track on every connected-cell crossing. A future CELL-level MUSC caller needs its own gate.
Output:/tmp/audit/audio/dim_4.md
Dim 5: Engine-Side Consumers
Paths: byroredux/src/systems/{audio,dialogue_voice}.rs, byroredux/src/components.rs (footstep/water-audio/region resources), byroredux/src/asset_provider/{audio,texture}.rs, byroredux/src/scene.rs
First step: git log --since=D -- byroredux/src/systems/audio.rs byroredux/src/systems/dialogue_voice.rs byroredux/src/asset_provider/audio.rs
Guards (systems/audio.rs tests): first_tick_seeds_last_position_without_firing, stride_threshold_is_bethesda_units_not_metres, walking_for_one_second_fires_a_walking_cadence_not_one_per_frame, single_large_jump_fires_one_footstep_only, water_splash_event_reaches_audio_dispatcher, interior_to_exterior_transition_resets_send_to_dry, the #5146 trio orbiting_camera_with_stationary_body_fires_nothing / swimming_body_is_silent_and_exit_does_not_replay_the_swim_distance / airborne_body_is_silent_and_landing_does_not_replay_the_air_distance, plus the no_*_is_safe_noop pair. Highest-yield dimension in the 2026-08/09 reports.
- Callers:
footstep_system,impact_audio_system(Late exclusive registered right afterfootstep_system: drainsPhysicsWorld::take_contact_force_events— thestartedforce crossings, belowImpactConfig::min_force_magnitudeskipped — and plays one{1.0, 24.0}m one-shot per impact at the pair midpoint; material =resolve_impact_material(collider 1's non-zero, else collider 2's, else 0 while either handle lives, #5549);impact_sounds_to_playskips only an unmapped impact, neverbreaks the frame, #5554; the whole system returns early with nodefault_soundand nosurface_sounds),water_audio_systemandcombat_anim.rs::play_oneshot_cached(PostUpdate, enqueued before Lateaudio_system) are liveplay_oneshotcallers, withbyroredux/src/systems/dialogue_voice.rsas a fifth (#5367 Phase V: FO3/FNV per-line voice throughplay_oneshot_following, anchored to the speaker and skipped when it has noGlobalTransform(#5410) and cut bystop_line_voiceon topic change / close / speaker death (#5432); segments chained viabyroredux_audio::with_start_delay, each named by its authoredresponse_number;Attenuation::default(), volume 1.0; gated onAudioWorld::is_activebefore any archive work; decoded throughVoiceSoundCachewith the provider read inside the loader (#5382/#5383); file name and owning plugin perdocs/engine/dialogue-trees.md(#5393/#5395, 5b68792b7; names truncate only when quest + topic exceed 25 characters, one candidate, #5521); missing voice is a silent fallback to the subtitle estimate);reverb_zone_systemthe onlyset_reverb_send_dbcaller,dispatch_region_ambient_musicthe onlyplay_music/stop_musiccaller. Re-grep for any new caller (git grep -n 'play_oneshot\|play_music\|set_underwater\|stop_sounds_for\|take_contact_force_events' -- byroredux/src) and audit it. - Footsteps: XZ-only stride accumulation against
FootstepEmitter.stride_threshold, authored in Bethesda units (0.75 * BETHESDA_UNITS_PER_METER), compared toGlobalTransformdeltas with no conversion at the compare site; reset to 0 on fire (subtract-remainder multiplies footsteps on a teleport); first tick seedslast_positionand fires nothing. Attenuation{0.5, 12.0}in metres (no pre-scaling by 70, no widening). TheFootstepScratchVec(capacity 32) is restored on the success and theAudioWorld-absent path; no component lock is held acrossplay_oneshotand the scratch lock drops beforeAudioWorldis taken. The player-body gate (CharacterController/WaterContact) is resolved as an owned(body, gated)pair BEFORE anyGlobalTransform/FootstepEmitter/scratch guard (#5305, 6915fe783: taking it inside closed three lock-order cycles) — a per-entity get back inside that scope is the regression. Every missing resource is a silent no-op (no per-frame log). Opt-in is component-driven. Surface selection (Wave 4): each trigger resolves its material at footstep cadence throughPhysicsWorld::surface_material_under, which takes a MANDATORYexcluded_body(the player'sRapierHandles.body, else the capsule centre self-hits at toi 0 and every player step reads material 0, #5545 3909778eb);select_footstep_soundis a pure function of aFootstepConfig.surface_soundssnapshot cloned once, after the no-trigger early return and BEFORE theAudioWorldguard (noAudioWorld→FootstepConfiglock edge, #5556 ac710964e); the Access row declares thePhysicsWorld/RapierHandlesreads (scheduler_access_tests.rs::footstep_declares_its_physics_reads). Material keys are u32 end to end (Skyrim's are CRC32s; 0 is STONE on Oblivion/FO3/FNV and the value of every unauthored body, #5542). Which entity walks (#5146, ccc743160): FlyCam keeps the camera's emitter (PlayerEntityisNone); character mode adds an emitter on the player body, and while a body exists camera-borne emitters are re-seeded, never accumulated (the 180 BU third-person boom turned mouse-look into footsteps). The body accumulates only whileCharacterController.is_groundedand not swimming (WaterContactdepth vsdepth_reaches_swimlevel, the same predicate ascharacter.rs); gated frames re-seed so a landing or water exit never replays as a burst. A runtime FlyCam toggle that leavesPlayerEntityset keeps the camera silent — check that is intended. The Access row declares thePlayerEntity/CharacterController/WaterContactreads. - Water audio:
water_audio_systemwritesAudioWorld::set_underwaterfrom the active camera'sSubmersionState.head_submergedevery frame (before the config early-return), then playsSplashEventone-shots (range 1-24 m) and, when no splash fired, one cooldown-limitedRippleEvent(per-surface cooldown, 0.45 damping). The range and the 0.45 factor are uncited (no authored SOUN range): note as unsourced, never invent one.WaterAudioConfig.splash_soundisNonewithout an archive (valid headless state);try_load_default_footstepandtry_load_default_water_splash(asset_provider/texture.rs) no-op cleanly when the arg or archive is absent. - Combat feedback sounds (
play_oneshot_cached): registered outside theBYRO_NO_AI_LOCOMOTIONblock since #4709 (70d9896fb) — a move back inside silently mutes them; hardcoded Skyrim paths perdocs/engine/p2-combat-anim-sound-fixture.md, each request counted per kind inCombatSoundRequests(swing / impact / death_voice; incremented only when the one-shot was actually requested, surfaced bycombat.status, #5148 7208af67c), attenuation{0.5, 24.0}m (uncited, like the water range); lookup goes throughSoundCache::get_or_loadwhose loader readsSoundArchiveProviderwhile theSoundCachewrite guard is held (resource-in-resource nesting). Dialogue voice uses the same cache → provider order onVoiceSoundCache(#5383, 080b35f35; the old provider → cache order closed a lock-order cycle with this path);dialogue_voice.rs::voice_and_combat_sound_paths_share_one_cache_provider_orderdrives both consumers in one process — a third consumer taking the provider first is the regression. Headless / archive-less paths skip silently. reverb_zone_system:-12 dBinterior /NEG_INFINITYexterior; the transition is bit-equality gated (to_bits), no-ops withoutCellLightingRes(pre-load) orAudioWorld(headless).dispatch_region_ambient_music: stops outstanding playback on every failure layer (no provider, no--sounds-bsa, unresolved FormID, folder FNAM, archive miss, decode failure,music_form: None) because what is audible belonged to the previous directive; the authored-but-unresolved log fires once per process (std::sync::Once);loopingthreads fromsound_loops(SOUN Loop bit) intoplay_music.SounRecord::sound_pathis a file or a trailing-separator folder (about half of FNV's FNAM-bearing records):sound_is_folderfails closed before any archive lookup, and any futureresolve_sound_pathconsumer must gate on it (variant selection is an unmade policy, so no "extract any file under this folder" fallback exists).- Archive precedence:
SoundArchiveProvider::extractiterates.rev()(last-listed wins, as mesh/texture/material; FNV'sFallout - Sound.bsa+Update.bsaoverlap with differing bytes): forward iteration is the regression.ScriptProvideris the deliberate first-wins exception.
Output:/tmp/audit/audio/dim_5.md
Phase 3: Merge
Combine /tmp/audit/audio/dim_*.md into docs/audits/AUDIT_AUDIO_<TODAY>.md (header per _audit-common.md Report finalization): Executive Summary (severity counts, headless-boot PASS/FAIL), Lifecycle Invariant Matrix (field-drop order, sticky listener, despawn truncation, schedule order: verified or drifted), Findings (deduplicated), Future-Phase Readiness (FOOT, MUSC/MUST/MSET, occlusion). Then rm -rf /tmp/audit/audio; suggest /audit-publish docs/audits/AUDIT_AUDIO_<TODAY>.md (labels audio, plus game:* when one title's sound data is the cause).