1// Playback level: peak normalization, or loudness normalization with a soft clipper.
3export type LevelMode = "peak" | "loudness";
5/** Returns a copy of `samples` scaled for playback or export, in [-1, 1]. */
6export function normalize(samples: Float32Array, fs: number, mode: LevelMode, peak = 0.95): Float32Array {
7 const out = new Float32Array(samples.length);
8 if (samples.length === 0) return out;
9 if (mode === "peak") {
10 let max = 0;
11 for (let k = 0; k < samples.length; k++) max = Math.max(max, Math.abs(samples[k]));
12 const g = max > 0 ? peak / max : 0;
13 for (let k = 0; k < samples.length; k++) out[k] = samples[k] * g;
14 return out;
15 }
16 // Loudness: scale the RMS of the sustained part (after the first 50 ms) to -16 dBFS,
17 // then soft-clip so that transients cannot exceed the peak.
18 const start = Math.min(samples.length - 1, Math.round(0.05 * fs));
19 let sum = 0;
20 for (let k = start; k < samples.length; k++) sum += samples[k] * samples[k];
21 const rms = Math.sqrt(sum / Math.max(1, samples.length - start));
22 const g = rms > 0 ? 10 ** (-16 / 20) / rms : 0;
23 for (let k = 0; k < samples.length; k++) out[k] = peak * Math.tanh((samples[k] * g) / peak);
24 return out;
25}