feat(audio): Campaign A slice A2 — retail's 2D pan+gain mixer replaces AL 3D
Retail is not a 3D audio engine. Every gameplay buffer is created with m_3D = 0 and the DirectSound 3D listener the client sets up is dead code; spatialization is two CPU scalars per voice, frozen at emission. This slice ports that math and demotes OpenAL to a voice bank. RetailSoundMixer (new, Core) carries the byte-decoded curve from SoundManager::GetAttenuation @0x00550020: g = dist < 5 ? vol : 25*vol/d2, clamped to 1 BEFORE the single master multiply, db = ceil(20*log10 g), with a hard -50 dB floor at which retail does not start the voice at all (audible radius ~94.2 m at unity). Pan is PlaySoundInternal @0x00550170's (int)(-15*sin(delta-bearing)) in whole decibels, truncating toward zero, forced to dead centre when (int)distance < 5, with no front/back and no elevation cue. Every AL source is now source-relative with rolloff 0 and the global distance model is None: AL's InverseDistanceClamped was first-power (2/d), quieter than retail up close and far louder at range with no cutoff whatsoever. That was the largest audible divergence in the subsystem (AP-28, retired here). RetailVoicePool (new, Core) ports the allocator at 0x0054FEC0: ring scan for a free or finished slot, then evict the first slot whose DAT priority is strictly lower, else drop. Eviction compared GAIN before, so a loud unimportant sound could silence a quiet important one. It lives in Core because the engine's play path talks to native AL handles and could not be tested; the pool now has 12 conformance tests. The listener keeps using the camera position, which the decode shows is retail-faithful (SmartBox::set_viewer @0x00452D36 hands the same collided camera Position to SoundManager) — only the heading extraction changes, since retail reads one compass bearing and never a forward/up basis. An earlier draft of the plan called this a defect; corrected in the plan so it is not fixed backwards. Opus review found and this commit fixes: a linear pan-to-azimuth mapping that saturated to full separation at 30 degrees (OpenAL Soft's own speaker angle) where retail gives 15 dB — now inverts the constant-power pan law, so full deflection reaches 0.776 of the arc and both channels stay live; the stale FUN_00550ad0 / gain-eviction class header, which contradicted the register row this commit writes; missing discriminating tests for clamp order and pan truncation; dead PlayingGain state whose comment invented a retail symbol; and a third in-tree copy of Position::heading, now delegating to MoveToMath.PositionHeading. MasterVolume folds into the mixer's one multiply instead of AL listener gain, so the cutoff, radius and dB quantisation move with the slider. Register: AP-28 retired; AP-173 (pan law), AP-174 (volume taxonomy), TS-64 (two unimplemented sound prefs), TS-65 (volume-squared quirk, applied on the ambient path only) filed. Research note corrected twice where its summary contradicted its own decode (30 m dB, floor vs trunc). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c69b3bde04
commit
e42b99482e
13 changed files with 1123 additions and 106 deletions
|
|
@ -7,6 +7,7 @@ using AcDream.App.Rendering.Wb;
|
|||
using AcDream.App.Settings;
|
||||
using AcDream.App.Streaming;
|
||||
using AcDream.App.World;
|
||||
using AcDream.Core.Audio;
|
||||
using AcDream.Core.Lighting;
|
||||
using AcDream.Core.Physics;
|
||||
using AcDream.Core.Rendering;
|
||||
|
|
@ -381,14 +382,19 @@ internal sealed class RuntimeWorldFrameSettingsPreview : IWorldFrameSettingsPrev
|
|||
if (_audio is not { IsAvailable: true })
|
||||
return;
|
||||
|
||||
// Retail's audio listener IS the camera viewer, refreshed per rendered
|
||||
// frame (`SmartBox::set_viewer` @ 0x00452D36 hands the same collided
|
||||
// camera Position to SoundManager, the sky, and the camera setup), so
|
||||
// the position source here is already faithful. What retail reads off it
|
||||
// is only the origin and `Frame::get_heading` — a compass bearing, not a
|
||||
// basis: pan comes from that heading alone, which is why retail has no
|
||||
// front/back or elevation cue to reproduce.
|
||||
Matrix4x4 inverse = camera.InverseView;
|
||||
var forward = new Vector3(-inverse.M31, -inverse.M32, -inverse.M33);
|
||||
var up = new Vector3(inverse.M21, inverse.M22, inverse.M23);
|
||||
Vector3 position = camera.Position;
|
||||
_audio.SetListener(
|
||||
position.X, position.Y, position.Z,
|
||||
forward.X, forward.Y, forward.Z,
|
||||
up.X, up.Y, up.Z);
|
||||
RetailSoundMixer.CompassHeadingDegrees(Vector3.Zero, forward));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue