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