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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user