/ proof-of-concept / steelsim21
Sign in
proof-of-concept / steelsim21
steelsim21 / src / cli / metrics.ts
66 lines · 2.8 KBBlameHistoryRaw
1// Summary numbers of a rendered clip.
3import { FFT } from "../audio/fft.ts";
5export interface ClipMetrics {
6 peakPa: number;
7 peakDbSpl: number;
8 rmsPa: number;
9 /** Spectral centroid [Hz] of the sound between 0.2 s and 1.2 s. */
10 centroidHz: number;
11 /** Fitted 60 dB decay times [s] per band, from 0.2 s to the end (Infinity when not decaying). */
12 t60: Record<string, number>;
13 /** Band levels [dB] relative to the 100-500 Hz band, at 0.1 s. */
14 bandLevelsDb: Record<string, number>;
17const BANDS: [string, number, number][] = [["20-100", 20, 100], ["100-500", 100, 500], ["500-2k", 500, 2000], ["2k-8k", 2000, 8000]];
19export function clipMetrics(a: Float32Array, fs: number): ClipMetrics {
20 let peak = 0, sum = 0;
21 for (let k = 0; k < a.length; k++) { peak = Math.max(peak, Math.abs(a[k])); sum += a[k] * a[k]; }
22 const rms = Math.sqrt(sum / Math.max(1, a.length));
23 const win = Math.round(0.1 * fs);
24 const n = Math.floor(a.length / win);
25 const N = 1 << Math.ceil(Math.log2(win));
26 const fft = new FFT(N);
27 const re = new Float32Array(N), im = new Float32Array(N);
28 const rows: number[][] = [];
29 for (let i = 0; i < n; i++) {
30 re.fill(0); im.fill(0);
31 for (let k = 0; k < win; k++) re[k] = a[i * win + k] * (0.5 - 0.5 * Math.cos((2 * Math.PI * k) / win));
32 fft.transform(re, im);
33 const row: number[] = [];
34 for (const [, lo, hi] of BANDS) {
35 let e = 0;
36 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];
37 row.push(10 * Math.log10(e + 1e-20));
38 }
39 rows.push(row);
40 }
41 const t60: Record<string, number> = {};
42 const bandLevelsDb: Record<string, number> = {};
43 BANDS.forEach(([name], k) => {
44 const pts = rows.slice(2).map((r, i) => [(i + 2) * 0.1, r[k]]);
45 let slope = 0;
46 if (pts.length >= 3) {
47 const mx = pts.reduce((s, p) => s + p[0], 0) / pts.length, my = pts.reduce((s, p) => s + p[1], 0) / pts.length;
48 slope = pts.reduce((s, p) => s + (p[0] - mx) * (p[1] - my), 0) / pts.reduce((s, p) => s + (p[0] - mx) ** 2, 0);
49 }
50 t60[name] = slope < 0 ? 60 / -slope : Infinity;
51 bandLevelsDb[name] = rows.length > 1 ? rows[1][k] - rows[1][1] : 0;
52 });
53 // Spectral centroid, 0.2 to 1.2 s.
54 const seg = a.subarray(Math.round(0.2 * fs), Math.min(a.length, Math.round(1.2 * fs)));
55 const M = 1 << Math.ceil(Math.log2(Math.max(2, seg.length)));
56 const fft2 = new FFT(M);
57 const r2 = new Float32Array(M), i2 = new Float32Array(M);
58 r2.set(seg);
59 fft2.transform(r2, i2);
60 let num = 0, den = 0;
61 for (let b = 1; b < M / 2; b++) {
62 const p = r2[b] * r2[b] + i2[b] * i2[b];
63 num += ((b * fs) / M) * p; den += p;
64 }
65 return { peakPa: peak, peakDbSpl: peak > 0 ? 20 * Math.log10(peak / 2e-5) : -Infinity, rmsPa: rms, centroidHz: den > 0 ? num / den : 0, t60, bandLevelsDb };
↑↓ move↵openescclose