MasterSoundEffects 0→1 is how a 1.12 addon stops a sound — make it stop sounds

grom: SoftSpoken "played a voice line over and over", and its **Silence NPC Voices**
option did nothing although it was switched on. Both are one deviation, and it is ours.

There is no Dialog channel to mute on 1.12, and SoftSpoken's own shim says so:

    function SoundUtils:MuteChannel(channel, muted, fadeOut)
        -- Intentionally unsupported on this client.
    end
    function SoundUtils:StopSound(clip, fadeMs)
        SetCVar("MasterSoundEffects", 0)
        SetCVar("MasterSoundEffects", 1)
    end

That toggle is its whole repertoire, and it is not a hack the addon invented — the
reference makes it work on the **enabling** edge:

    0x457500   the CVar callback: cl = !!value, tail-calls…
    0x7a6570   …the flag store, edge-triggered (7a657f: je on an unchanged flag)
    0x7a67a0   walks the live instance list; per node:
                 flags & 0x2        → skipped (the cinematic bypass)
                 flags & 0x4000008  → muted, not stopped (0x7a65a0)
                 otherwise, and ONLY when enabling (7a67eb: test cl,cl / je):
                   flags & 0x4      → 0x7a5950   stop
                   else             → 0x7a4200   stop, then unlink (0x7604f0)

`0x7a5950` is unmistakable: it takes the node's stream handle (+0x34) and channel id
(+0x30) and calls the FMOD stop thunks. The *disabling* edge stops nothing at all —
sounds run on, gated silent at the pump, which is exactly what `category_amp` already
did. So we had half the CVar, and it was the half that does not matter. The comment on
`SoundConfig::enabled` claimed the two "sound the same"; they do for a player moving a
slider, and do not for an addon.

Both symptoms follow. `MuteGameDialogue` is gated on `AutoToggleDialog`, whose default is
`(Version.IsLegacyVanilla or …)` — true here, which is why his saved profile has no
`Audio` block and the option was on anyway — and on this client its body is nothing but
the toggle. The gate passed, the toggle ran, the NPC greeting played straight over the
line being read. The repeat is the same toggle spent on SoftSpoken's own clip:
`PlaySound` cuts *before* it plays ("On Vanilla, interrupt NPC audio BEFORE playback"),
and `StopKeeping` is documented as "silence the head but keep its place: it replays from
the start when Advance next reaches it, since the client has no seek". With the cut inert
the head never stopped, so the replay layered a second copy on a first still going.

