Files
Psychotoxical-psysonic/src/store/loudnessGainCache.ts
T
Frank Stellmacher 18b88e3ae0 refactor(player): E.8 — extract loudness-gain cache encapsulation (#571)
The two parallel maps (`cachedLoudnessGainByTrackId`,
`stableLoudnessGainByTrackId`) plus their helpers
(`isReplayGainActive`, `loudnessCacheStateKeysForTrackId`,
`clearLoudnessCacheStateForTrackId`, `loudnessGainDbForEngineBind`)
move into `src/store/loudnessGainCache.ts`. The new module exposes a
thin API:

  - `getCachedLoudnessGain` / `setCachedLoudnessGain`
  - `hasStableLoudness` / `markLoudnessStable` (atomic set + stable)
  - `forgetLoudnessGain` (single-key delete) vs
    `clearLoudnessCacheStateForTrackId` (two-form delete)
  - existing names kept for `isReplayGainActive`,
    `loudnessGainDbForEngineBind`, `loudnessCacheStateKeysForTrackId`

playerStore's seven direct-access sites (delete pairs, set-pair, stable
flag check, cached read, neighbour-cache write) become API calls — the
mutables are no longer reachable from outside the module.

All helpers were file-private; no caller-side changes outside
playerStore's own imports. 22 focused tests pin the API surface including
the partial-vs-stable visibility split and the two delete-shape variants.

playerStore 3415 → 3389 LOC.
2026-05-12 13:54:41 +02:00

93 lines
3.6 KiB
TypeScript

import { useAuthStore } from './authStore';
/**
* In-memory cache of the per-track loudness normalization gain (dB). Two
* parallel maps:
*
* - `cachedLoudnessGainByTrackId` — the dB value last computed (from an
* `analysis_get_loudness_for_track` row, a partial-loudness event, or
* a placeholder-until-cache value).
* - `stableLoudnessGainByTrackId` — `true` once the value has been
* promoted to the final cached/analysis-confirmed form. Engine bind
* only trusts entries flagged stable; partial / placeholder values
* deliberately omit the flag so Rust uses its pre-trim default until
* the analysis catches up.
*
* Keys can land in either the bare Subsonic id form or the `stream:`
* prefixed form depending on which event surface wrote the entry —
* `loudnessCacheStateKeysForTrackId` returns the two forms a caller may
* need to look up or clear together.
*/
const cachedLoudnessGainByTrackId: Record<string, number> = {};
const stableLoudnessGainByTrackId: Record<string, true> = {};
/** Returns the two-form key list (bare id + `stream:<id>`) for paired lookups. */
export function loudnessCacheStateKeysForTrackId(trackId: string): string[] {
if (!trackId) return [];
const out: string[] = [trackId];
if (trackId.startsWith('stream:')) {
const bare = trackId.slice('stream:'.length);
if (bare) out.push(bare);
} else {
out.push(`stream:${trackId}`);
}
return out;
}
export function getCachedLoudnessGain(trackId: string): number | undefined {
return cachedLoudnessGainByTrackId[trackId];
}
export function setCachedLoudnessGain(trackId: string, gainDb: number): void {
cachedLoudnessGainByTrackId[trackId] = gainDb;
}
export function hasStableLoudness(trackId: string): boolean {
return Boolean(stableLoudnessGainByTrackId[trackId]);
}
/** Atomic: write the cached value AND mark it stable (analysis-confirmed). */
export function markLoudnessStable(trackId: string, gainDb: number): void {
cachedLoudnessGainByTrackId[trackId] = gainDb;
stableLoudnessGainByTrackId[trackId] = true;
}
/** Drop both maps for the literal track id (no stream-form expansion). */
export function forgetLoudnessGain(trackId: string): void {
delete cachedLoudnessGainByTrackId[trackId];
delete stableLoudnessGainByTrackId[trackId];
}
/** Drop both maps for each form of the track id (bare + `stream:<id>`). */
export function clearLoudnessCacheStateForTrackId(trackId: string): void {
for (const k of loudnessCacheStateKeysForTrackId(trackId)) {
delete cachedLoudnessGainByTrackId[k];
delete stableLoudnessGainByTrackId[k];
}
}
/**
* Pass to `audio_play` / `audio_chain_preload` only — DB-backed gain. Omit
* partial hints so Rust uses pre-trim until `analysis:loudness-partial` +
* `audio_update_replay_gain`.
*/
export function loudnessGainDbForEngineBind(trackId: string | undefined | null): number | null {
if (!trackId) return null;
if (!stableLoudnessGainByTrackId[trackId]) return null;
const v = cachedLoudnessGainByTrackId[trackId];
return Number.isFinite(v) ? v : null;
}
/** True when ReplayGain is selected AND user has it enabled in Settings. */
export function isReplayGainActive(): boolean {
const a = useAuthStore.getState();
return a.normalizationEngine === 'replaygain' && a.replayGainEnabled;
}
/** Test-only: wipe both maps so each spec starts clean. */
export function _resetLoudnessGainCacheForTest(): void {
for (const k of Object.keys(cachedLoudnessGainByTrackId)) delete cachedLoudnessGainByTrackId[k];
for (const k of Object.keys(stableLoudnessGainByTrackId)) delete stableLoudnessGainByTrackId[k];
}