From 4e8392fcfbb4edefd8c6b9459b0a37423697a487 Mon Sep 17 00:00:00 2001 From: Cyd Date: Thu, 6 Aug 2026 00:00:36 -0500 Subject: [PATCH] PageUp and PageDown are the volume knob The cabinets had no volume control - they ran at unity and left level to an external amplifier - so a player without that hardware had nowhere to turn it down but environ.ini and a restart. PageUp and PageDown now step the master volume by 0.05 while you play, from silent to double, and whatever you leave it on is written to volume.cfg beside the exe and used from then on. The environ.ini figure decides where a machine that has never been touched starts out; the keys are the knob, and a knob stays where it was left. Page keys because they produce no typed character, so they cannot collide with the character-keyed commands the engine already answers to, nothing else in RP binds them, and they are on every keyboard including tenkeyless. They are polled rather than read off the key-message path, which is worth recording because the message path looked like the obvious home for them and was tried first. RP's keyboard pump only takes WM_KEYUP, WM_SYSKEYUP and WM_CHAR off the front of the queue, and the front end runs message loops of its own, so key messages get raced for and lost: six deliberate, well-spaced presses arrived as two. Fine for the abort chord, useless for something you tap repeatedly to find a level. Reading key state directly costs nothing and cannot be dropped. That losses figure is a pre-existing property of the input path, not something this change introduced, and is worth knowing before anything else gets bound there. Builds clean, runs, and does not fire unprompted. The step function itself is proven - it was driven end to end through the message path before the switch, stepping the right way, clamping, and persisting. What I could not test from here is the polling trigger, because Windows would not hand the game foreground and injecting keys without it would have sprayed them across whatever else was open. Co-Authored-By: Claude Opus 5 (1M context) --- MUNGA_L4/L4AUDRND.cpp | 78 +++++++++++++++++++++++++++++++++++++++++-- MUNGA_L4/L4AUDRND.h | 8 +++++ MUNGA_L4/L4CTRL.cpp | 56 +++++++++++++++++++++++++++++++ RP_L4/RPL4ENVIRON.cpp | 7 +++- docs/CONTROLS.md | 15 +++++++++ docs/SOUND.md | 17 ++++++++-- 6 files changed, 176 insertions(+), 5 deletions(-) diff --git a/MUNGA_L4/L4AUDRND.cpp b/MUNGA_L4/L4AUDRND.cpp index 9565e0a..3e47625 100644 --- a/MUNGA_L4/L4AUDRND.cpp +++ b/MUNGA_L4/L4AUDRND.cpp @@ -6,6 +6,16 @@ #include "..\munga\notation.h" #include "openal/alc.h" +#include + +// +// Master volume limits, shared by the startup load and the PgUp/PgDn step. +// The file sits beside the exe with the other runtime state. +// +static const char kAudioVolumeFile[] = "volume.cfg"; +static const float kAudioVolumeStep = 0.05f; +static const float kAudioVolumeMax = 2.0f; + // //############################################################################# // L4AudioRenderer @@ -406,14 +416,33 @@ void { float value = (float)atof(setting); - if (value >= 0.0f && value <= 4.0f) + if (value >= 0.0f && value <= kAudioVolumeMax) { master_volume = value; - Tell("Audio master volume set to " << master_volume << "\n"); } } + // + // Whatever the player last set with the volume keys wins over the + // environ.ini figure: the keys are the amplifier knob, and a knob + // stays where it was left. environ.ini sets where it starts on a + // machine that has never been touched. + // + if (FILE *cfg = fopen(kAudioVolumeFile, "rt")) + { + float value = -1.0f; + + if (fscanf(cfg, "%f", &value) == 1 + && value >= 0.0f && value <= kAudioVolumeMax) + { + master_volume = value; + } + fclose(cfg); + } + + gRPMasterVolume = master_volume; alListenerf(AL_GAIN, master_volume); + Tell("Audio master volume " << (int)(master_volume * 100.0f + 0.5f) << "%\n"); } // @@ -1299,6 +1328,51 @@ Logical return resources_available; } +//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Master volume ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// +// The pod ran at unity and left volume to an external amplifier, so the game +// never had a level control. Standing in for that amplifier means the player +// needs to reach it while playing, not only through environ.ini -- hence the +// PgUp/PgDn binding in L4Application::KeyCommandMessageHandler. +// +// Page keys specifically: they produce no typed character, so they cannot +// collide with any of the engine's character-keyed commands the way '+'/'-' +// would, they are bound to nothing in any RP layout, and they exist on +// tenkeyless keyboards. +// +float gRPMasterVolume = 1.0f; + +void + RPAudioMasterVolumeStep(int direction) +{ + gRPMasterVolume += (direction > 0) ? kAudioVolumeStep : -kAudioVolumeStep; + + if (gRPMasterVolume < 0.0f) gRPMasterVolume = 0.0f; + if (gRPMasterVolume > kAudioVolumeMax) gRPMasterVolume = kAudioVolumeMax; + + // + // Snap to the step grid so repeated presses cannot drift on float error and + // land somewhere that never reads back as a round number. + // + gRPMasterVolume = + (float)((int)(gRPMasterVolume / kAudioVolumeStep + 0.5f)) * kAudioVolumeStep; + + alListenerf(AL_GAIN, gRPMasterVolume); + + // + // Persist immediately. A pod operator setting the level expects it to still + // be there after the cabinet is power-cycled, and there is no settings UI to + // hang it off. + // + if (FILE *cfg = fopen(kAudioVolumeFile, "wt")) + { + fprintf(cfg, "%.2f\n", gRPMasterVolume); + fclose(cfg); + } + + Tell("Audio master volume " << (int)(gRPMasterVolume * 100.0f + 0.5f) << "%\n"); +} + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ OpenAL source pool ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Sources are expensive to create and destroy and are a HARD per-context diff --git a/MUNGA_L4/L4AUDRND.h b/MUNGA_L4/L4AUDRND.h index e5aeee5..fca198e 100644 --- a/MUNGA_L4/L4AUDRND.h +++ b/MUNGA_L4/L4AUDRND.h @@ -6,6 +6,14 @@ #include "l4audres.h" #include "openal/al.h" +// +// Master volume, standing in for the amplifier the cabinets had. Stepped by +// PgUp/PgDn (L4APP.cpp) and persisted to volume.cfg; see L4AUDRND.cpp. +// +extern float gRPMasterVolume; + +void RPAudioMasterVolumeStep(int direction); + //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ L4AudioRenderer ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/MUNGA_L4/L4CTRL.cpp b/MUNGA_L4/L4CTRL.cpp index 98cb2e0..f5b1454 100644 --- a/MUNGA_L4/L4CTRL.cpp +++ b/MUNGA_L4/L4CTRL.cpp @@ -6,6 +6,7 @@ #include "l4ctrl.h" #include "l4keybd.h" #include "l4app.h" +#include "l4audrnd.h" // RPAudioMasterVolumeStep, for the PgUp/PgDn keys #include "l4dinput.h" #include "..\munga\appmgr.h" #include "dxutils.h" @@ -1513,6 +1514,61 @@ void // Update the PC keyboard mapping group //------------------------------------------------------------------------- // + // + //------------------------------------------------------------------------- + // Master volume, PgUp louder / PgDn quieter. + // + // The cabinets ran the game at unity and left level to an external + // amplifier and crossover; without that hardware the player has to be able + // to reach the volume while playing. + // + // POLLED, not taken off the key message below, and that is deliberate. The + // pump below only ever consumes WM_KEYUP / WM_SYSKEYUP / WM_CHAR from the + // front of the queue, and the front-end runs message loops of its own, so + // key messages are raced for and routinely lost -- measured here at roughly + // two of every six presses arriving. That is survivable for a one-shot like + // the abort chord; it is not survivable for a control you tap repeatedly to + // find a level. Reading the key state directly costs nothing and cannot be + // dropped. + // + // Page keys because they produce no typed character, so they cannot collide + // with the character-keyed commands the pump feeds, nothing else in RP binds + // them, and they exist on tenkeyless keyboards. + // + // The foreground check keeps an alt-tabbed game from eating the volume keys + // of whatever the player switched to. + //------------------------------------------------------------------------- + // + { + static int volume_up_held = 0; + static int volume_down_held = 0; + + int focused = 0; + + if (HWND foreground = GetForegroundWindow()) + { + DWORD foreground_process = 0; + + GetWindowThreadProcessId(foreground, &foreground_process); + focused = (foreground_process == GetCurrentProcessId()); + } + + const int up = focused && (GetAsyncKeyState(VK_PRIOR) & 0x8000) != 0; + const int down = focused && (GetAsyncKeyState(VK_NEXT) & 0x8000) != 0; + + if (up && !volume_up_held) + { + RPAudioMasterVolumeStep(+1); + } + if (down && !volume_down_held) + { + RPAudioMasterVolumeStep(-1); + } + + volume_up_held = up; + volume_down_held = down; + } + if (flags.keyboardExists) { //RB 1/20/07 diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index 2a29c9f..95b54ca 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -234,10 +234,15 @@ namespace "# certainly have neither, so these two stand in for them. Both default\n" "# to leaving the mix exactly as the pod played it.\n" "\n" -"# Master volume, 0.0 to 4.0, the amplifier's knob. 1.0 is unity. The\n" +"# Master volume, 0.0 to 2.0, the amplifier's knob. 1.0 is unity. The\n" "# sound effects now carry the pitch, layering and dynamics the original\n" "# AWE32 soundbanks ask for, which is a good deal livelier than earlier\n" "# 4.12 builds - lower this if the whole thing sits too hot.\n" +"#\n" +"# PageUp and PageDown change it while you play, in steps of 0.05, and\n" +"# whatever you leave it on is written to volume.cfg beside the exe and\n" +"# used from then on - so this line only decides where a machine that has\n" +"# never been touched starts out. Delete volume.cfg to come back here.\n" "#RP412AUDIOVOLUME=0.8\n" "\n" "# Bass trim, 0.0 to 1.0, the crossover's low band. 1.0 is the low end\n" diff --git a/docs/CONTROLS.md b/docs/CONTROLS.md index dabfd25..9c16977 100644 --- a/docs/CONTROLS.md +++ b/docs/CONTROLS.md @@ -126,6 +126,21 @@ Every one of these buttons is also **clickable on screen** — the red strips around each MFD and the amber strips beside the map are the same addresses, and they light up when the game commands their lamps. +### Volume + +`Page Up` and `Page Down` raise and lower the master volume in steps of +0.05, from silent up to 2.0. The level is written to `volume.cfg` beside +the exe as you change it and is picked up again next launch, so it stays +where you left it. `RP412AUDIOVOLUME` in `environ.ini` sets where a +machine starts out before anyone touches the keys; delete `volume.cfg` to +go back to it. + +The cabinets had no volume control of their own — they ran the game at +unity and left level and tone to an external amplifier and a 3-way +crossover. These keys stand in for the amplifier; `RP412AUDIOBASS` in +`environ.ini` stands in for the crossover's low band. See +[SOUND.md](SOUND.md). + ### Not bound by default The pilot keypad (`0x50`–`0x5F`) and external operator keypad diff --git a/docs/SOUND.md b/docs/SOUND.md index 74c0f3e..039cc5e 100644 --- a/docs/SOUND.md +++ b/docs/SOUND.md @@ -521,12 +521,25 @@ the pod played it: | Knob | Stands in for | Range | Default | |---|---|---|---| -| `RP412AUDIOVOLUME` | the amplifier's volume | 0.0 – 4.0 | 1.0 (unity, as the pod ran) | +| `RP412AUDIOVOLUME` / **PgUp**, **PgDn** | the amplifier's volume | 0.0 – 2.0 | 1.0 (unity, as the pod ran) | | `RP412AUDIOBASS` | the crossover's low band | 0.0 – 1.0 | 1.0 (as authored) | `RP412AUDIOVOLUME` is a straight `alListenerf(AL_GAIN, …)` at renderer init. There was no listener gain call at all before, so the default is a genuine -no-op. +no-op. **PgUp/PgDn** step it live by 0.05 and persist to `volume.cfg` beside the +exe, which then wins over `environ.ini` on the next launch — the env var decides +where an untouched machine starts, the keys are the knob, and a knob stays where +it was left. + +The keys are **polled** (`GetAsyncKeyState` in `LBE4ControlsManager::Execute`), +not taken off the key-message path, and that is not a style choice. RP's keyboard +pump consumes only `WM_KEYUP`/`WM_SYSKEYUP`/`WM_CHAR` from the front of the +queue while the front-end runs message loops of its own, so key messages are +raced for and routinely lost — measured at roughly **two of every six** presses +arriving when the volume was first wired through that path. Survivable for a +one-shot like the abort chord; not for a control you tap repeatedly to find a +level. This is a pre-existing property of the input path, worth knowing before +binding anything else to it. `RP412AUDIOBASS` is **not** an EFX filter, and the reason is worth recording: **the OpenAL this game ships implements only `AL_FILTER_LOWPASS`.** It is