`kit::cut_effects_on_reenable` now stops every `SoundCategory::Sfx` channel when the edge
is spent. Music and ambience are spared (the reference's mute-not-stop class) and
cinematic audio never enters `SoundOutput::channels` at all (the `flags & 0x2` bypass).

Two details are load-bearing, both pinned:

- **The edge is armed by the write, never sampled from the value.** The addon writes 0
  and 1 inside one Lua call, so a system reading `SoundConfig::enabled` once a frame sees
  `true` on both sides and no edge at all. `EffectCutArmed` is set in the CVar observer,
  the one place that sees every write. It is its own resource rather than a `SoundConfig`
  field because spending it needs `ResMut`, which as a field measured **43** new
  ambiguous pairs against every `Res<SoundConfig>` reader in the frame.
- **The cut is spent between the Lua pass and the sound drain.** The reference stops
  synchronously inside `SetCVar`, which is before the `PlaySoundFile` the addon calls
  next. Applied after the drain it would stop the new clip instead of the old one — the
  symptom inverted, not fixed.

The ceiling rises by 1, for the pair against the collider finisher: the same exclusive
`&mut World` shape `log_new_corpses` and `textinput::open_early` already record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-10-30 14:48:13 -06:00
co-authored by Claude Opus 5
parent 08d574b09c
commit f8e93c4484
6 changed files with 271 additions and 7 deletions
+91
View File
@@ -1541,3 +1541,94 @@ Still deviating, and now written where it lives: the final `UNIT_MOD_CAST_SPEED`
(`6e342c`, taken only when `Attributes & 0x30` is clear) is not applied, because no site streams
unit field 139. It is 1.0 without a casting-speed aura, so it moves nothing a physical ability
does — a Curse of Tongues target's cast lines are the case that still reads short.
## SoftSpoken: one inert CVar toggle, two symptoms
grom reported SoftSpoken "play a voice line over and over", and that its **Silence NPC Voices**
option did nothing although it was on. Both are one deviation, and it is ours.
### The only cut primitive 1.12 gives an addon
SoftSpoken's own 1.12 shim says what it cannot do:
```lua
function SoundUtils:MuteChannel(channel, muted, fadeOut)
-- Intentionally unsupported on this client.
end
function SoundUtils:StopSound(clip, fadeMs)
SetCVar("MasterSoundEffects", 0)
SetCVar("MasterSoundEffects", 1)
clip.handle = nil
end
```
There is no Dialog channel to mute on 1.12, so the whole repertoire is that CVar pair. It is not a
hack the addon invented: toggling `MasterSoundEffects` off and straight back on is how a vanilla
addon stops a sound that is already playing, and the reference makes it work on the **enabling**
edge:
```
0x457500 the CVar callback: cl = !!value, tail-calls…
0x7a6570 …the flag store, edge-triggered (7a657f: je on an unchanged flag)
0x7a67a0 walks the live instance list; per node:
flags & 0x2 → skipped (the cinematic bypass)
flags & 0x4000008 → muted, not stopped (0x7a65a0)
otherwise, and ONLY when enabling (7a67eb: test cl,cl / je):
flags & 0x4 → 0x7a5950 stop
else → 0x7a4200 stop, then unlink (0x7604f0)
```
`0x7a5950` is unmistakable: it takes the node's stream handle (`+0x34`) and channel id (`+0x30`)
and calls the FMOD stop thunks. The *disabling* edge stops nothing at all — sounds run on, gated
silent at the pump, which is exactly what `SoundConfig::category_amp` already did by zeroing every
category. So our deviation was only ever half of the CVar, and the half we had was the half that
does not matter.
An earlier revision of `SoundConfig::enabled`'s comment said benilla's gain-zeroing "sounds the
same". It does, for a player moving the slider. It does not for an addon, and that difference is
the only sound-cutting primitive the era offers.
### The two symptoms
`SoundQueue:MuteGameDialogue` is gated on the player's option and is, on this client, nothing but
the toggle:
```lua
if not Addon.db.profile.Audio.AutoToggleDialog then return end
if Version.IsLegacyVanilla then
if speakingOn then
SetCVar("MasterSoundEffects", 0)
SetCVar("MasterSoundEffects", 1)
end
return
end
```
`AutoToggleDialog` defaults to `(Version.IsLegacyVanilla or …)` — **true** here — which is why his
saved profile has no `Audio` block at all and the option was nonetheless on. The gate passed, the
toggle ran, nothing was cut: the NPC greeting played straight over the line being read.
The repeat is the same toggle spent on SoftSpoken's own clip. `SoundQueue:PlaySound` cuts *before*
it plays (`-- On Vanilla, interrupt NPC audio BEFORE playback`), and `StopKeeping` is documented as
"silence the head but keep its place: it replays from the start when Advance next reaches it, since
the client has no seek". With the cut inert the head never stopped, so the replay layered a second
copy on a first that was still going — a line over and over, over itself.
### What changed
`kit::cut_effects_on_reenable` stops every `SoundCategory::Sfx` channel when the edge is spent.
Music and ambience are left alone, matching the reference's mute-not-stop class; cinematic audio
never enters `SoundOutput::channels` at all, matching the `flags & 0x2` bypass.
Two details are load-bearing and both are pinned by tests:
- **The edge is armed by the CVar write, never sampled from the value.** The addon writes `0` and
`1` inside one Lua call, so a system reading `SoundConfig::enabled` once a frame sees `true` on
both sides and no edge at all. `EffectCutArmed` is set in the CVar observer, which is the one
place that sees every write — and it is its own resource rather than a `SoundConfig` field
because spending it needs `ResMut`, which as a field cost 43 ambiguous pairs against every
`Res<SoundConfig>` reader.
- **The cut is spent between the Lua pass and the sound drain.** The reference stops synchronously
inside `SetCVar`, which is *before* the `PlaySoundFile` that follows it in the same addon
function. A cut applied after the drain would stop the new clip instead of the old one — the
symptom inverted rather than fixed.