feat(audio): Phase E.2 OpenAL engine + SoundTable cookbook + hook wiring
Full audio pipeline from MotionHook → OpenAL 3D playback. Faithful to retail's 16-voice pool, inverse-square falloff, and SoundTable probabilistic variant selection. Core layer (AcDream.Core/Audio): - WaveDecoder parses the WAVEFORMATEX in Wave dat headers. PCM (wFormatTag=1) decodes directly; MP3 (0x55) and ADPCM (0x02) return null + log (ACM compressed decoders need Windows winmm; cross-platform path deferred). Cites r05 §2.1-2.3 + ACE Wave.cs. - SoundCookbook.Roll implements the probability-weighted entry pick that gives retail footsteps their variation. Cumulative-distribution walk; silence tail when probabilities sum to <1. - DatSoundCache: ConcurrentDictionary-backed lazy load of Wave / SoundTable dats, decoded PCM memoized. App layer (AcDream.App/Audio): - OpenAlAudioEngine (Silk.NET.OpenAL): 16-source 3D pool with round-robin first-free, then evict-quieter-slot algorithm matching retail chunk_00550000.c FUN_00550ad0 exactly. Separate 4-source UI pool (source-relative). AL buffer cache keyed by Wave id. InverseDistanceClamped distance model. Fail-open when AL driver missing or ACDREAM_NO_AUDIO=1 — client continues without audio. - AudioHookSink routes SoundHook / SoundTableHook / SoundTweakedHook from the Phase E.1 animation-hook router into OpenAL. All three hook types fire on both player AND NPCs/monsters (the sequencer dispatches per-entity and the sink uses entity worldPos for 3D pan). - DictionaryEntitySoundTable holds per-entity SoundTable mapping, populated from Setup.DefaultSoundTable at hydration time. Server- sent overrides would take precedence here when wired. GameWindow integration: - OpenAL init in OnLoad after dat collection, suppressible via ACDREAM_NO_AUDIO=1. - SetListener called each OnRender frame with camera position + view basis vectors (fwd = -Z, up = +Y of inverse view). - AudioEngine disposed in OnClosing before dats. Tests: 6 WaveDecoder (PCM / MP3-null / ADPCM-null / stereo / truncated / peek) + 6 SoundCookbook (empty / single / 50-30-20 distribution within 5%, silence tail, table lookup, missing table key). Verified against r05 §2 + ACViewer export-path. Build green, 497 tests pass (up from 485). Ref: r05 §2 (Wave format), §5.3 (16-voice pool + eviction). Ref: FUN_00550ad0 (chunk_00550000.c:527) eviction algorithm. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b04d393329
commit
351723928f
9 changed files with 1072 additions and 0 deletions
|
|
@ -129,6 +129,13 @@ public sealed class GameWindow : IDisposable
|
|||
// per-entity tick loop can just call it unconditionally.
|
||||
private readonly AcDream.Core.Physics.AnimationHookRouter _hookRouter = new();
|
||||
|
||||
// Phase E.2 audio. Null when ACDREAM_NO_AUDIO=1 or the OpenAL driver
|
||||
// failed to init; all three are set together.
|
||||
private AcDream.App.Audio.OpenAlAudioEngine? _audioEngine;
|
||||
private AcDream.Core.Audio.DatSoundCache? _soundCache;
|
||||
private AcDream.App.Audio.DictionaryEntitySoundTable? _entitySoundTables;
|
||||
private AcDream.App.Audio.AudioHookSink? _audioSink;
|
||||
|
||||
// Phase B.2: player movement mode.
|
||||
private AcDream.App.Input.PlayerMovementController? _playerController;
|
||||
private AcDream.App.Rendering.ChaseCamera? _chaseCamera;
|
||||
|
|
@ -559,6 +566,33 @@ public sealed class GameWindow : IDisposable
|
|||
_dats = new DatCollection(_datDir, DatAccessType.Read);
|
||||
_animLoader = new AcDream.Core.Physics.DatCollectionLoader(_dats);
|
||||
|
||||
// Phase E.2 audio: init OpenAL + hook sink. Suppressible via
|
||||
// ACDREAM_NO_AUDIO=1 for headless tests / broken audio drivers.
|
||||
if (Environment.GetEnvironmentVariable("ACDREAM_NO_AUDIO") != "1")
|
||||
{
|
||||
try
|
||||
{
|
||||
_soundCache = new AcDream.Core.Audio.DatSoundCache(_dats);
|
||||
_audioEngine = new AcDream.App.Audio.OpenAlAudioEngine();
|
||||
_entitySoundTables = new AcDream.App.Audio.DictionaryEntitySoundTable();
|
||||
if (_audioEngine.IsAvailable)
|
||||
{
|
||||
_audioSink = new AcDream.App.Audio.AudioHookSink(
|
||||
_audioEngine, _soundCache, _entitySoundTables);
|
||||
_hookRouter.Register(_audioSink);
|
||||
Console.WriteLine("audio: OpenAL engine ready (16 voices, 3D positional)");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.WriteLine("audio: OpenAL unavailable (driver missing / headless) — audio disabled");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"audio: init failed: {ex.Message} — audio disabled");
|
||||
}
|
||||
}
|
||||
|
||||
uint centerLandblockId = 0xA9B4FFFFu;
|
||||
Console.WriteLine($"loading world view centered on 0x{centerLandblockId:X8}");
|
||||
|
||||
|
|
@ -1114,6 +1148,17 @@ public sealed class GameWindow : IDisposable
|
|||
CurrFrame = idleCycle.LowFrame,
|
||||
Sequencer = sequencer,
|
||||
};
|
||||
|
||||
// Phase E.2: register entity's SoundTable so SoundTableHook can
|
||||
// resolve creature-specific sounds (footsteps, attack vocalizations,
|
||||
// damage grunts, etc). Server-sent SoundTable override would take
|
||||
// precedence here when the wire layer delivers it.
|
||||
if (_entitySoundTables is not null)
|
||||
{
|
||||
uint soundTableId = (uint)setup.DefaultSoundTable;
|
||||
if (soundTableId != 0)
|
||||
_entitySoundTables.Set(entity.Id, soundTableId);
|
||||
}
|
||||
}
|
||||
|
||||
// Dump a summary periodically so we can see drop breakdowns without
|
||||
|
|
@ -2702,6 +2747,21 @@ public sealed class GameWindow : IDisposable
|
|||
// Extract camera world position from the inverse of the view matrix.
|
||||
System.Numerics.Matrix4x4.Invert(camera.View, out var invView);
|
||||
var camPos = new System.Numerics.Vector3(invView.M41, invView.M42, invView.M43);
|
||||
|
||||
// Phase E.2 audio: update listener pose so 3D sounds pan/attenuate
|
||||
// correctly relative to where we're looking. Fwd = -Z of the view
|
||||
// matrix (OpenGL convention), up = +Y. Both live in the inverse
|
||||
// view matrix's basis vectors.
|
||||
if (_audioEngine is not null && _audioEngine.IsAvailable)
|
||||
{
|
||||
var fwd = new System.Numerics.Vector3(-invView.M31, -invView.M32, -invView.M33);
|
||||
var up = new System.Numerics.Vector3( invView.M21, invView.M22, invView.M23);
|
||||
_audioEngine.SetListener(
|
||||
camPos.X, camPos.Y, camPos.Z,
|
||||
fwd.X, fwd.Y, fwd.Z,
|
||||
up.X, up.Y, up.Z);
|
||||
}
|
||||
|
||||
var visibility = _cellVisibility.ComputeVisibility(camPos);
|
||||
bool cameraInsideCell = visibility?.CameraCell is not null;
|
||||
|
||||
|
|
@ -3166,6 +3226,7 @@ public sealed class GameWindow : IDisposable
|
|||
// _dats; Dispose cancels the token and waits up to 2s for the thread.
|
||||
_streamer?.Dispose();
|
||||
_liveSession?.Dispose();
|
||||
_audioEngine?.Dispose(); // Phase E.2: stop all voices, close AL context
|
||||
_staticMesh?.Dispose();
|
||||
_textureCache?.Dispose();
|
||||
_meshShader?.Dispose();
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue