Files
minstrel/web/src/lib/player/gain.ts
T
bvandeusenandClaude Opus 5.5 94a9c8cbe3 chore(deps): SvelteKit 3 with adapter-static 4 and TypeScript 6, full-tree npm audit (#5021)
Merges Renovate's kit 3 (PR #149) and adapter-static 4 (PR #148) bumps,
plus the migration they need. The mechanical part is `sv migrate
sveltekit-3`, run one task at a time and reviewed:

- svelte.config.js is gone. Its options move into sveltekit() in
  vite.config.ts, exported as kitOptions so vitest.config.ts runs the
  same kit setup, including the $test-utils alias the tests import.
- $lib becomes #lib through package.json "imports". There is no
  src/lib/index, so only the "#lib/*" entry is kept.
- tsconfig extends $app/tsconfig.
- Peer floors raised to kit 3's requirements: svelte ^5.57.1, vite
  ^8.0.12, svelte-check ^4.7.5.

By hand, from the codemod's list of non-automated tasks:

- goto's replaceState option is now replace; keepFocus becomes
  reset: false. For the search typeahead, reset: false also stops the
  scroll-to-top, which is wanted while typing.
- The test setup mocks drop pushState/replaceState and $app/paths
  base/assets, which kit 3 removed, and mock refreshAll in place of
  invalidateAll.
- The other flagged files only read page.url or goto internal routes,
  so they needed no change.

TypeScript goes to ^6, not the ^7 Renovate offers: kit 3 declares
typescript ^6 as a peer and svelte-check 4.7 accepts ^5 || ^6. Move to
7 once both accept it.

With Tailwind 4 and kit 3 in, `npm audit` on the whole tree reports 0,
so the web lane now audits every dependency rather than only what
ships to browsers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 10:50:39 -04:00

69 lines
2.7 KiB
TypeScript

// Loudness normalization math for the web player (M464 #4999). Pure, so the
// rules are tested without an <audio> element or an AudioContext.
import type { NormalizationPrefs, ReplayGain } from '#lib/api/normalization.js';
import type { TrackRef } from '#lib/api/types.js';
// The server's gains are to the ReplayGain 2.0 reference.
const REFERENCE_LUFS = -18;
// Headroom mode raises a quiet track only until its true peak reaches this.
export const PEAK_CEILING_DBTP = -1;
// No track is raised more than this, whatever its measurement says: a
// near-silent track would otherwise come out as amplified noise.
export const MAX_BOOST_DB = 12;
/**
* Whether the track at `index` is being played as part of its album, in
* order: a neighbour in the queue is from the same album and sits on the
* right side of it. That is when album gain keeps the album's own dynamics
* (a quiet intro stays quiet); anywhere else track gain levels the mix.
*/
export function playingAsAlbum(queue: TrackRef[], index: number): boolean {
const cur = queue[index];
const curOrder = cur ? order(cur) : null;
if (!cur || curOrder === null) return false;
const inOrder = (n: TrackRef | undefined, before: boolean): boolean => {
if (!n || n.album_id !== cur.album_id) return false;
const o = order(n);
if (o === null) return false;
return before ? o < curOrder : o > curOrder;
};
return inOrder(queue[index - 1], true) || inOrder(queue[index + 1], false);
}
// Disc-major track order. A track with no number has no place in the order
// and is never evidence of album play.
function order(t: TrackRef): number | null {
if (t.track_number == null) return null;
return (t.disc_number ?? 1) * 1000 + t.track_number;
}
/**
* The gain to apply, in dB, for one track. 0 when leveling is off or the
* track has not been measured yet: an unmeasured track plays as mastered.
*/
export function gainDb(
prefs: NormalizationPrefs,
g: ReplayGain | undefined,
asAlbum: boolean
): number {
if (prefs.mode === 'off' || !g) return 0;
const wantAlbum = prefs.mode === 'album' || (prefs.mode === 'auto' && asAlbum);
// Album gain falls back to track gain while the album is still being
// measured; track gain never falls back to album gain.
const useAlbum = wantAlbum && g.album_gain !== undefined;
const gain = useAlbum ? g.album_gain : g.track_gain;
const peak = useAlbum ? g.album_peak : g.track_peak;
if (gain === undefined) return 0;
let db = gain + (prefs.target_lufs - REFERENCE_LUFS);
if (prefs.boost === 'headroom' && peak !== undefined && peak > 0) {
db = Math.min(db, PEAK_CEILING_DBTP - 20 * Math.log10(peak));
}
return Math.min(db, MAX_BOOST_DB);
}
export function dbToLinear(db: number): number {
return Math.pow(10, db / 20);
}