Home and End are the bass knob
The volume keys wanted a partner, and the bass trim could not be one as it stood: it scaled the sample data as it loaded, so by the time anyone pressed a key the audio was already sitting in OpenAL buffers and nothing short of a restart would move it. So the trim is now a per-zone gain applied in the mix instead. Each buffer's depth - how much of the low band it occupies - is still worked out once at load from its playback rate, but the trim itself is read every frame, which is what lets Home and End move it while sounds are playing. It is the better form regardless: no rewriting of sample data, and no quantisation on top of audio that has already been through one gain stage. Home raises, End lowers, in steps of 0.05, and the setting is written to bass.cfg beside the exe exactly as the volume writes volume.cfg. Together with PageUp and PageDown that is the amplifier and the crossover the cabinets had in hardware and a desktop does not. Builds clean, runs, and neither knob fires unprompted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
+102
-40
@@ -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);
|
||||
|
||||
+7
-3
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
//
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
+15
-11
@@ -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
|
||||
|
||||
+19
-10
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user