Retail EoR has no music system: the linked winmm MIDI player has zero callers, 'music' appears zero times in the 65 MB decomp, SoundType has no music member, InitPrefs registers no music key, and the install ships no music files. So PlayMusic/StopMusic/MusicVolume and the AudioSettings Music knob are deleted rather than left as an API modelling dead code — the string-keyed signature was the tell, since every other entry point is DID-keyed. Old settings.json files carrying a 'music' key still load; the reader ignores unknown keys and the next save drops it. The Ambient slider is now surfaced, because slice A5 gave it something to drive, and its default returns to retail's 1.0 from an invented 0.8 — InitPrefs defaults every sound preference to unity. The panel rule is unchanged: no slider that does nothing. r05-audio-sound.md gets a SUPERSEDED banner naming its five wrong sections (falloff, pan, voice pool, selection, music, ambient) so a future reader reaches the lane notes instead of the Ghidra-era reads that this campaign spent its first two slices undoing. TS-9 re-scoped from 'any MP3 cue' to the measured blast radius: exactly 1 MP3 among 786 shipped waves, a ~2 s mono clip. Its original framing assumed a music system that does not exist. The ADPCM count remains unmeasured and is named as the open question. Deferred deliberately: #321's sound-cache decode-dedup race. It is a pre-existing concurrency flake rather than audio-parity behaviour, and shipping a speculative fix to a race I have not reproduced is exactly the shortcut this project's no-workarounds rule exists to prevent. Campaign A is code-complete; the plan carries the closeout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
109 lines
5.9 KiB
C#
109 lines
5.9 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
|
|
namespace AcDream.Core.Audio;
|
|
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
// Scaffold for R5 — audio data model + engine interface.
|
|
// Full research: docs/research/deepdives/r05-audio-sound.md
|
|
// Runtime backend (Silk.NET.OpenAL) lives in AcDream.App.
|
|
// ─────────────────────────────────────────────────────────────────────
|
|
|
|
// SoundId moved to its own file on 2026-07-29, when the hand-curated
|
|
// 23-member subset was replaced by retail's full 205-entry SoundType
|
|
// catalog: src/AcDream.Core/Audio/SoundId.cs (same namespace).
|
|
|
|
// 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
|
|
/// load time. Retail supports both PCM and MP3 source; we decode MP3
|
|
/// to PCM once at load (same as retail does for long clips).
|
|
/// </summary>
|
|
public sealed class WaveData
|
|
{
|
|
public int ChannelCount { get; init; }
|
|
public int SampleRate { get; init; }
|
|
public int BitsPerSample{ get; init; } // 8 or 16
|
|
public byte[] PcmBytes { get; init; } = Array.Empty<byte>();
|
|
public TimeSpan Duration { get; init; }
|
|
}
|
|
|
|
// An `AudioFalloff` helper lived here until 2026-08-08 (Campaign A slice A2).
|
|
// It had the right shape but a 1-metre reference distance where retail's is 5,
|
|
// a `PanFromRelative` that was linear in relative X over an invented 20-metre
|
|
// range where retail pans by the SINE of a compass bearing, and no audibility
|
|
// cutoff. Nothing ever called either method. Both are superseded by
|
|
// `RetailSoundMixer`, which carries the byte-decoded retail math.
|
|
|
|
/// <summary>
|
|
/// Interface the platform audio engine (AcDream.App layer) implements.
|
|
/// Core defines the contract; App implements via Silk.NET.OpenAL.
|
|
/// </summary>
|
|
public interface IAudioEngine : IDisposable
|
|
{
|
|
/// <summary>
|
|
/// acdream's own master, on top of retail's two live knobs. Retail has
|
|
/// three preferences — effect, ambient, and an interface one it registers
|
|
/// and never reads — and NO master and no music knob. See AP-174.
|
|
/// </summary>
|
|
float MasterVolume { get; set; }
|
|
|
|
/// <summary>Retail's <c>effect_sound_volume</c>.</summary>
|
|
float SfxVolume { get; set; }
|
|
|
|
/// <summary>Retail's <c>ambient_sound_volume</c> (applied twice — TS-65).</summary>
|
|
float AmbientVolume{ get; set; }
|
|
|
|
/// <summary>
|
|
/// Update the listener pose. Retail's listener is <c>SmartBox::viewer</c> —
|
|
/// the COLLIDED third-person camera Position, refreshed once per rendered
|
|
/// frame (<c>SmartBox::update_viewer</c> @ <c>0x00453CE0</c>), falling back
|
|
/// to the player's own position when the camera sweep fails. Only two
|
|
/// things are ever read out of it: the origin, for distance, and
|
|
/// <c>Frame::get_heading</c>, for pan. There is no up vector, no velocity,
|
|
/// and therefore no doppler and no elevation cue — which is why this takes
|
|
/// a single compass heading rather than a forward/up basis.
|
|
/// </summary>
|
|
void SetListener(float posX, float posY, float posZ, float headingDegrees);
|
|
|
|
/// <summary>Play a 2D UI sound (no falloff).</summary>
|
|
void PlayUi(SoundId id);
|
|
|
|
/// <summary>Play a 3D sound at a world position.</summary>
|
|
void Play3D(SoundId id, float x, float y, float z);
|
|
|
|
// A `StartAmbient(id, x, y, z)` / `StopAmbient(handle)` pair lived here
|
|
// until 2026-08-08 (Campaign A slice A5). It modelled a LOOPING,
|
|
// handle-owned ambient voice, which retail does not have: retail never sets
|
|
// the DirectSound loop flag, and a "continuous" ambient is a one-shot
|
|
// re-fired every min_rate seconds off an absolute-deadline queue, with a
|
|
// fresh variant pick and crossfade volume each time. The implementation was
|
|
// a stub that minted a handle and played nothing, while StopAmbient looked
|
|
// up a source that was never created. `AmbientSoundController` +
|
|
// `AmbientSoundScheduler` carry the real model.
|
|
|
|
// A `PlayMusic(resourceName, loop)` / `StopMusic()` pair and a `MusicVolume`
|
|
// knob lived here until 2026-08-08 (Campaign A slice A6). They modelled a
|
|
// subsystem retail does not have. The EoR client links a complete winmm
|
|
// midiStream player and never feeds it: `midiPlay` @ 0x00553390 has zero
|
|
// callers (its only textual occurrences in the 65 MB decomp are its own
|
|
// definition and its own queue drainer), both MIDI callbacks are statically
|
|
// null, the string "music" appears zero times, `SoundType` has no music
|
|
// member, `SoundManager::InitPrefs` registers no music preference, and the
|
|
// retail install ships no .mid/.mp3/.wav at all. The string-keyed signature
|
|
// was itself the tell — every other entry point in this interface is
|
|
// DID-keyed. What players remember as music is the AdminEnvirons UI stinger
|
|
// family (see `EnvironSoundCueMap`) and the intro AVI's audio track.
|
|
// Evidence: docs/research/2026-08-08-audio-retail-music-absence.md.
|
|
}
|