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
|
|
@ -80,7 +80,11 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
public uint OwnerId;
|
||||
public float PlayingGain; // gain at play time (for eviction compare)
|
||||
public bool InUse;
|
||||
public uint PriorityBase; // raw priority from SoundEntry.Priority
|
||||
// The DAT-authored priority, a float in [0,1] — NOT an 0..7 int. 4,100
|
||||
// of the shipped entries carry a sub-1.0 priority that an int cast
|
||||
// collapsed to 0, which flattened the eviction ordering this field
|
||||
// exists for. A2 makes eviction compare it.
|
||||
public float Priority;
|
||||
}
|
||||
private readonly Slot3D[] _pool3D = CreateWorldSlots();
|
||||
private int _pool3DCursor; // round-robin start
|
||||
|
|
@ -251,8 +255,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
WaveData wave,
|
||||
Vector3 position,
|
||||
float volume,
|
||||
float priority,
|
||||
float pitch = 1.0f)
|
||||
float priority)
|
||||
{
|
||||
if (_worldAudioSuspended || !_available || _al is null) return false;
|
||||
|
||||
|
|
@ -285,7 +288,8 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
_al.SetSourceProperty(slot.SourceId, SourceInteger.Buffer, 0); // detach old
|
||||
_al.SetSourceProperty(slot.SourceId, SourceInteger.Buffer, (int)buffer);
|
||||
_al.SetSourceProperty(slot.SourceId, SourceFloat.Gain, effectiveGain);
|
||||
_al.SetSourceProperty(slot.SourceId, SourceFloat.Pitch, pitch);
|
||||
// No pitch: retail never calls SetFrequency on a sound buffer, so
|
||||
// there is no per-play pitch variation to reproduce.
|
||||
_al.SetSourceProperty(slot.SourceId, SourceVector3.Position, position.X, position.Y, position.Z);
|
||||
_al.SetSourceProperty(slot.SourceId, SourceBoolean.SourceRelative, false);
|
||||
_al.SetSourceProperty(slot.SourceId, SourceBoolean.Looping, false);
|
||||
|
|
@ -294,7 +298,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
slot.PlayingGain = effectiveGain;
|
||||
slot.InUse = true;
|
||||
slot.OwnerId = ownerId;
|
||||
slot.PriorityBase = (uint)Math.Clamp((int)priority, 0, 7);
|
||||
slot.Priority = priority;
|
||||
_pool3DCursor = (slotIdx + 1) & (PoolSize3D - 1);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -330,7 +334,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
/// Play a raw WaveData blob as a 2D UI sound (no falloff, ignores
|
||||
/// listener position).
|
||||
/// </summary>
|
||||
public bool PlayUiWave(uint waveId, WaveData wave, float volume = 1f, float pitch = 1f)
|
||||
public bool PlayUiWave(uint waveId, WaveData wave, float volume = 1f)
|
||||
{
|
||||
if (!_available || _al is null) return false;
|
||||
|
||||
|
|
@ -350,7 +354,6 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
_al.SetSourceProperty(src, SourceInteger.Buffer, 0);
|
||||
_al.SetSourceProperty(src, SourceInteger.Buffer, (int)buffer);
|
||||
_al.SetSourceProperty(src, SourceFloat.Gain, Math.Clamp(volume, 0f, 1f) * SfxVolume);
|
||||
_al.SetSourceProperty(src, SourceFloat.Pitch, pitch);
|
||||
_al.SourcePlay(src);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -517,7 +520,7 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen
|
|||
|
||||
slot.OwnerId = 0;
|
||||
slot.PlayingGain = 0f;
|
||||
slot.PriorityBase = 0;
|
||||
slot.Priority = 0f;
|
||||
slot.InUse = false;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue