diff --git a/crates/benilla-app/src/sound/kit.rs b/crates/benilla-app/src/sound/kit.rs index 4558f6bc..daa03d7b 100644 --- a/crates/benilla-app/src/sound/kit.rs +++ b/crates/benilla-app/src/sound/kit.rs @@ -1000,11 +1000,30 @@ pub(super) fn arms_effect_cut(was: bool, now: bool) -> bool { now && !was } -/// Which categories the enabling edge stops. `Sfx` alone: music and ambience are the reference's -/// `flags & 0x4000008` class, muted rather than stopped (`0x7a65a0`), so a toggle must not cost -/// the player their music — the point of the toggle is to cut a bark, not to restart the score. -fn stopped_by_reenable(category: SoundCategory) -> bool { - matches!(category, SoundCategory::Sfx) +/// Which channels the enabling edge stops: a kit-played `Sfx` channel, and nothing else. +/// +/// Two exclusions, and they have different standing. +/// +/// Music and ambience are **derived**: they are the reference's `flags & 0x4000008` class, muted +/// rather than stopped (`0x7a65a0`), so a toggle must not cost the player their music — the point +/// of the toggle is to cut a bark, not to restart the score. +/// +/// `kit == 0`, the `PlaySoundFile` path ([`play_file`], whose doc records that id as its own +/// marker), is **measured, not derived**, and the distinction is worth keeping honest. Stopping +/// those too is what the first revision of this did, and it silenced SoftSpoken completely: +/// grom's session carries ten `MasterSoundEffects re-enabled` lines and he heard none of the ten +/// voice lines that produced them. It has to be excluded, because **every 1.12 voice-over addon +/// toggles this CVar and then plays its own file** — a cut that reaches file playback cuts the +/// very thing the toggle was spent to make room for. +/// +/// Which of the reference's two flag classes a streamed file falls in is not settled here; +/// `0x4000008`'s meaning is unreversed, so this is a player's measurement standing in for a fact +/// we do not have. What it costs is the other half of SoftSpoken's use of the toggle: +/// `SoundQueue.StopKeeping` spends it to stop its *own* clip before replaying one, and that does +/// nothing again. If the overlapping-replay symptom returns, this is why — and the per-kit line +/// the stop now logs is what will say so. +fn stopped_by_reenable(kit: u32, category: SoundCategory) -> bool { + matches!(category, SoundCategory::Sfx) && kit != 0 } /// Spend an armed `MasterSoundEffects` enabling edge: stop every playing effect, as the @@ -1042,17 +1061,25 @@ pub(super) fn cut_effects_on_reenable( return; } let before = out.channels.len(); + // Named, not merely counted: which kits a toggle cuts is the only evidence separating "the + // bark died, as intended" from "we cut the line the addon was about to speak". + let mut stopped: Vec = Vec::new(); + let mut spared_files = 0usize; out.channels.retain_mut(|c| { - if !stopped_by_reenable(c.category) { + if !stopped_by_reenable(c.kit, c.category) { + spared_files += usize::from(c.kit == 0 && c.category == SoundCategory::Sfx); return true; } + stopped.push(c.kit); c.handle.stop(mixer::declick()); false }); - let stopped = before - out.channels.len(); - if stopped > 0 { + if !stopped.is_empty() || spared_files > 0 { info!( - "sound: MasterSoundEffects re-enabled — {stopped} effect channel(s) stopped (0x7a67a0)" + "sound: MasterSoundEffects re-enabled — stopped {} of {before} channel(s), kits {:?}; \ + spared {spared_files} PlaySoundFile channel(s) (0x7a67a0)", + stopped.len(), + stopped ); } } @@ -1130,15 +1157,25 @@ mod tests { assert!(enabled, "and leave sound effects on"); } - /// The toggle cuts effects and leaves the score alone. + /// The toggle cuts barks, leaves the score alone, and — the one grom's silence paid for — + /// never touches an addon's own `PlaySoundFile` audio. #[test] - fn the_reenabling_edge_spares_music_and_ambience() { - assert!(stopped_by_reenable(SoundCategory::Sfx)); + fn the_reenabling_edge_spares_music_ambience_and_addon_files() { assert!( - !stopped_by_reenable(SoundCategory::Music), + stopped_by_reenable(1, SoundCategory::Sfx), + "a kit-played effect is the bark the toggle exists to cut" + ); + assert!( + !stopped_by_reenable(1, SoundCategory::Music), "music is the reference's mute-not-stop class (`flags & 0x4000008`, `0x7a65a0`)" ); - assert!(!stopped_by_reenable(SoundCategory::Ambience)); + assert!(!stopped_by_reenable(1, SoundCategory::Ambience)); + assert!( + !stopped_by_reenable(0, SoundCategory::Sfx), + "kit 0 is `play_file`'s marker, so this is an addon's own `PlaySoundFile`. Cutting \ + it silenced SoftSpoken outright: every 1.12 voice-over addon toggles this CVar and \ + then plays its own file, so the cut would take the line the toggle made room for" + ); } /// Pinned as the 13 dwords at `.data` `0x87ce60`.