refactor(lib): fold utils/audio + autodj-overlap into lib/audio (audio-transition infra; drains utils/playback)

This commit is contained in:
Psychotoxical
2026-06-30 20:30:07 +02:00
parent aea8b7750b
commit 31d4a87401
25 changed files with 27 additions and 27 deletions
+22
View File
@@ -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 230', () => {
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);
});
});
+28
View File
@@ -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 });
});
});
+25
View File
@@ -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));
}