refactor(player): E.37 — extract updateReplayGainForCurrentTrack (#601)

Move the ~50-LOC `updateReplayGainForCurrentTrack` action body into
`runUpdateReplayGainForCurrentTrack(set, get)` in a dedicated module,
following the helper-with-thin-wrapper pattern from
`applyQueueHistorySnapshot` (E.30) rather than a single-action
factory.

The action recomputes the normalization snapshot + pushes fresh
ReplayGain/loudness state to the engine when mode toggles change
or the loudness cache fills mid-playback.

Pure code-move. playerStore.ts: 991 → 938 LOC (−53).
This commit is contained in:
Frank Stellmacher
2026-05-12 21:14:24 +02:00
committed by GitHub
parent 55b2b12fa5
commit 14346d1482
2 changed files with 85 additions and 55 deletions
+2 -55
View File
@@ -11,8 +11,6 @@ import { useHotCacheStore } from './hotCacheStore';
import { orbitBulkGuard } from '../utils/orbitBulkGuard';
import { useOrbitStore } from './orbitStore';
import { estimateLivePosition } from '../api/orbit';
import { loudnessGainPlaceholderUntilCacheDb } from '../utils/loudnessPlaceholder';
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../utils/loudnessPreAnalysisSlider';
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
import { shuffleArray } from '../utils/shuffleArray';
import { songToTrack } from '../utils/songToTrack';
@@ -37,16 +35,12 @@ import {
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
import { isInOrbitSession } from './orbitSession';
import {
getCachedLoudnessGain,
hasStableLoudness,
isReplayGainActive,
loudnessCacheStateKeysForTrackId,
loudnessGainDbForEngineBind,
} from './loudnessGainCache';
import {
clearAllPlaybackScheduleTimers,
} from './scheduleTimers';
import { invokeAudioUpdateReplayGainDeduped } from './normalizationIpcDedupe';
import { touchHotCacheOnPlayback } from './hotCacheTouch';
import { applySkipStarOnManualNext } from './skipStarRating';
import { resetLoudnessBackfillStateForTrackId } from './loudnessBackfillState';
@@ -149,6 +143,7 @@ import type { PlayerState, Track } from './playerStoreTypes';
export type { PlayerState, Track };
import { createLastfmActions } from './lastfmActions';
import { createMiscActions } from './miscActions';
import { runUpdateReplayGainForCurrentTrack } from './updateReplayGainAction';
import { createQueueMutationActions } from './queueMutationActions';
import { createScheduleActions } from './scheduleActions';
import { createTransportLightActions } from './transportLightActions';
@@ -902,55 +897,7 @@ export const usePlayerStore = create<PlayerState>()(
});
},
updateReplayGainForCurrentTrack: () => {
const { currentTrack, queue, queueIndex, volume } = get();
if (!currentTrack || !currentTrack.id) return;
const authState = useAuthStore.getState();
const prev = queueIndex > 0 ? queue[queueIndex - 1] : null;
const next = queueIndex + 1 < queue.length ? queue[queueIndex + 1] : null;
const replayGainDb = resolveReplayGainDb(
currentTrack, prev, next,
isReplayGainActive(), authState.replayGainMode,
);
const replayGainPeak = isReplayGainActive()
? (currentTrack.replayGainPeak ?? null)
: null;
const normalization = deriveNormalizationSnapshot(currentTrack, queue, queueIndex);
const cachedLoud = getCachedLoudnessGain(currentTrack.id);
const cachedLoudDb = Number.isFinite(cachedLoud) ? cachedLoud! : null;
const haveStableLoud = hasStableLoudness(currentTrack.id);
const preEffForNorm = effectiveLoudnessPreAnalysisAttenuationDb(
authState.loudnessPreAnalysisAttenuationDb,
authState.loudnessTargetLufs,
);
const preAnalysisPlaceholderDb =
normalization.normalizationEngineLive === 'loudness'
&& cachedLoudDb == null
&& !haveStableLoud
&& Number.isFinite(preEffForNorm)
? loudnessGainPlaceholderUntilCacheDb(
authState.loudnessTargetLufs,
preEffForNorm,
)
: null;
set(prevState => ({
normalizationNowDb:
normalization.normalizationEngineLive === 'loudness'
? (cachedLoudDb ?? preAnalysisPlaceholderDb ?? prevState.normalizationNowDb)
: normalization.normalizationNowDb,
normalizationTargetLufs: normalization.normalizationTargetLufs,
normalizationEngineLive: normalization.normalizationEngineLive,
}));
invokeAudioUpdateReplayGainDeduped({
volume,
replayGainDb,
replayGainPeak,
loudnessGainDb: currentTrack ? (getCachedLoudnessGain(currentTrack.id) ?? null) : null,
preGainDb: authState.replayGainPreGainDb,
fallbackDb: authState.replayGainFallbackDb,
});
},
updateReplayGainForCurrentTrack: () => runUpdateReplayGainForCurrentTrack(set, get),
};
},
{
+83
View File
@@ -0,0 +1,83 @@
import { effectiveLoudnessPreAnalysisAttenuationDb } from '../utils/loudnessPreAnalysisSlider';
import { loudnessGainPlaceholderUntilCacheDb } from '../utils/loudnessPlaceholder';
import { resolveReplayGainDb } from '../utils/resolveReplayGainDb';
import { useAuthStore } from './authStore';
import {
getCachedLoudnessGain,
hasStableLoudness,
isReplayGainActive,
} from './loudnessGainCache';
import { deriveNormalizationSnapshot } from './normalizationSnapshot';
import { invokeAudioUpdateReplayGainDeduped } from './normalizationIpcDedupe';
import type { PlayerState } from './playerStoreTypes';
type SetState = (
partial: Partial<PlayerState> | ((state: PlayerState) => Partial<PlayerState>),
) => void;
type GetState = () => PlayerState;
/**
* Recompute and push fresh ReplayGain + loudness state to the engine
* for the currently-playing track. Called when ReplayGain mode /
* pre-gain / fallback toggles change while a track is mid-playback,
* or when the loudness cache for the current track resolves later
* than the initial play.
*
* - Re-derives the normalization snapshot (target LUFS, engine-live
* mode) from the current queue context.
* - Picks a placeholder loudness gain when in loudness mode and the
* real cached gain isn't ready yet, so the UI's "now playing -X dB"
* readout doesn't drop to zero between the cache miss and the
* eventual cache fill.
* - Pushes the new audio parameters to the Rust engine via the
* deduplicated IPC channel.
*/
export function runUpdateReplayGainForCurrentTrack(set: SetState, get: GetState): void {
const { currentTrack, queue, queueIndex, volume } = get();
if (!currentTrack || !currentTrack.id) return;
const authState = useAuthStore.getState();
const prev = queueIndex > 0 ? queue[queueIndex - 1] : null;
const next = queueIndex + 1 < queue.length ? queue[queueIndex + 1] : null;
const replayGainDb = resolveReplayGainDb(
currentTrack, prev, next,
isReplayGainActive(), authState.replayGainMode,
);
const replayGainPeak = isReplayGainActive()
? (currentTrack.replayGainPeak ?? null)
: null;
const normalization = deriveNormalizationSnapshot(currentTrack, queue, queueIndex);
const cachedLoud = getCachedLoudnessGain(currentTrack.id);
const cachedLoudDb = Number.isFinite(cachedLoud) ? cachedLoud! : null;
const haveStableLoud = hasStableLoudness(currentTrack.id);
const preEffForNorm = effectiveLoudnessPreAnalysisAttenuationDb(
authState.loudnessPreAnalysisAttenuationDb,
authState.loudnessTargetLufs,
);
const preAnalysisPlaceholderDb =
normalization.normalizationEngineLive === 'loudness'
&& cachedLoudDb == null
&& !haveStableLoud
&& Number.isFinite(preEffForNorm)
? loudnessGainPlaceholderUntilCacheDb(
authState.loudnessTargetLufs,
preEffForNorm,
)
: null;
set(prevState => ({
normalizationNowDb:
normalization.normalizationEngineLive === 'loudness'
? (cachedLoudDb ?? preAnalysisPlaceholderDb ?? prevState.normalizationNowDb)
: normalization.normalizationNowDb,
normalizationTargetLufs: normalization.normalizationTargetLufs,
normalizationEngineLive: normalization.normalizationEngineLive,
}));
invokeAudioUpdateReplayGainDeduped({
volume,
replayGainDb,
replayGainPeak,
loudnessGainDb: getCachedLoudnessGain(currentTrack.id) ?? null,
preGainDb: authState.replayGainPreGainDb,
fallbackDb: authState.replayGainFallbackDb,
});
}