From dd2cb92b99dc6a829138f1cb7b244afc63243d42 Mon Sep 17 00:00:00 2001 From: Erik Date: Sat, 8 Aug 2026 22:58:49 +0200 Subject: [PATCH] =?UTF-8?q?chore(audio):=20Campaign=20A=20slice=20A6=20?= =?UTF-8?q?=E2=80=94=20delete=20what=20retail=20does=20not=20have?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../retail-divergence-register.md | 2 +- .../plans/2026-08-08-audio-parity-campaign.md | 58 +++++++++++++++++-- docs/research/deepdives/r05-audio-sound.md | 30 ++++++++++ src/AcDream.App/Audio/OpenAlAudioEngine.cs | 3 - .../Settings/RuntimeSettingsTargets.cs | 1 - src/AcDream.Core/Audio/AudioModel.cs | 27 +++++++-- .../Panels/Settings/AudioSettings.cs | 26 ++++++--- .../Panels/Settings/SettingsPanel.cs | 31 ++++------ .../Panels/Settings/SettingsStore.cs | 2 - .../Panels/Settings/AudioSettingsTests.cs | 12 ++-- .../Panels/Settings/SettingsPanelTests.cs | 12 ++-- .../Panels/Settings/SettingsStoreTests.cs | 7 +-- .../Panels/Settings/SettingsVMTests.cs | 6 +- 13 files changed, 152 insertions(+), 65 deletions(-) diff --git a/docs/architecture/retail-divergence-register.md b/docs/architecture/retail-divergence-register.md index 11b065b3..4f430413 100644 --- a/docs/architecture/retail-divergence-register.md +++ b/docs/architecture/retail-divergence-register.md @@ -341,7 +341,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps. | TS-6 | Weather particle emission suppressed — all weathery DayGroups map to Overcast (correct fog/cloud tone, no precipitation); retail's camera-attached weather subsystem not yet located in the decomp | `src/AcDream.Core/World/WeatherState.cs:200` | Decomp research verified the sky loop never reads `DefaultPesObjectId`; an earlier name-based rain spawn regressed (rained where retail didn't, 2026-04-23) — inventing a name→rain path is forbidden until the real subsystem is found | Rainy/snowy/stormy days never show retail's precipitation effects (permanent missing visuals until the subsystem is found and ported) | FUN_00508010 / FUN_0051bed0→FUN_0051bfb0 (negative findings) | | TS-7 | SkyObject `weather_enabled` gate not honored — weather-flagged sky objects (bit 0x04) always instantiate | `src/AcDream.Core/World/SkyDescLoader.cs:50` | No weather_enabled toggle exists yet; IsWeather flag parsed + documented as the gate to wire | Weather-only sky meshes (rain cylinders) appear where retail-with-weather-off suppresses them | `GameSky::MakeObject` 0x00506ee0, guard at decomp:268630 | | ~~TS-8~~ | **RETIRED 2026-07-31 (#268 stat-chain closeout).** `EnchantmentWireReader` parses the complete 0x02C2 payload and `GameEventWiring` publishes its StatMod type/key/value and bucket through the same `ActiveEnchantmentRecord` used at login. An end-to-end dispatch test proves a mid-session skill modifier changes `LocalPlayerState.GetEffectiveSkill` immediately. | `src/AcDream.Core.Net/Messages/EnchantmentWireReader.cs`; `src/AcDream.Core.Net/GameEventWiring.cs`; `tests/AcDream.Core.Net.Tests/GameEventWiringTests.cs` | — | — | `CEnchantmentRegistry::EnchantAttribute @ 0x00594570`; `CEnchantmentRegistry::EnchantSkill @ 0x005947B0`; holtburger `messages/magic/types.rs` | -| TS-9 | MP3 (0x55) and MS-ADPCM (0x02) waves undecoded — affected sounds skipped; retail decoded both via winmm ACM | `src/AcDream.Core/Audio/WaveDecoder.cs:33` | Managed decoder (NAudio or similar) deferred; PCM covers the vast majority of ~3500 waves | Any MP3 (common for music-ish clips) or ADPCM cue plays as silence where retail plays it | winmm ACM path (r05 §2.1) | +| TS-9 | **RE-SCOPED 2026-08-08 (Campaign A slice A6) — the blast radius is one wave, measured.** MP3 (0x55) and MS-ADPCM (0x02) waves still decode to null and play as silence where retail decoded both through the winmm ACM. What changed is the size of the problem: an independent walk of the shipped dats found exactly **1 MP3 among 786 waves** (`0x0A000393`, a ~2 s mono clip) and the row's original "any MP3 cue, common for music-ish clips" framing was wrong — it was written when we believed retail had a music system, and retail has none. A managed decoder for one two-second asset is not worth the dependency; the honest options are a ~50-line decode or an accepted-loss row, and this row is now the accepted-loss record. The ADPCM count has NOT been measured and is the one open question here. | `src/AcDream.Core/Audio/WaveDecoder.cs:33` | Measured rather than assumed. PCM covers 785 of 786 waves. | One ~2 s clip is silent, plus an unmeasured number of ADPCM clips. | winmm ACM path; dat census in `docs/research/2026-08-08-audio-retail-dat-layer.md` §4c | | TS-14 | Setup `Flatten` ignores ParentIndex part hierarchy (treats every placement as root-local); still in production use (GameWindow hydration, SkyRenderer) | `src/AcDream.Core/Meshing/SetupMesh.cs:15` | Most Setups are flat single-level rigs where root-local equals composed; hierarchical composition deferred ("Phase 3") | Any Setup with genuinely nested parts renders them at wrong offsets — mis-assembled multi-part objects in the Flatten paths | retail Setup ParentIndex chain composition | | TS-15 | No distance-driven degrade (LOD): always close-detail slot 0; plus the **#47** static `Degrades[0]` swap for 34-part humanoids only (structural sentinel detector) | `src/AcDream.Core/Meshing/GfxObjDegradeResolver.cs:57` (+ `src/AcDream.App/Rendering/GameWindow.cs:2608`) | LOD plumbing doesn't exist; slot 0 is correct for player + nearby NPCs; #47 closed the visible low-detail-arms bug without porting UpdateViewerDistance | Distant objects render max-detail (perf + wrong visuals where far meshes intentionally differ/hide parts); a future 34-part non-humanoid matching the sentinel gets the wrong mesh swap | `CPhysicsPart::UpdateViewerDistance` 0x0050E030; ::Draw 0x0050D7A0; ::LoadGfxObjArray 0x0050DCF0 | | TS-17 | AttackConditions suffix always empty in combat chat — formatting ported, wire bitflag not plumbed (Phase I.7 follow-up) | `src/AcDream.Core/Chat/CombatChatTranslator.cs:233` | Only the wire plumbing is missing; the holtburger-ported formatter is ready | Combat log omits "[Sneak Attack]"-style suffixes retail displays — hidden combat-mechanic feedback | holtburger chat.rs:588-595 | diff --git a/docs/plans/2026-08-08-audio-parity-campaign.md b/docs/plans/2026-08-08-audio-parity-campaign.md index c50c8ca1..de89a759 100644 --- a/docs/plans/2026-08-08-audio-parity-campaign.md +++ b/docs/plans/2026-08-08-audio-parity-campaign.md @@ -1,6 +1,6 @@ # Campaign A — Audio retail-feel parity -**Status: PROPOSED 2026-08-08 — awaiting user go.** Research phase complete; +**Status: CODE-COMPLETE 2026-08-08 — awaiting the user listening gate.** All six slices landed (see the ledger). Research phase complete; six-lane named-retail decode done, all load-bearing claims byte-verified against the PDB-paired 2013 binary (BN pseudo-C alone was NOT sufficient — see "BN traps" below). @@ -303,8 +303,54 @@ global kill switch. | Slice | Status | Commit | Gates | |---|---|---|---| | A1 | **COMPLETE** 2026-08-08 | `c69b3bde` | 42 Core audio tests; full Release suite 11,563 passed / 4 skipped / 0 failed. Closes #355. | -| A2 | **COMPLETE** 2026-08-08 | `6d0156cb` | 118 Core audio tests (mixer + voice pool + cookbook); full Release suite 11,639 passed / 4 skipped / 0 failed. Opus review run and applied — 2 HIGH (pan-law saturation, stale `FUN_00550ad0` header), 5 MEDIUM (untested clamp order / pan truncation / voice pool, dead `PlayingGain`, duplicated heading helper), 5 LOW. Retires AP-28; files AP-173, AP-174, TS-64, TS-65. **Owed: user listening gate.** | -| A3 | **COMPLETE** 2026-08-08 | `3fae0c7d` | 14 wire-conformance tests + 5 controller tests; full Release suite 11,658 passed / 4 skipped / 0 failed. **Owed: connected gate** (melee hit / pickup / lifestone audible against ACE). | -| A4 | **COMPLETE** 2026-08-08 | `489ea6ad` | UI bank DID resolved from the dats (`0x2000004B`, content-verified: exactly the 32 `UI_*` slots) + 21-case environ table, 30 new Core tests; full Release suite 11,691 passed / 4 skipped. Retires TS-54; narrows AP-115 to notice-only. **Owed: connected gate** (`@environs` thunder + recall cues audible). **Suite note:** two load-dependent measurement flakes were observed on separate full-suite runs (`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate`, and one unnamed Core.Net test); both pass in isolation and neither touches audio. | -| A5 | **COMPLETE** 2026-08-08 | `e81bc474` | 46 ambient conformance tests; full Release suite 11,739 passed / 4 skipped. Opus review run and applied — it caught a FATAL frame bug (cell offsets built in absolute world coordinates while the listener is in the streamed frame: every contribution culled at ~32 km, feature silent with no error), a per-entry vs per-cell denominator error that would have pushed multi-entry beds under the audibility floor, an infinite loop on a zero play-rate, and newly-audible ambients not firing until a full period later. Also moved beds onto retail's single 16-voice priority pool and made the in-block direction test XY-only. Retires TS-29; files TS-66 (`seen_outside` interiors), TS-67 (in-plane weight). **Owed: user listening gate.** | -| A6 | — | — | — | +| A2 | **COMPLETE** 2026-08-08 | `e42b9948` | 118 Core audio tests (mixer + voice pool + cookbook); full Release suite 11,639 passed / 4 skipped / 0 failed. Opus review run and applied — 2 HIGH (pan-law saturation, stale `FUN_00550ad0` header), 5 MEDIUM (untested clamp order / pan truncation / voice pool, dead `PlayingGain`, duplicated heading helper), 5 LOW. Retires AP-28; files AP-173, AP-174, TS-64, TS-65. **Owed: user listening gate.** | +| A3 | **COMPLETE** 2026-08-08 | `8bc458fb` | 14 wire-conformance tests + 5 controller tests; full Release suite 11,658 passed / 4 skipped / 0 failed. **Owed: connected gate** (melee hit / pickup / lifestone audible against ACE). | +| A4 | **COMPLETE** 2026-08-08 | `6eaa490b` | UI bank DID resolved from the dats (`0x2000004B`, content-verified: exactly the 32 `UI_*` slots) + 21-case environ table, 30 new Core tests; full Release suite 11,691 passed / 4 skipped. Retires TS-54; narrows AP-115 to notice-only. **Owed: connected gate** (`@environs` thunder + recall cues audible). **Suite note:** two load-dependent measurement flakes were observed on separate full-suite runs (`RuntimeCollisionReportingStateTests.WarmedSteadyContactRefreshDoesNotAllocate`, and one unnamed Core.Net test); both pass in isolation and neither touches audio. | +| A5 | **COMPLETE** 2026-08-08 | `7c4dd1ad` | 46 ambient conformance tests; full Release suite 11,739 passed / 4 skipped. Opus review run and applied — it caught a FATAL frame bug (cell offsets built in absolute world coordinates while the listener is in the streamed frame: every contribution culled at ~32 km, feature silent with no error), a per-entry vs per-cell denominator error that would have pushed multi-entry beds under the audibility floor, an infinite loop on a zero play-rate, and newly-audible ambients not firing until a full period later. Also moved beds onto retail's single 16-voice priority pool and made the in-block direction test XY-only. Retires TS-29; files TS-66 (`seen_outside` interiors), TS-67 (in-plane weight). **Owed: user listening gate.** | +| A6 | **COMPLETE** 2026-08-08 | `1b083463` | Full Release suite 11,739 passed / 4 skipped. Deleted the music API (`PlayMusic`/`StopMusic`/`MusicVolume` + the `AudioSettings.Music` knob); exposed the Ambient slider now that A5 drives it; reset the invented 0.8 ambient default to retail's 1.0; SUPERSEDED banner on `r05-audio-sound.md` listing its five wrong sections; TS-9 re-scoped to the measured one-wave blast radius. **Deferred:** #321's decode-dedup race (a pre-existing concurrency flake, not audio-parity behaviour — not fixed speculatively without reproducing it). | + +--- + +## CAMPAIGN CLOSEOUT (code-complete 2026-08-08) + +Six slices, six commits, `c69b3bde` → A6. The full Release suite ends at +**11,739 passed / 4 skipped / 0 failed**, up from 11,563 at campaign start; +the audio subsystem went from 42 tests (one of which pinned the wrong model) +to ~215 conformance tests written against byte-decoded values. + +**What was wrong, and is now right:** + +| Was | Now | +|---|---| +| Probability treated as a selection weight, and skipped entirely for the 4,183/4,184 single-entry sounds | Retail's uniform `(n−1)` pick plus an independent Bernoulli silence gate | +| OpenAL 3-D spatialization, `2/d` falloff, no cutoff | Retail's CPU 2-D model: `25·vol/d²` past a 5 m knee, −50 dB no-allocate floor (~94 m), ±15 dB sine pan with a 5 m dead zone | +| Voices evicted by gain | Evicted by DAT-authored float priority, strictly-less, ring order | +| `0xF750` unparsed — every server cue silent | Parsed, guid-queued-and-replayed, played at the wire volume | +| No interface sound bank | Bank DID resolved from the dats' EnumIDMap chain; portal cues + 21 AdminEnvirons stingers live | +| No ambient system at all | Region-authored, per-land-cell, 3×3 ring, deadline-queue one-shots with terrain crossfade | +| A music API | Deleted — retail has no music system | + +**Process notes worth carrying forward:** + +1. **Binary Ninja could not be trusted anywhere in this subsystem.** Five + float compares render inverted or with elided constants; `GetAttenuation` + prints `* 0f`, which ports as silence at every distance. Every load-bearing + value here came from byte-decoding the PDB-paired binary. The lane notes + record the verified values; a future reader should decode rather than + re-read the pseudo-C. +2. **Two research notes were wrong and were corrected in place** — lane 1's + 30 m decibel row (contradicted its own gain column) and lane 5's + transposed `GetByEnum` arguments (which would have made the UI bank + unresolvable). Both were caught by recomputing rather than copying. +3. **The reviews earned their cost.** A2's review caught a pan mapping that + saturated to full separation where retail gives 15 dB. A5's caught a + coordinate-frame error that would have made the entire ambient system + silent with nothing logged — the tests passed, the build was green, and it + would have failed only at the listening gate. +4. **An architecture guard caught a design error the tests could not**: + `ExtractedUpdateOwners_DoNotRetainAnonymousCallbacks` rejected an + `Action` frame hook and forced the typed `IAmbientFramePhase`. + +**Still owed:** the user listening gate (A2 falloff, A4 cues, A5 ambients) and +the connected gates for A3/A4. Open rows: AP-173, AP-174, TS-64, TS-65, TS-66, +TS-67, TS-9 (re-scoped), #321. diff --git a/docs/research/deepdives/r05-audio-sound.md b/docs/research/deepdives/r05-audio-sound.md index 069ad413..7dcc0fea 100644 --- a/docs/research/deepdives/r05-audio-sound.md +++ b/docs/research/deepdives/r05-audio-sound.md @@ -1,5 +1,35 @@ # R5 — Audio System Deep Dive +> **SUPERSEDED 2026-08-08 (Campaign A).** This document predates the named +> retail decomp and the byte-level verification pass; several of its +> load-bearing sections are wrong and were used to build code that had to be +> undone. Read the six lane notes instead — +> `docs/research/2026-08-08-audio-retail-*.md` — and +> `docs/plans/2026-08-08-audio-parity-campaign.md` for the summary. +> +> Specifically corrected: +> +> - **§5.1 falloff.** The `FUN_00550c30` reading is not retail's curve. +> `SoundManager::GetAttenuation` @ `0x00550020` is +> `g = dist < 5 ? vol : 25·vol/dist²`, clamped BEFORE one master multiply, +> then `ceil(20·log10 g)` with a hard −50 dB no-allocate floor (audible +> radius ≈94.2 m). Binary Ninja elides the x87 constants and prints `* 0f`; +> porting its rendering yields silence at every distance. +> - **§5.2 pan / §5.3 pool.** Pan is `(int)(−15·sin Δbearing)`, truncating +> toward zero, dead centre inside 5 m, with no front/back and no elevation. +> The voice allocator is `PlaySoundInternal` @ `0x0054FEC0` and it evicts on +> DAT-authored float PRIORITY, never gain; `FUN_00550AD0` is a hash-table +> constructor. +> - **§4 selection.** Probability is a Bernoulli play/skip GATE at the play +> site, not a selection weight; selection is a uniform index over `(n−1)`. +> - **§6 music.** There is no music system in retail EoR. The linked MIDI +> player has zero callers. The `PlayMusic` API this section inspired modelled +> dead code and was deleted at slice A6. +> - **§7 ambient.** Ambients are region-authored, per-LAND-CELL over a 3×3 +> landblock ring, and play as re-fired one-shots off an absolute-deadline +> queue — not landblock-attached loops. Indoors is silent by design. + + Ground-truth port plan for acdream's audio subsystem, derived from decompiled `acclient.exe`, cross-referenced against ACE (server), ACViewer (dat loader), holtburger (client protocol), AC2D (C++ client), and DatReaderWriter (dat diff --git a/src/AcDream.App/Audio/OpenAlAudioEngine.cs b/src/AcDream.App/Audio/OpenAlAudioEngine.cs index 2cd75c7f..4709e625 100644 --- a/src/AcDream.App/Audio/OpenAlAudioEngine.cs +++ b/src/AcDream.App/Audio/OpenAlAudioEngine.cs @@ -138,7 +138,6 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen // ── Public volume knobs ────────────────────────────────────────────────── public float MasterVolume { get; set; } = 1f; public float SfxVolume { get; set; } = 1f; - public float MusicVolume { get; set; } = 0.7f; public float AmbientVolume{ get; set; } = 0.8f; public bool IsAvailable => _available; @@ -555,8 +554,6 @@ public sealed unsafe class OpenAlAudioEngine : IAudioEngine, IWorldAudioQuiescen /// private float AmbientMaster => MasterVolume * AmbientVolume; - public void PlayMusic(string resourceName, bool loop) { /* R5 §6 MIDI — not ported */ } - public void StopMusic() { /* ditto */ } // ── Private helpers ────────────────────────────────────────────────────── diff --git a/src/AcDream.App/Settings/RuntimeSettingsTargets.cs b/src/AcDream.App/Settings/RuntimeSettingsTargets.cs index 2be9d202..e772e08f 100644 --- a/src/AcDream.App/Settings/RuntimeSettingsTargets.cs +++ b/src/AcDream.App/Settings/RuntimeSettingsTargets.cs @@ -125,7 +125,6 @@ internal sealed class RuntimeSettingsStartupTargets : IRuntimeSettingsStartupTar if (engine is not { IsAvailable: true }) return; engine.MasterVolume = audio.Master; - engine.MusicVolume = audio.Music; engine.SfxVolume = audio.Sfx; engine.AmbientVolume = audio.Ambient; } diff --git a/src/AcDream.Core/Audio/AudioModel.cs b/src/AcDream.Core/Audio/AudioModel.cs index ab28e7f9..2f023812 100644 --- a/src/AcDream.Core/Audio/AudioModel.cs +++ b/src/AcDream.Core/Audio/AudioModel.cs @@ -52,10 +52,17 @@ public sealed class WaveData /// public interface IAudioEngine : IDisposable { - /// Set master volume [0..1]. + /// + /// 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. + /// float MasterVolume { get; set; } + + /// Retail's effect_sound_volume. float SfxVolume { get; set; } - float MusicVolume { get; set; } + + /// Retail's ambient_sound_volume (applied twice — TS-65). float AmbientVolume{ get; set; } /// @@ -86,7 +93,17 @@ public interface IAudioEngine : IDisposable // up a source that was never created. `AmbientSoundController` + // `AmbientSoundScheduler` carry the real model. - /// Start music (fades out previous if any). - void PlayMusic(string resourceName, bool loop); - void StopMusic(); + // 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. } diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs b/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs index 43a1b474..1821efa1 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/AudioSettings.cs @@ -1,10 +1,17 @@ namespace AcDream.UI.Abstractions.Panels.Settings; /// -/// Audio mixer preferences persisted to settings.json. Drives the -/// existing Phase E.2 OpenAL engine — the host wires these values into -/// OpenAlAudioEngine.MasterVolume / SfxVolume / -/// MusicVolume / AmbientVolume on Save and on startup. +/// Audio mixer preferences persisted to settings.json. The host wires +/// these into OpenAlAudioEngine.MasterVolume / SfxVolume / +/// AmbientVolume on Save and on startup. +/// +/// +/// A Music field lived here until 2026-08-08 (Campaign A slice A6). +/// Retail has no music system to turn down — see the note on +/// IAudioEngine — so the knob moved nothing. Old settings.json +/// files carrying a "music" key still load: the reader simply ignores +/// keys it does not know, and the next save drops it. +/// /// /// /// Defaults match the engine's hard-coded starting values so a user @@ -14,15 +21,16 @@ namespace AcDream.UI.Abstractions.Panels.Settings; /// public sealed record AudioSettings( float Master, - float Music, float Sfx, float Ambient) { - /// Values used on first launch. Mirror the engine's - /// constructor-default Volume properties. + /// + /// Values used on first launch. Retail's own defaults are 1.0 for every + /// sound preference (SoundManager::InitPrefs @ 0x005503F0), + /// so ambient starts at unity rather than the invented 0.8. + /// public static AudioSettings Default { get; } = new( Master: 1.0f, - Music: 0.7f, Sfx: 1.0f, - Ambient: 0.8f); + Ambient: 1.0f); } diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs index 6008a6a2..fc928115 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsPanel.cs @@ -253,15 +253,15 @@ public sealed class SettingsPanel : IPanel } /// - /// Render the Audio tab — Master + SFX volume sliders (live preview - /// against the running OpenAL engine). Music + Ambient fields exist - /// in and persist round-trip, but their - /// sliders are intentionally hidden here because the underlying - /// engine paths (PlayMusic / StartAmbient) are stubbed for R5 MIDI - /// playback that hasn't shipped yet — exposing the sliders would be - /// "moving a knob that does nothing." When R5 lands, restore the - /// hidden sliders below and the JSON-persisted values will already - /// be in place. + /// Render the Audio tab — Master, SFX and Ambient volume sliders, all with + /// live preview against the running engine. + /// + /// + /// Ambient was hidden until Campaign A slice A5 because nothing drove it; + /// the region ambient soundscape now does, so the knob moves something and + /// is exposed. The Music slider is gone entirely: retail has no music + /// system, so there was never anything for it to turn down. + /// /// private void RenderAudioTab(IPanelRenderer renderer) { @@ -275,16 +275,9 @@ public sealed class SettingsPanel : IPanel if (renderer.SliderFloat("SFX", ref sfx, 0f, 1f)) _vm.SetAudio(a with { Sfx = sfx }); - // Music + Ambient hidden until R5 MIDI / ambient-loop engines - // exist. AudioSettings still carries the fields so the JSON - // round-trips and a future client doesn't drop them on save. - // - // float music = a.Music; - // if (renderer.SliderFloat("Music", ref music, 0f, 1f)) - // _vm.SetAudio(a with { Music = music }); - // float ambient = a.Ambient; - // if (renderer.SliderFloat("Ambient", ref ambient, 0f, 1f)) - // _vm.SetAudio(a with { Ambient = ambient }); + float ambient = a.Ambient; + if (renderer.SliderFloat("Ambient", ref ambient, 0f, 1f)) + _vm.SetAudio(a with { Ambient = ambient }); renderer.Spacing(); renderer.TextWrapped( diff --git a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs index 0c238961..0656dd7a 100644 --- a/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs +++ b/src/AcDream.UI.Abstractions/Panels/Settings/SettingsStore.cs @@ -108,7 +108,6 @@ public sealed class SettingsStore var d = AudioSettings.Default; return new AudioSettings( Master: ReadFloat(audio, "master", d.Master), - Music: ReadFloat(audio, "music", d.Music), Sfx: ReadFloat(audio, "sfx", d.Sfx), Ambient: ReadFloat(audio, "ambient", d.Ambient)); } @@ -588,7 +587,6 @@ public sealed class SettingsStore { ["ambient"] = a.Ambient, ["master"] = a.Master, - ["music"] = a.Music, ["sfx"] = a.Sfx, }; diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs index 42d6f817..28917d7a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/AudioSettingsTests.cs @@ -13,13 +13,13 @@ public sealed class AudioSettingsTests [Fact] public void Default_values_match_engine_constructor_defaults() { - // OpenAlAudioEngine ctor: Master=1.0, Music=0.7, Sfx=1.0, - // Ambient=0.8 — see src/AcDream.App/Audio/OpenAlAudioEngine.cs. + // Retail's SoundManager::InitPrefs @ 0x005503F0 defaults every sound + // preference to 1.0; acdream's extra master matches. The Music knob is + // gone (retail has no music system) and Ambient's invented 0.8 with it. var d = AudioSettings.Default; Assert.Equal(1.0f, d.Master); - Assert.Equal(0.7f, d.Music); Assert.Equal(1.0f, d.Sfx); - Assert.Equal(0.8f, d.Ambient); + Assert.Equal(1.0f, d.Ambient); } [Fact] @@ -35,8 +35,8 @@ public sealed class AudioSettingsTests [Fact] public void With_expression_clones_one_field() { - var d = AudioSettings.Default with { Music = 0.25f }; - Assert.Equal(0.25f, d.Music); + var d = AudioSettings.Default with { Ambient = 0.25f }; + Assert.Equal(0.25f, d.Ambient); // Other fields untouched. Assert.Equal(AudioSettings.Default.Master, d.Master); Assert.Equal(AudioSettings.Default.Sfx, d.Sfx); diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs index 7b420af9..55229823 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsPanelTests.cs @@ -349,11 +349,11 @@ public sealed class SettingsPanelTests [Fact] public void Audio_tab_when_active_renders_implemented_volume_sliders() { - // L.0 ships Master + SFX only — Music + Ambient sliders are - // hidden until R5 MIDI / ambient-loop engines exist. The - // AudioSettings record still carries those fields so the - // JSON round-trips, but the panel doesn't surface a slider - // that wouldn't actually do anything. + // The rule is "no slider that does nothing", and Campaign A changed + // what qualifies. Ambient is now surfaced because slice A5 gave it a + // region ambient system to drive. Music is gone entirely — retail has + // no music system, so slice A6 deleted the field rather than leaving a + // knob that could never do anything. var (panel, _, _, _) = Build(); var r = new FakePanelRenderer { ActiveTabLabel = "Audio" }; @@ -363,8 +363,8 @@ public sealed class SettingsPanelTests .Select(c => (string)c.Args[0]!).ToList(); Assert.Contains("Master", sliders); Assert.Contains("SFX", sliders); + Assert.Contains("Ambient", sliders); Assert.DoesNotContain("Music", sliders); - Assert.DoesNotContain("Ambient", sliders); } [Fact] diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs index b6012b6c..c602927a 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsStoreTests.cs @@ -156,7 +156,7 @@ public sealed class SettingsStoreTests : System.IDisposable public void SaveAudio_then_LoadAudio_round_trips_all_fields() { var store = new SettingsStore(_tempPath); - var original = new AudioSettings(Master: 0.3f, Music: 0.45f, Sfx: 0.9f, Ambient: 0.6f); + var original = new AudioSettings(Master: 0.3f, Sfx: 0.9f, Ambient: 0.6f); store.SaveAudio(original); var loaded = store.LoadAudio(); @@ -178,7 +178,6 @@ public sealed class SettingsStoreTests : System.IDisposable var loaded = store.LoadAudio(); Assert.Equal(0.25f, loaded.Master); - Assert.Equal(AudioSettings.Default.Music, loaded.Music); Assert.Equal(AudioSettings.Default.Sfx, loaded.Sfx); Assert.Equal(AudioSettings.Default.Ambient, loaded.Ambient); } @@ -200,10 +199,10 @@ public sealed class SettingsStoreTests : System.IDisposable { // Reverse order — audio must survive a subsequent display save. var store = new SettingsStore(_tempPath); - store.SaveAudio(AudioSettings.Default with { Music = 0.1f }); + store.SaveAudio(AudioSettings.Default with { Ambient = 0.1f }); store.SaveDisplay(DisplaySettings.Default with { ShowFps = true }); - Assert.Equal(0.1f, store.LoadAudio().Music); + Assert.Equal(0.1f, store.LoadAudio().Ambient); Assert.True(store.LoadDisplay().ShowFps); } diff --git a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs index b4a15961..7dafceeb 100644 --- a/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs +++ b/tests/AcDream.UI.Abstractions.Tests/Panels/Settings/SettingsVMTests.cs @@ -360,7 +360,7 @@ public sealed class SettingsVMTests [Fact] public void AudioDraft_initial_value_matches_persisted() { - var custom = AudioSettings.Default with { Master = 0.3f, Music = 0.1f }; + var custom = AudioSettings.Default with { Master = 0.3f, Ambient = 0.1f }; var (vm, _, _, _, _, _, _, _, _, _) = Build(persistedAudio: custom); Assert.Equal(custom, vm.AudioDraft); Assert.False(vm.HasUnsavedChanges); @@ -391,9 +391,9 @@ public sealed class SettingsVMTests [Fact] public void Cancel_reverts_audio_draft_to_persisted() { - var custom = AudioSettings.Default with { Music = 0.2f }; + var custom = AudioSettings.Default with { Ambient = 0.2f }; var (vm, _, _, _, _, _, _, _, _, _) = Build(persistedAudio: custom); - vm.SetAudio(vm.AudioDraft with { Music = 0.9f, Master = 0.3f }); + vm.SetAudio(vm.AudioDraft with { Ambient = 0.9f, Master = 0.3f }); Assert.True(vm.HasUnsavedChanges); vm.Cancel();