/** * Defines an audio file's waveform, the bars the player seeks on: how many * peaks it has, and how they are taken from the samples. The server makes an * uploaded file's from its decoded audio (`api/lib/ffmpeg.ts`), and the * dump form a local file's in the browser (`src/utils/player/waveform.ts`), * both with {@link peaksOf}, so the two draw alike. * * @module */ /** How many peaks, and so bars, a waveform has. */ export const WAVEFORM_PEAKS = 150; /** * Returned by `GET /api/dumps/:dumpId/waveform` for an audio file dump, to the * player's waveform. */ export interface DumpWaveform { /** {@link WAVEFORM_PEAKS} heights from 0 to 1, as {@link peaksOf} gives. */ peaks: number[]; } /** * Returns `n` peaks of `samples`, each the loudest of its slice, scaled so the * loudest of all is 1 and rounded to three places: a quiet recording fills the * waveform as a loud one does. `samples` may be signed, as audio is, or * already peaks of smaller slices. Silence gives zeros. */ export function peaksOf( samples: ArrayLike, n: number = WAVEFORM_PEAKS, ): number[] { const peaks: number[] = []; for (let i = 0; i < n; i++) { // Slices cover every sample; with fewer samples than peaks, some share. const start = Math.floor((i * samples.length) / n); const end = Math.max(start + 1, Math.floor(((i + 1) * samples.length) / n)); let peak = 0; for (let j = start; j < Math.min(end, samples.length); j++) { peak = Math.max(peak, Math.abs(samples[j])); } peaks.push(peak); } const loudest = Math.max(...peaks); return peaks.map((p) => loudest > 0 ? Math.round((p / loudest) * 1000) / 1000 : 0 ); }