Files
muzick/workers/src/audio-analysis.ts
T
kami 4c48d11e9d
Typecheck / typecheck (backend) (push) Has been cancelled
Typecheck / typecheck (workers) (push) Has been cancelled
feat: enhance discovery, vibe sessions, and library enrichment
2026-08-01 14:40:48 +04:00

65 lines
2.4 KiB
TypeScript

/**
* Shared audio-analysis contract.
*
* Values in track_audio_features are deliberately modest: they are inputs to
* recommendation heuristics, not a claim that we can infer every Spotify-like
* descriptor from local DSP. Keep the persisted set restricted to fields we
* actually compute and validate before it reaches the database.
*/
export const AUDIO_ANALYSIS_VERSION = 2;
export const AUDIO_ANALYSIS_FEATURES = ['bpm', 'key', 'energy', 'danceability'] as const;
export type AudioAnalysisFeature = typeof AUDIO_ANALYSIS_FEATURES[number];
export const AUDIO_ANALYSIS_JOB_OPTIONS = {
attempts: 3,
backoff: { type: 'exponential', delay: 5_000 },
removeOnComplete: { age: 7 * 24 * 60 * 60, count: 10_000 },
removeOnFail: { age: 30 * 24 * 60 * 60, count: 10_000 },
} as const;
const ENERGY_FLOOR_DBFS = -48;
const ENERGY_CEILING_DBFS = -6;
export function clampUnit(value: number): number | null {
return Number.isFinite(value) ? Math.min(1, Math.max(0, value)) : null;
}
/**
* Convert Essentia's mean-square sample energy to a perceptually useful 0..1
* RMS level. A linear multiplier makes normal mastered music saturate at 1;
* dBFS preserves the distinction between quiet and loud recordings.
*/
export function normaliseMeanSquareEnergy(meanSquare: number): number | null {
if (!Number.isFinite(meanSquare) || meanSquare < 0) return null;
if (meanSquare === 0) return 0;
const rms = Math.sqrt(meanSquare);
const dbfs = 20 * Math.log10(rms);
return clampUnit((dbfs - ENERGY_FLOOR_DBFS) / (ENERGY_CEILING_DBFS - ENERGY_FLOOR_DBFS));
}
export function normaliseDanceability(value: number): number | null {
if (!Number.isFinite(value) || value < 0) return null;
// Essentia Danceability is normally 0..3. Values outside that range are
// clamped rather than allowed to poison downstream scoring.
return clampUnit(value / 3);
}
export function validBpm(value: number | null | undefined): number | null {
if (!Number.isFinite(value) || value == null || value < 30 || value > 300) return null;
return Math.round(value * 10) / 10;
}
export function validKey(value: string | null | undefined): string | null {
if (!value) return null;
const key = value.trim();
// Keep tag conventions such as "8A" intact, but reject malformed or
// unexpectedly large values before persisting them.
return key.length > 0 && key.length <= 32 ? key : null;
}
export function audioAnalysisJobId(trackId: string): string {
return `audio-${trackId}`;
}