release / govulncheck (push) Successful in 18s
release / web (push) Successful in 1m13s
release / go (push) Successful in 1m29s
release / integration (push) Successful in 4m26s
release / android (push) Successful in 5m20s
release / Build signed APK (releases and dev) (push) Successful in 5m31s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 32s
release / Verify release artifacts (tag releases only) (push) Skipped
Cuts go through element.volume. Boosts route the element through a Web Audio GainNode and a DynamicsCompressor (a -1 dBFS limiter in limiter mode, a pass-through otherwise), built only when a track wants a boost and only once an AudioContext is confirmed running; iOS never gets the graph. Auto mode takes album gain when a queue neighbour is from the same album in track order. Gains are fetched for the next 50 tracks as the queue moves, with a 10s deadline. The prefetch element is untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
63 lines
2.3 KiB
TypeScript
63 lines
2.3 KiB
TypeScript
import { api, apiFetch } from './client';
|
|
|
|
// Loudness normalization preference (M464 #4998). One per user, stored on
|
|
// the server so the web player, the Android app and casts agree.
|
|
export type NormalizationMode = 'off' | 'auto' | 'track' | 'album';
|
|
export type NormalizationTarget = -18 | -16 | -14;
|
|
export type NormalizationBoost = 'headroom' | 'limiter';
|
|
|
|
export type NormalizationPrefs = {
|
|
mode: NormalizationMode;
|
|
target_lufs: NormalizationTarget;
|
|
boost: NormalizationBoost;
|
|
};
|
|
|
|
// Must match library.DefaultNormalizationPrefs on the server.
|
|
export const DEFAULT_NORMALIZATION: NormalizationPrefs = {
|
|
mode: 'auto',
|
|
target_lufs: -18,
|
|
boost: 'headroom'
|
|
};
|
|
|
|
export const NORMALIZATION_MODES: NormalizationMode[] = ['off', 'auto', 'track', 'album'];
|
|
export const NORMALIZATION_TARGETS: NormalizationTarget[] = [-18, -16, -14];
|
|
|
|
export function isNormalizationPrefs(v: unknown): v is NormalizationPrefs {
|
|
if (typeof v !== 'object' || v === null) return false;
|
|
const p = v as Record<string, unknown>;
|
|
return (
|
|
NORMALIZATION_MODES.includes(p.mode as NormalizationMode) &&
|
|
NORMALIZATION_TARGETS.includes(p.target_lufs as NormalizationTarget) &&
|
|
(p.boost === 'headroom' || p.boost === 'limiter')
|
|
);
|
|
}
|
|
|
|
export function getNormalization(): Promise<NormalizationPrefs> {
|
|
return api.get<NormalizationPrefs>('/api/me/normalization');
|
|
}
|
|
|
|
// The whole preference is sent every time; the server keeps the last write.
|
|
export function putNormalization(p: NormalizationPrefs): Promise<NormalizationPrefs> {
|
|
return api.put<NormalizationPrefs>('/api/me/normalization', p);
|
|
}
|
|
|
|
// Per-track gains (#4997): dB to the -18 LUFS ReplayGain reference and linear
|
|
// peaks. A field is absent until the server has measured it.
|
|
export type ReplayGain = {
|
|
track_gain?: number;
|
|
track_peak?: number;
|
|
album_gain?: number;
|
|
album_peak?: number;
|
|
};
|
|
|
|
/**
|
|
* Gains for up to 200 tracks; an id missing from items has none yet. Given
|
|
* a deadline: the player waits on it, and a request that never answers
|
|
* would leave those tracks unleveled for the rest of the session.
|
|
*/
|
|
export function getReplayGain(ids: string[]): Promise<{ items: Record<string, ReplayGain> }> {
|
|
return apiFetch(`/api/tracks/replay-gain?ids=${ids.map(encodeURIComponent).join(',')}`, {
|
|
signal: AbortSignal.timeout(10_000)
|
|
}) as Promise<{ items: Record<string, ReplayGain> }>;
|
|
}
|