fix(audio): Campaign A slice A1 — retail's sound probability gate (#355)
The SoundTable probability field is a Bernoulli play/skip gate applied at the play site (SoundManager::PlayProbability @0x005500E0), not a selection weight — and variant selection (SoundManager::GetSound @0x00550680) is a uniform index over (n-1) that ignores probability entirely. SoundCookbook did the opposite: a cumulative-distribution walk weighted BY probability, short-circuiting single-entry lists before rolling at all. A dat census says 4,183 of 4,184 entries are single-entry and 686 of those carry probability < 1.0, so the gate was categorically absent: Speak1 idle chatter authored at 0.05 fired every trigger (~20x too often), wound/attack/ swoosh variants never dropped, and six 0.0001 entries always played. Split into retail's two steps (PickVariant + PlayProbability, composed by Select) over a new ISoundRandom modelling both retail roll ranges: the variant roll clamped below 1.0 (0x00797D48) and the gate's 1/32767 grid, which is why 0.0001 resolves to ~1.2e-4. PickVariant reproduces retail's (n-1) off-by-one verbatim per the port-faithfully rule — the last variant of a multi-entry sound is unreachable, costing exactly one wave (0x0A00051E) in the shipped dats. Also removes invented mechanism this review disproved: the dead Core SoundEntry/ISoundCache scaffold (PitchMin/PitchMax, Loop, Is3D — retail never calls SetFrequency, never sets the loop flag, and creates every gameplay buffer 2D), the engine's pitch plumbing, the int 0..7 priority cast (the dat field is a float in [0,1]; 4,100 entries collapsed to 0), and the clamp-at-the-field on volume (an unbounded gain retail clamps only after the distance divide). Tests rewritten as conformance against the disassembled values, replacing a self-referential suite that pinned the wrong model. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
ffa5087527
commit
c69b3bde04
8 changed files with 445 additions and 161 deletions
|
|
@ -13,22 +13,17 @@ namespace AcDream.Core.Audio;
|
|||
// 23-member subset was replaced by retail's full 205-entry SoundType
|
||||
// catalog: src/AcDream.Core/Audio/SoundId.cs (same namespace).
|
||||
|
||||
/// <summary>
|
||||
/// Per-SoundId entry from the SoundTable dat (0x20000000..0x2000FFFF).
|
||||
/// One Sound can have multiple entries with probabilities — that's
|
||||
/// retail's variation mechanism (e.g. 3 different footstep clips).
|
||||
/// </summary>
|
||||
public sealed class SoundEntry
|
||||
{
|
||||
public uint WaveId { get; init; } // → Wave dat (0x0A000000..0x0A00FFFF)
|
||||
public int Priority { get; init; } // eviction ordering (0..7)
|
||||
public float Probability{ get; init; } // for entries with multiple alternatives
|
||||
public float VolumeBase { get; init; } // 0..1 multiplier applied before falloff
|
||||
public float PitchMin { get; init; }
|
||||
public float PitchMax { get; init; }
|
||||
public bool Loop { get; init; }
|
||||
public bool Is3D { get; init; } // 3D positional vs UI/music flat
|
||||
}
|
||||
// A local `SoundEntry` scaffold class lived here until 2026-08-08 (Campaign A
|
||||
// slice A1). It had no implementers and no readers — the live path consumes
|
||||
// DatReaderWriter's `SoundEntry` directly — and four of its eight fields were
|
||||
// invented: `PitchMin`/`PitchMax` (retail never calls SetFrequency), `Loop`
|
||||
// (retail never sets the DirectSound loop flag; "looping" ambients are
|
||||
// re-fired one-shots), and `Is3D` (every retail gameplay buffer is created
|
||||
// with m_3D = 0). Its `Priority` was also typed `int` "0..7" where the dat
|
||||
// field is a float in [0,1]. The `ISoundCache` interface that returned it went
|
||||
// the same way. Evidence:
|
||||
// docs/research/2026-08-08-audio-retail-soundmanager-core.md,
|
||||
// docs/research/2026-08-08-audio-retail-dat-layer.md §1.
|
||||
|
||||
/// <summary>
|
||||
/// Raw decoded PCM data from a Wave dat. Set by <c>WaveDecoder</c> at
|
||||
|
|
@ -102,14 +97,3 @@ public interface IAudioEngine : IDisposable
|
|||
void PlayMusic(string resourceName, bool loop);
|
||||
void StopMusic();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cache of decoded waves + SoundTable lookups. Owned by the App-layer
|
||||
/// AudioEngine; Core exposes the interface.
|
||||
/// </summary>
|
||||
public interface ISoundCache
|
||||
{
|
||||
WaveData GetWave(uint waveId);
|
||||
IReadOnlyList<SoundEntry> GetSoundEntries(SoundId id);
|
||||
IReadOnlyList<SoundEntry> GetSoundEntries(uint soundTableId, SoundId id);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue