mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-21 08:00:43 +00:00
Apply perceptual curve to volume sliders (#4272)
## Problem Players reported having to turn the volume slider down to ~20% before noticing any change in loudness. The sliders fed their linear 0–1 position straight to Howler's `volume()`, which is linear amplitude gain. Human loudness perception is roughly logarithmic, so the top ~80% of the slider all sounds nearly identical — the classic linear-fader problem. ## Fix Square the slider position into a perceptual (audio-taper) gain inside `SoundManager`. The stored setting and the displayed `%` remain the intuitive linear slider position; only the gain handed to Howler is curved. | Slider | Old gain (linear) | New gain (x²) | |--------|-------------------|---------------| | 100% | 1.00 | 1.00 | | 90% | 0.90 | 0.81 | | 80% | 0.80 | 0.64 | | 50% | 0.50 | 0.25 | | 20% | 0.20 | 0.04 | Lowering the slider from 100→80 now produces an audible drop instead of nothing until ~20%. ## Notes - Quadratic (x²) was chosen as a balanced, conservative taper. Cubic (x³) would make the top-end drop-off even more immediate if preferred. - Existing saved settings are unaffected; the same slider position will simply sound slightly quieter, which is the intended correction. ## Tests Updated `SoundManager.test.ts` to assert the curved gain and added a dedicated test locking in the top-of-range behavior. All 18 tests pass. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -113,7 +113,8 @@ describe("SoundManager", () => {
|
||||
new SoundManager(bus, settings);
|
||||
const bgHowls = howlInstances.slice(0, 3);
|
||||
bgHowls.forEach((h) => {
|
||||
expect(h.volume).toHaveBeenCalledWith(0.5);
|
||||
// Slider position is curved (squared) into perceptual gain: 0.5² = 0.25.
|
||||
expect(h.volume).toHaveBeenCalledWith(0.25);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,8 +125,9 @@ describe("SoundManager", () => {
|
||||
howlInstances.length = 0;
|
||||
new SoundManager(bus, settings);
|
||||
bus.emit(new PlaySoundEffectEvent("click"));
|
||||
// Slider position 0.3 is curved (squared) into perceptual gain: 0.3² = 0.09.
|
||||
expect(howlCtor).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ volume: 0.3 }),
|
||||
expect.objectContaining({ volume: 0.09 }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -133,7 +135,8 @@ describe("SoundManager", () => {
|
||||
eventBus.emit(new SetBackgroundMusicVolumeEvent(0.7));
|
||||
const bgHowls = howlInstances.slice(0, 3);
|
||||
bgHowls.forEach((h) => {
|
||||
expect(h.volume).toHaveBeenCalledWith(0.7);
|
||||
// 0.7² = 0.49 perceptual gain.
|
||||
expect(h.volume).toHaveBeenCalledWith(0.7 * 0.7);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -142,7 +145,8 @@ describe("SoundManager", () => {
|
||||
const clickHowl = howlInstances[howlInstances.length - 1];
|
||||
clickHowl.volume.mockClear();
|
||||
eventBus.emit(new SetSoundEffectsVolumeEvent(0.4));
|
||||
expect(clickHowl.volume).toHaveBeenCalledWith(0.4);
|
||||
// 0.4² = 0.16 perceptual gain.
|
||||
expect(clickHowl.volume).toHaveBeenCalledWith(0.4 * 0.4);
|
||||
});
|
||||
|
||||
it("clamps volume values between 0 and 1", () => {
|
||||
@@ -159,6 +163,16 @@ describe("SoundManager", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("curves the slider position into perceptual gain so the top of the range is audibly distinct", () => {
|
||||
const bgHowls = howlInstances.slice(0, 3);
|
||||
// Linear gain would make 0.9 and 1.0 nearly indistinguishable; squaring
|
||||
// spreads the top end (0.9 → 0.81) so reductions are noticeable sooner.
|
||||
eventBus.emit(new SetBackgroundMusicVolumeEvent(0.9));
|
||||
bgHowls.forEach((h) => {
|
||||
expect(h.volume).toHaveBeenLastCalledWith(0.81);
|
||||
});
|
||||
});
|
||||
|
||||
it("dispose() unsubscribes from EventBus so events no longer play sounds", () => {
|
||||
eventBus.emit(new PlaySoundEffectEvent("click"));
|
||||
const clickHowl = howlInstances[howlInstances.length - 1];
|
||||
|
||||
Reference in New Issue
Block a user