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>
78 lines
2.3 KiB
C#
78 lines
2.3 KiB
C#
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using DatReaderWriter;
|
|
using DatReaderWriter.DBObjs;
|
|
|
|
namespace AcDream.Core.Audio;
|
|
|
|
/// <summary>
|
|
/// DatCollection-backed cache of decoded waves + SoundTable lookups.
|
|
///
|
|
/// <para>
|
|
/// Lazy-loads each Wave / SoundTable on first request, decodes Wave PCM
|
|
/// data via <see cref="WaveDecoder"/>, and memoizes the result. Safe to
|
|
/// call from any thread — the inner dicts are
|
|
/// <see cref="ConcurrentDictionary{TKey,TValue}"/>.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// Returns <c>null</c> for waves whose format tag isn't PCM (MP3 / ADPCM
|
|
/// fall through silently until a compressed-decoder path is wired up).
|
|
/// </para>
|
|
/// </summary>
|
|
public sealed class DatSoundCache
|
|
{
|
|
private readonly DatCollection _dats;
|
|
private readonly ConcurrentDictionary<uint, WaveData?> _waves = new();
|
|
private readonly ConcurrentDictionary<uint, SoundTable?> _tables = new();
|
|
|
|
public DatSoundCache(DatCollection dats)
|
|
{
|
|
_dats = dats;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve decoded PCM data for a Wave dat id. Returns null if the
|
|
/// wave is missing, malformed, or uses a currently-unsupported format
|
|
/// (MP3 / ADPCM).
|
|
/// </summary>
|
|
public WaveData? GetWave(uint waveId)
|
|
{
|
|
if (_waves.TryGetValue(waveId, out var cached)) return cached;
|
|
|
|
var wave = _dats.Get<Wave>(waveId);
|
|
if (wave is null)
|
|
{
|
|
_waves[waveId] = null;
|
|
return null;
|
|
}
|
|
|
|
var decoded = WaveDecoder.Decode(wave.Header, wave.Data);
|
|
_waves[waveId] = decoded;
|
|
return decoded;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Retrieve a SoundTable by dat id. Returns null if the table is
|
|
/// missing from the dats.
|
|
/// </summary>
|
|
public SoundTable? GetSoundTable(uint soundTableId)
|
|
{
|
|
if (_tables.TryGetValue(soundTableId, out var cached)) return cached;
|
|
|
|
var table = _dats.Get<SoundTable>(soundTableId);
|
|
_tables[soundTableId] = table;
|
|
return table;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Total number of waves that have been accessed (hit or miss).
|
|
/// Useful for diagnostic overlays.
|
|
/// </summary>
|
|
public int CachedWaveCount => _waves.Count;
|
|
|
|
/// <summary>
|
|
/// Total number of SoundTables that have been accessed.
|
|
/// </summary>
|
|
public int CachedSoundTableCount => _tables.Count;
|
|
}
|