diff --git a/MUNGA_L4/L4AUDIO.cpp b/MUNGA_L4/L4AUDIO.cpp index 8ed7d2a..be2858b 100644 --- a/MUNGA_L4/L4AUDIO.cpp +++ b/MUNGA_L4/L4AUDIO.cpp @@ -1182,9 +1182,11 @@ void // non-positional cockpit path which never took distance attenuation anyway. // const float direct_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue()); + PatchResource *direct_patch = Cast_Object(PatchResource*, GetAudioResource()); for (int i=0; i < channelSet.count; i++) { - alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); + alSourcef(channelSet.sources[i], AL_GAIN, + volume_scale * volume_scale * direct_patch->GetZoneBassGain(i)); alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * direct_note_pitch); } } @@ -1574,10 +1576,12 @@ void } const float dynamic_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue()); + PatchResource *dynamic_patch = Cast_Object(PatchResource*, GetAudioResource()); for (int i=0; i < channelSet.count; i++) { alSource3f(channelSet.sources[i],AL_POSITION,pos.x,pos.y,pos.z); - alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); + alSourcef(channelSet.sources[i], AL_GAIN, + volume_scale * volume_scale * dynamic_patch->GetZoneBassGain(i)); alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * dynamic_note_pitch); alSource3f(channelSet.sources[i],AL_VELOCITY,-relative_velocity.x,-relative_velocity.y,-relative_velocity.z); @@ -2163,7 +2167,8 @@ void const float static_note_pitch = RPNotePitchFactor((int)GetCurrentNoteValue()); for (int i=0; i < channelSet.count; i++) { - alSourcef(channelSet.sources[i], AL_GAIN, volume_scale * volume_scale); + alSourcef(channelSet.sources[i], AL_GAIN, + volume_scale * volume_scale * patch_resource->GetZoneBassGain(i)); alSourcef(channelSet.sources[i], AL_PITCH, (float)relativePitch * static_note_pitch); alSource3f(channelSet.sources[i],AL_POSITION,relative_position.x,relative_position.y,relative_position.z); diff --git a/MUNGA_L4/L4AUDLVL.cpp b/MUNGA_L4/L4AUDLVL.cpp index 35baa6b..d5ef095 100644 --- a/MUNGA_L4/L4AUDLVL.cpp +++ b/MUNGA_L4/L4AUDLVL.cpp @@ -310,3 +310,19 @@ MIDINRPNValue Check(patch_level_of_detail); return patch_level_of_detail->GetMaxMIDIFilterCutoff(); } + +// +//############################################################################# +//############################################################################# +// +float + PatchResource::GetZoneBassGain(int zone_index) +{ + Check(this); + + PatchLevelOfDetail *patch_level_of_detail = + Cast_Object(PatchLevelOfDetail*, GetAudioLevelOfDetail()); + + Check(patch_level_of_detail); + return patch_level_of_detail->GetZoneBassGain(zone_index); +} diff --git a/MUNGA_L4/L4AUDLVL.h b/MUNGA_L4/L4AUDLVL.h index 73dca59..0065680 100644 --- a/MUNGA_L4/L4AUDLVL.h +++ b/MUNGA_L4/L4AUDLVL.h @@ -37,6 +37,12 @@ struct PRESETINFO extern PRESETINFO allPresets[2][100]; +// +// Defined in L4AUDRES.cpp; declared here rather than including that header so +// the level-of-detail and resource headers stay independent of each other. +// +float RPBufferBassGain(int buffer_index); + bool PRESET_isImplemented(int bank, int preset); int PRESET_getNumSamples(int bank, int preset); SAMPLEINFO PRESET_getSampleInfo(int bank, int preset, int sampleInd); @@ -69,6 +75,14 @@ public: GetVoiceCount() {return PRESET_getNumSamples(bankID,patchID);} + // + // Gain this zone takes from the Home/End bass trim, 1.0 when untouched. + // + float + GetZoneBassGain(int zone_index) + {return RPBufferBassGain( + PRESET_getSampleInfo(bankID,patchID,zone_index).bufferIndex);} + // //----------------------------------------------------------------------- // BuildFromPage @@ -163,6 +177,12 @@ public: void SetupPatch(SourceSet sourceSet); + // + // Gain this zone takes from the Home/End bass trim, 1.0 when untouched. + // + float + GetZoneBassGain(int zone_index); + MIDINRPNValue GetMaxMIDIFilterCutoff(); }; diff --git a/MUNGA_L4/L4AUDRES.cpp b/MUNGA_L4/L4AUDRES.cpp index 6fd43d8..eb79704 100644 --- a/MUNGA_L4/L4AUDRES.cpp +++ b/MUNGA_L4/L4AUDRES.cpp @@ -20,17 +20,18 @@ int g_numBuffers; //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Bass trim ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // -// RP412AUDIOBASS, 0.0..1.0, default 1.0 (the mix exactly as authored). +// RP412AUDIOBASS, 0.0..1.0, default 1.0 (the mix exactly as authored), stepped +// live by the Home/End keys. // // The arcade pod ran the game at unity and did its volume and tone shaping in // hardware -- an external amplifier and a 3-way crossover. A desktop player has // neither, so the low band needs a control in software. This is the crossover's -// low trim; RP412AUDIOVOLUME (L4AUDRND.cpp) is the amplifier's. +// low trim; the master volume (L4AUDRND.cpp) is the amplifier's. // // It cannot be an EFX filter: the OpenAL this game ships implements only -// AL_FILTER_LOWPASS, so there is no high-shelf or bandpass to lean on, and the -// one direct filter per source is already carrying the authored brightness -// model. Instead the trim scales sample data as it is loaded. +// AL_FILTER_LOWPASS, so there is no low shelf or bandpass to lean on, and the +// one direct filter a source gets is already carrying the authored brightness +// model. So the trim is a GAIN, applied per zone in the mix. // // That works because of HOW the low end is built. RP's soundbanks carry their // weight in discrete deep layer zones whose per-zone tuning bakes out to a very @@ -41,62 +42,104 @@ int g_numBuffers; // overall cut. // // Ramp: untouched at or above 22050 Hz, full trim at or below 5512 Hz, log -// interpolated between, so nothing steps abruptly at a threshold. +// interpolated between, so nothing steps abruptly at a threshold. Each buffer's +// DEPTH is fixed at load; the trim itself is read at mix time, which is what +// lets the keys move it while sounds are playing. // static const ALsizei kBassTrimFullRate = 5512; // at/below: full trim static const ALsizei kBassTrimNoneRate = 22050; // at/above: untouched +static const char kBassTrimFile[] = "bass.cfg"; +static const float kBassTrimStep = 0.05f; + +static float *g_bufferBassDepth = NULL; // one per loaded buffer +static float g_bassTrim = 1.0f; + +// +// How much of the trim a buffer at this rate takes: 0 = untouched, 1 = fully. +// +static float + RPBassDepthForRate(ALsizei rate) +{ + if (rate >= kBassTrimNoneRate) return 0.0f; + if (rate <= kBassTrimFullRate) return 1.0f; + + const float span = (float)log((double)kBassTrimNoneRate / (double)kBassTrimFullRate); + + return (float)log((double)kBassTrimNoneRate / (double)rate) / span; +} void - RPApplyBassTrim(char *data, int bytes, unsigned long format_bits, ALsizei rate) + RPBassTrimInitialize() { - static float s_trim = -1.0f; + g_bassTrim = 1.0f; - if (s_trim < 0.0f) + if (const char *setting = getenv("RP412AUDIOBASS")) { - s_trim = 1.0f; + float value = (float)atof(setting); - if (const char *setting = getenv("RP412AUDIOBASS")) + if (value >= 0.0f && value <= 1.0f) { - float value = (float)atof(setting); - - if (value >= 0.0f && value <= 1.0f) - { - s_trim = value; - Tell("Audio bass trim set to " << s_trim << "\n"); - } + g_bassTrim = value; } } - if (s_trim >= 0.999f || format_bits != 16 || data == NULL || bytes <= 0) - { - return; // default: nothing to do - } - if (rate >= kBassTrimNoneRate) - { - return; // this zone is not part of the low band - } - // - // How much of the trim this zone takes, 0 at the no-trim rate rising to 1 at - // the full-trim rate, in octaves so the ramp is even to the ear. + // Whatever the player last set with the keys wins, exactly as the master + // volume behaves -- environ.ini only decides where an untouched machine + // starts out. // - float depth = 1.0f; - - if (rate > kBassTrimFullRate) + if (FILE *cfg = fopen(kBassTrimFile, "rt")) { - const float span = (float)log((double)kBassTrimNoneRate / (double)kBassTrimFullRate); - depth = (float)log((double)kBassTrimNoneRate / (double)rate) / span; + float value = -1.0f; + + if (fscanf(cfg, "%f", &value) == 1 && value >= 0.0f && value <= 1.0f) + { + g_bassTrim = value; + } + fclose(cfg); } - const float scale = 1.0f - (1.0f - s_trim) * depth; + Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n"); +} - short *samples = (short *)data; - const int count = bytes / 2; +void + RPBassTrimStep(int direction) +{ + g_bassTrim += (direction > 0) ? kBassTrimStep : -kBassTrimStep; - for (int s = 0; s < count; s++) + if (g_bassTrim < 0.0f) g_bassTrim = 0.0f; + if (g_bassTrim > 1.0f) g_bassTrim = 1.0f; + + g_bassTrim = (float)((int)(g_bassTrim / kBassTrimStep + 0.5f)) * kBassTrimStep; + + if (FILE *cfg = fopen(kBassTrimFile, "wt")) { - samples[s] = (short)(samples[s] * scale); + fprintf(cfg, "%.2f\n", g_bassTrim); + fclose(cfg); } + + Tell("Audio bass trim " << (int)(g_bassTrim * 100.0f + 0.5f) << "%\n"); +} + +float + RPBassTrim() +{ + return g_bassTrim; +} + +// +// The gain a zone takes at the current trim. 1.0 whenever the player has not +// touched it, so the default costs one multiply by one. +// +float + RPBufferBassGain(int buffer_index) +{ + if (g_bassTrim >= 0.999f || g_bufferBassDepth == NULL + || buffer_index < 0 || buffer_index >= g_numBuffers) + { + return 1.0f; + } + return 1.0f - (1.0f - g_bassTrim) * g_bufferBassDepth[buffer_index]; } //############################################################################# @@ -647,6 +690,18 @@ void g_buffers = NULL; g_numBuffers = 0; } + else + { + // + // Parallel to g_buffers: how much of the bass trim each zone takes. + // + RPBassTrimInitialize(); + g_bufferBassDepth = new float[g_numBuffers]; + for (int b = 0; b < g_numBuffers; b++) + { + g_bufferBassDepth[b] = 0.0f; + } + } } int bufferInd = 0; @@ -728,7 +783,14 @@ void sf_read_raw(file,data,size); sf_close(file); - RPApplyBassTrim(data, size, formatBits, alSampleRate); + // + // Record which band this zone sits in, for the Home/End bass + // trim. Fixed per buffer; the trim itself is read at mix time. + // + if (g_bufferBassDepth != NULL) + { + g_bufferBassDepth[bufferInd] = RPBassDepthForRate(alSampleRate); + } //Feed the buffer alBufferData(g_buffers[bufferInd],format,data,size,alSampleRate); diff --git a/MUNGA_L4/L4AUDRES.h b/MUNGA_L4/L4AUDRES.h index 6cf7880..a7d2740 100644 --- a/MUNGA_L4/L4AUDRES.h +++ b/MUNGA_L4/L4AUDRES.h @@ -10,10 +10,14 @@ extern ALuint *g_buffers; extern int g_numBuffers; // -// RP412AUDIOBASS low-band trim, applied to sample data as buffers are loaded. -// See the comment block in L4AUDRES.cpp for why it lives here and not in EFX. +// RP412AUDIOBASS low-band trim, stepped live by the Home/End keys. Applied as +// a per-zone gain in the mix; see the comment block in L4AUDRES.cpp for why it +// lives here and not in EFX. // -void RPApplyBassTrim(char *data, int bytes, unsigned long format_bits, ALsizei rate); +void RPBassTrimInitialize(); +void RPBassTrimStep(int direction); +float RPBassTrim(); +float RPBufferBassGain(int buffer_index); //class AudioHardware; diff --git a/MUNGA_L4/L4AUDRND.cpp b/MUNGA_L4/L4AUDRND.cpp index 3e47625..8d1c69a 100644 --- a/MUNGA_L4/L4AUDRND.cpp +++ b/MUNGA_L4/L4AUDRND.cpp @@ -446,9 +446,9 @@ void } // - // The bass trim is not set here: it scales sample data as buffers are - // loaded, so it lives in the resource manager (RPAudioBassTrim, - // L4AUDRES.cpp) and is read there. PreloadResources runs below. + // The bass trim is not set here: it is a per-zone gain owned by the + // resource manager (L4AUDRES.cpp), which needs the buffers to exist + // first. PreloadResources initialises it below. // } diff --git a/MUNGA_L4/L4CTRL.cpp b/MUNGA_L4/L4CTRL.cpp index f5b1454..da0900c 100644 --- a/MUNGA_L4/L4CTRL.cpp +++ b/MUNGA_L4/L4CTRL.cpp @@ -7,6 +7,7 @@ #include "l4keybd.h" #include "l4app.h" #include "l4audrnd.h" // RPAudioMasterVolumeStep, for the PgUp/PgDn keys +#include "l4audres.h" // RPBassTrimStep, for the Home/End keys #include "l4dinput.h" #include "..\munga\appmgr.h" #include "dxutils.h" @@ -1539,9 +1540,14 @@ void // of whatever the player switched to. //------------------------------------------------------------------------- // + // Home/End do the same for the bass trim - the crossover's low band to + // PgUp/PgDn's amplifier. + // { static int volume_up_held = 0; static int volume_down_held = 0; + static int bass_up_held = 0; + static int bass_down_held = 0; int focused = 0; @@ -1555,6 +1561,8 @@ void const int up = focused && (GetAsyncKeyState(VK_PRIOR) & 0x8000) != 0; const int down = focused && (GetAsyncKeyState(VK_NEXT) & 0x8000) != 0; + const int bass_up = focused && (GetAsyncKeyState(VK_HOME) & 0x8000) != 0; + const int bass_down = focused && (GetAsyncKeyState(VK_END) & 0x8000) != 0; if (up && !volume_up_held) { @@ -1564,9 +1572,19 @@ void { RPAudioMasterVolumeStep(-1); } + if (bass_up && !bass_up_held) + { + RPBassTrimStep(+1); + } + if (bass_down && !bass_down_held) + { + RPBassTrimStep(-1); + } volume_up_held = up; volume_down_held = down; + bass_up_held = bass_up; + bass_down_held = bass_down; } if (flags.keyboardExists) diff --git a/RP_L4/RPL4ENVIRON.cpp b/RP_L4/RPL4ENVIRON.cpp index 95b54ca..aedb7ef 100644 --- a/RP_L4/RPL4ENVIRON.cpp +++ b/RP_L4/RPL4ENVIRON.cpp @@ -250,8 +250,12 @@ namespace "# engines and explosions - deep layers earlier builds played at the\n" "# wrong rate, so they barely sounded at all. Lower this to pull that\n" "# back; it eases in below 22kHz of playback rate and reaches full cut\n" -"# on the deepest layers, leaving the mid and top alone. Applied as the\n" -"# sounds load, so it takes a restart like everything else here.\n" +"# on the deepest layers, leaving the mid and top alone.\n" +"#\n" +"# Home and End change it while you play, in steps of 0.05, and what you\n" +"# leave it on is written to bass.cfg beside the exe and used from then\n" +"# on - so this line only decides where an untouched machine starts.\n" +"# Delete bass.cfg to come back here.\n" "#RP412AUDIOBASS=0.7\n" "\n" "# Invert the stick on top of whatever bindings.txt produces:\n" diff --git a/docs/CONTROLS.md b/docs/CONTROLS.md index 9c16977..3e616aa 100644 --- a/docs/CONTROLS.md +++ b/docs/CONTROLS.md @@ -126,19 +126,23 @@ 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 +### Volume and bass -`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. +| Key | Does | +|---|---| +| `Page Up` / `Page Down` | master volume, in steps of 0.05 from silent to 2.0 | +| `Home` / `End` | bass, in steps of 0.05 from as-authored down to none | -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 +Both take effect as you press them, including on sounds already playing, +and both are written beside the exe as you change them — `volume.cfg` and +`bass.cfg` — so they stay where you left them. `RP412AUDIOVOLUME` and +`RP412AUDIOBASS` in `environ.ini` set where a machine starts out before +anyone touches the keys; delete the matching `.cfg` to go back to them. + +The cabinets had neither control. They ran the game at unity and left +level and tone to an external amplifier and a 3-way crossover, so these +keys are standing in for hardware the arcade had and you do not: Page +Up/Down is the amplifier, Home/End is the crossover's low band. See [SOUND.md](SOUND.md). ### Not bound by default diff --git a/docs/SOUND.md b/docs/SOUND.md index 039cc5e..9d4dbf3 100644 --- a/docs/SOUND.md +++ b/docs/SOUND.md @@ -522,7 +522,10 @@ the pod played it: | Knob | Stands in for | Range | Default | |---|---|---|---| | `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) | +| `RP412AUDIOBASS` / **Home**, **End** | the crossover's low band | 0.0 – 1.0 | 1.0 (as authored) | + +Both step live by 0.05 and persist beside the exe (`volume.cfg`, `bass.cfg`), +which then win over `environ.ini` next launch. `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 @@ -549,15 +552,21 @@ not OpenAL Soft, and it rejects both `AL_FILTER_HIGHPASS` and the neat answer, carrying the authored brightness model on `GAINHF` and the trim on `GAINLF` across the one direct filter a source gets. It is not available. -So the trim scales sample data as buffers load. That works because of *how* RP's -low end is built: the weight sits in discrete deep layer zones whose per-zone -tuning bakes out to a very low playback rate — 13 zones below 8 kHz, 3.4 to 5.2 -octaves below their recorded pitch, against 81% of the set at 22 kHz and above. -A zone's baked rate is a reliable proxy for which band it occupies, so -attenuating the low-rate zones is a real low-band trim rather than a blunt -overall cut. The ramp is untouched at/above 22050 Hz, full trim at/below -5512 Hz, log-interpolated between. At `0.7` that is −3.1 dB on the deepest -layers, −1.4 dB at 11 kHz, nothing from 22 kHz up. +So the trim is a **per-zone gain applied in the mix**. That works because of +*how* RP's low end is built: the weight sits in discrete deep layer zones whose +per-zone tuning bakes out to a very low playback rate — 13 zones below 8 kHz, +3.4 to 5.2 octaves below their recorded pitch, against 81% of the set at 22 kHz +and above. A zone's baked rate is a reliable proxy for which band it occupies, +so attenuating the low-rate zones is a real low-band trim rather than a blunt +overall cut. Each buffer's *depth* is fixed at load from its rate; the trim +itself is read at mix time, which is what lets Home/End move it while sounds are +playing. The ramp is untouched at/above 22050 Hz, full trim at/below 5512 Hz, +log-interpolated between. At `0.7` that is −3.1 dB on the deepest layers, +−1.4 dB at 11 kHz, nothing from 22 kHz up. + +(An earlier revision scaled the PCM at load instead. That could never be a live +knob — the samples are already in OpenAL buffers by the time a key is pressed — +and it risked quantisation on top. The gain form is both live and cleaner.) **A caution for anyone extending the EFX work:** `EFX_Initialize` reads `alGetError()` after configuring the scratch filter, so asking for a filter type