// Summary numbers of a rendered clip. import { FFT } from "../audio/fft.ts"; export interface ClipMetrics { peakPa: number; peakDbSpl: number; rmsPa: number; /** Spectral centroid [Hz] of the sound between 0.2 s and 1.2 s. */ centroidHz: number; /** Fitted 60 dB decay times [s] per band, from 0.2 s to the end (Infinity when not decaying). */ t60: Record; /** Band levels [dB] relative to the 100-500 Hz band, at 0.1 s. */ bandLevelsDb: Record; } const BANDS: [string, number, number][] = [["20-100", 20, 100], ["100-500", 100, 500], ["500-2k", 500, 2000], ["2k-8k", 2000, 8000]]; export function clipMetrics(a: Float32Array, fs: number): ClipMetrics { let peak = 0, sum = 0; for (let k = 0; k < a.length; k++) { peak = Math.max(peak, Math.abs(a[k])); sum += a[k] * a[k]; } const rms = Math.sqrt(sum / Math.max(1, a.length)); const win = Math.round(0.1 * fs); const n = Math.floor(a.length / win); const N = 1 << Math.ceil(Math.log2(win)); const fft = new FFT(N); const re = new Float32Array(N), im = new Float32Array(N); const rows: number[][] = []; for (let i = 0; i < n; i++) { re.fill(0); im.fill(0); for (let k = 0; k < win; k++) re[k] = a[i * win + k] * (0.5 - 0.5 * Math.cos((2 * Math.PI * k) / win)); fft.transform(re, im); const row: number[] = []; for (const [, lo, hi] of BANDS) { let e = 0; for (let b = Math.floor((lo * N) / fs); b < Math.min(N / 2, Math.ceil((hi * N) / fs)); b++) e += re[b] * re[b] + im[b] * im[b]; row.push(10 * Math.log10(e + 1e-20)); } rows.push(row); } const t60: Record = {}; const bandLevelsDb: Record = {}; BANDS.forEach(([name], k) => { const pts = rows.slice(2).map((r, i) => [(i + 2) * 0.1, r[k]]); let slope = 0; if (pts.length >= 3) { const mx = pts.reduce((s, p) => s + p[0], 0) / pts.length, my = pts.reduce((s, p) => s + p[1], 0) / pts.length; slope = pts.reduce((s, p) => s + (p[0] - mx) * (p[1] - my), 0) / pts.reduce((s, p) => s + (p[0] - mx) ** 2, 0); } t60[name] = slope < 0 ? 60 / -slope : Infinity; bandLevelsDb[name] = rows.length > 1 ? rows[1][k] - rows[1][1] : 0; }); // Spectral centroid, 0.2 to 1.2 s. const seg = a.subarray(Math.round(0.2 * fs), Math.min(a.length, Math.round(1.2 * fs))); const M = 1 << Math.ceil(Math.log2(Math.max(2, seg.length))); const fft2 = new FFT(M); const r2 = new Float32Array(M), i2 = new Float32Array(M); r2.set(seg); fft2.transform(r2, i2); let num = 0, den = 0; for (let b = 1; b < M / 2; b++) { const p = r2[b] * r2[b] + i2[b] * i2[b]; num += ((b * fs) / M) * p; den += p; } return { peakPa: peak, peakDbSpl: peak > 0 ? 20 * Math.log10(peak / 2e-5) : -Infinity, rmsPa: rms, centroidHz: den > 0 ? num / den : 0, t60, bandLevelsDb }; }