Skip to content

Non-linear volume controls - #66

Open
AntonioSanFer wants to merge 2 commits into
swingmx:masterfrom
AntonioSanFer:volume_controls
Open

Non-linear volume controls#66
AntonioSanFer wants to merge 2 commits into
swingmx:masterfrom
AntonioSanFer:volume_controls

Conversation

@AntonioSanFer

Copy link
Copy Markdown

feat: make volume controls non-linear

The problem

HTMLMediaElement.volume is a linear amplitude multiplier, but loudness perception
is logarithmic. The slider fed its position straight into it, so the same amount of
travel did wildly different things depending on where you grabbed it:

slider move perceived change
0.9 → 1.0 0.92 dB — barely audible
0.1 → 0.2 6.02 dB — huge

In practice the top half of the slider did almost nothing and every meaningful
adjustment was crammed into the bottom tenth.
While listening music, I spent most of the time tweaking the volume at the 1-7 range,
having anything above 10 just being really loud and without much perceptible difference.

The approach

Split the one value into two:

  • settings.volume — the slider position (0–1). What the user sees and adjusts,
    and what gets persisted.
  • settings.volume_gain (new getter) — that position run through a decibel-linear
    taper, and the only thing that ever reaches an audio element.

The taper is dB = -50 × (1 - position), with a linear fade to true silence below 5%
so the bottom of the slider still means off rather than -50 dB.
This results in constant 5 dB per 10% of travel, anywhere on the slider.

50 dB was picked to keep ~0.5 dB of resolution per 1% of travel — fine enough that small
nudges near the top are audible, wide enough that the low end stays usable for quiet
listening.

Background: https://www.dr-lex.be/info-stuff/volumecontrols.html

Because the stored value is still a slider position, no migration is needed — existing
persisted settings keep working as-is.

Changes

  • utils/audio/volume.ts (new) — volumeToGain, volumeRampGain, clamp.
  • stores/settings/index.tsvolume_gain getter; setVolume clamps and pushes the gain.
  • stores/player.ts — the three places that assigned settings.volume to an audio
    element now use volume_gain.
  • utils/audio/crossFade.ts — ramps over slider positions, so fades are even by ear
    (a linear-amplitude ramp spends most of its time inaudible, then lurches). Also switched
    from drift-prone step accumulation to elapsed-time progress, 25 ms ticks instead of 100 ms
    (10 steps over a 1 s fade was a 5 dB zipper), a clearer fade_out: boolean in place of
    start_volume, and a WeakMap so a new fade cancels a stale one on the same element
    during rapid track switching.
  • components/BottomBar/Volume.vue — wheel deltas normalised per deltaMode and capped
    at 3 notches (raw deltaY is wildly device-dependent; one flick of a high-res wheel could
    slam silent → full), with sub-step movement carried between events so slow trackpad
    scrolling still registers. Added aria-label / aria-valuetext to the range input.

Testing

Verified end-to-end against ghcr.io/swingmx/swingmusic:latest in Docker, serving this
branch's own build.
Also: tsc --noEmit clean on all touched files (6 pre-existing errors elsewhere), eslint
clean, vite build succeeds, and the curve logic was exercised offline for monotonicity and
range across 10k points, endpoints, NaN / out-of-range inputs, ramp clamping, and wheel
behaviour.

audio element volume is a linear amplitude multiplier, but loudness perception
is logarithmic, meaning the most notable volume adjustments always happened on
the 0-10 scale.

settings.volume now holds the slider position and is mapped through a decibel-linear
taper before reaching the audio elements.

ramp crossfades over slider positions instead of raw amplitude
normalise mouse wheel deltas across devices and carry sub-step movement
use the unused full volume icon above 80%
it looked horrible above 80%. back to the original mute/low/mid icons,
the threshold and markup are identical to master again.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant