mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
refactor(lib): fold utils/audio + autodj-overlap into lib/audio (audio-transition infra; drains utils/playback)
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
autodjMaxOverlapCapSec,
|
||||
DEFAULT_AUTODJ_OVERLAP_CAP_SEC,
|
||||
sanitizeAutodjOverlapCapSec,
|
||||
} from '@/lib/audio/autodjOverlapCap';
|
||||
|
||||
describe('autodjOverlapCap', () => {
|
||||
it('sanitizes cap seconds to 2–30', () => {
|
||||
expect(sanitizeAutodjOverlapCapSec(1)).toBe(2);
|
||||
expect(sanitizeAutodjOverlapCapSec(40)).toBe(30);
|
||||
expect(sanitizeAutodjOverlapCapSec(DEFAULT_AUTODJ_OVERLAP_CAP_SEC)).toBe(15);
|
||||
});
|
||||
|
||||
it('uses 12 s in auto mode', () => {
|
||||
expect(autodjMaxOverlapCapSec({ autodjOverlapCapMode: 'auto', autodjOverlapCapSec: 20 })).toBe(12);
|
||||
});
|
||||
|
||||
it('uses configured cap in limit mode', () => {
|
||||
expect(autodjMaxOverlapCapSec({ autodjOverlapCapMode: 'limit', autodjOverlapCapSec: 20 })).toBe(20);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AuthState } from '@/store/authStoreTypes';
|
||||
import { DYNAMIC_OVERLAP_HARD_CAP_SEC } from '@/lib/waveform/waveformSilence';
|
||||
|
||||
export type AutodjOverlapCapMode = 'auto' | 'limit';
|
||||
|
||||
export const AUTODJ_OVERLAP_CAP_MIN_SEC = 2;
|
||||
export const AUTODJ_OVERLAP_CAP_MAX_SEC = 30;
|
||||
export const DEFAULT_AUTODJ_OVERLAP_CAP_SEC = 15;
|
||||
|
||||
export function sanitizeAutodjOverlapCapSec(value: unknown): number {
|
||||
const n = typeof value === 'number' ? value : Number(value);
|
||||
if (!Number.isFinite(n)) return DEFAULT_AUTODJ_OVERLAP_CAP_SEC;
|
||||
return Math.min(AUTODJ_OVERLAP_CAP_MAX_SEC, Math.max(AUTODJ_OVERLAP_CAP_MIN_SEC, n));
|
||||
}
|
||||
|
||||
export function sanitizeAutodjOverlapCapMode(value: unknown): AutodjOverlapCapMode {
|
||||
return value === 'limit' ? 'limit' : 'auto';
|
||||
}
|
||||
|
||||
/** Upper bound (seconds) for content-driven AutoDJ overlap calculations. */
|
||||
export function autodjMaxOverlapCapSec(
|
||||
auth: Pick<AuthState, 'autodjOverlapCapMode' | 'autodjOverlapCapSec'>,
|
||||
): number {
|
||||
if (auth.autodjOverlapCapMode === 'auto') {
|
||||
return DYNAMIC_OVERLAP_HARD_CAP_SEC;
|
||||
}
|
||||
return sanitizeAutodjOverlapCapSec(auth.autodjOverlapCapSec);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ,
|
||||
audioPlayHiResBlendArgs,
|
||||
sanitizeHiResCrossfadeResampleHz,
|
||||
} from '@/lib/audio/hiResCrossfadeResample';
|
||||
|
||||
describe('hiResCrossfadeResample', () => {
|
||||
it('defaults unknown values to 44.1 kHz', () => {
|
||||
expect(sanitizeHiResCrossfadeResampleHz(undefined)).toBe(44_100);
|
||||
expect(sanitizeHiResCrossfadeResampleHz(48_000)).toBe(44_100);
|
||||
});
|
||||
|
||||
it('keeps allowed blend rates', () => {
|
||||
expect(sanitizeHiResCrossfadeResampleHz(88_200)).toBe(88_200);
|
||||
expect(sanitizeHiResCrossfadeResampleHz(96_000)).toBe(96_000);
|
||||
});
|
||||
|
||||
it('omits blend rate when hi-res is off', () => {
|
||||
expect(
|
||||
audioPlayHiResBlendArgs({
|
||||
enableHiRes: false,
|
||||
hiResCrossfadeResampleHz: DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ,
|
||||
}),
|
||||
).toEqual({ hiResEnabled: false, hiResCrossfadeResampleHz: null });
|
||||
});
|
||||
|
||||
it('forwards blend rate when hi-res is on', () => {
|
||||
expect(
|
||||
audioPlayHiResBlendArgs({ enableHiRes: true, hiResCrossfadeResampleHz: 96_000 }),
|
||||
).toEqual({ hiResEnabled: true, hiResCrossfadeResampleHz: 96_000 });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
/** Hi-Res transition blend output rates (Hz) for crossfade, AutoDJ, and gapless. */
|
||||
export const HI_RES_CROSSFADE_RESAMPLE_OPTIONS = [44_100, 88_200, 96_000] as const;
|
||||
|
||||
export type HiResCrossfadeResampleHz = (typeof HI_RES_CROSSFADE_RESAMPLE_OPTIONS)[number];
|
||||
|
||||
export const DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ: HiResCrossfadeResampleHz = 44_100;
|
||||
|
||||
export function sanitizeHiResCrossfadeResampleHz(value: unknown): HiResCrossfadeResampleHz {
|
||||
if (value === 88_200 || value === 96_000) return value;
|
||||
return DEFAULT_HI_RES_CROSSFADE_RESAMPLE_HZ;
|
||||
}
|
||||
|
||||
/** Args forwarded to `audio_play` for hi-res + crossfade blend rate. */
|
||||
export function audioPlayHiResBlendArgs(auth: {
|
||||
enableHiRes: boolean;
|
||||
hiResCrossfadeResampleHz: HiResCrossfadeResampleHz;
|
||||
}): {
|
||||
hiResEnabled: boolean;
|
||||
hiResCrossfadeResampleHz: number | null;
|
||||
} {
|
||||
return {
|
||||
hiResEnabled: auth.enableHiRes,
|
||||
hiResCrossfadeResampleHz: auth.enableHiRes ? auth.hiResCrossfadeResampleHz : null,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/** Pre-analysis level is defined relative to a −14 LUFS target; engine uses an offset for other targets. */
|
||||
export const LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS = -14 as const;
|
||||
|
||||
/**
|
||||
* dB actually applied by the engine (and placeholder math) for the current target.
|
||||
* Example: ref −4.5 dB at −14, at −12 → −2.5 dB.
|
||||
*/
|
||||
export function effectiveLoudnessPreAnalysisAttenuationDb(
|
||||
storedDbRelativeToRef: number,
|
||||
targetLufs: number,
|
||||
): number {
|
||||
const stepped = Math.round(storedDbRelativeToRef * 2) / 2;
|
||||
const effective = stepped + (targetLufs - LOUDNESS_PRE_ANALYSIS_REF_TARGET_LUFS);
|
||||
return Math.max(-24, Math.min(0, effective));
|
||||
}
|
||||
|
||||
/** Stored [−24, 0] dB, meaning “at −14 LUFS target”. */
|
||||
export function clampStoredLoudnessPreAnalysisAttenuationRefDb(v: number): number {
|
||||
const n = Math.round(v * 2) / 2;
|
||||
return Math.max(-24, Math.min(0, n));
|
||||
}
|
||||
Reference in New Issue
Block a user