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