"use client"; import { useState } from "react"; import { DEFAULT_TARGET, loudnessSpread, loudnessVerdict, type Loudness, type LoudnessTarget, } from "@/lib/loudness-types"; // What has been measured, and a button for what has not. // // Nothing is measured on render -- see the route for why. A row that has never // been measured says so; it does not show a zero, and it does not quietly // borrow another row's figure. type Row = { rel: string; loudness: Loudness | null }; export default function LoudnessTable({ song, initial, target = DEFAULT_TARGET, }: { song: string; initial: Row[]; target?: LoudnessTarget; }) { const [rows, setRows] = useState(initial); const [busy, setBusy] = useState(null); const [error, setError] = useState(null); const measure = async (rel: string) => { setBusy(rel); setError(null); try { const res = await fetch("/api/browse/loudness", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ song, rel }), cache: "no-store", }); const j = (await res.json()) as { error?: string; loudness?: Loudness }; if (!res.ok || !j.loudness) throw new Error(j.error || `HTTP ${res.status}`); const measured = j.loudness; setRows((r) => r.map((x) => (x.rel === rel ? { ...x, loudness: measured } : x))); } catch (e) { setError(`${rel}: ${e instanceof Error ? e.message : String(e)}`); } finally { setBusy(null); } }; /** Sequential, not parallel: each one is an ffmpeg decode of a whole render. */ const measureAll = async () => { for (const r of rows) { if (!r.loudness) await measure(r.rel); } }; const done = rows.filter((r) => r.loudness); const spread = loudnessSpread(done.map((r) => r.loudness!)); return (
loudness target {target.lufs} LUFS · {target.truePeak} dBTP {spread !== null && ( // Two cuts of one song that went through different chains and differ // by 2 LU is a bug, and nothing else reports it. 1 ? "text-[var(--color-dirty)]" : "text-[var(--color-meter)]"}`} data-spread={spread} > spread {spread} LU across {done.length} measured )} {rows.some((r) => !r.loudness) && ( )}
{rows.map((r) => { const v = r.loudness ? loudnessVerdict(r.loudness, target) : null; return ( ); })}
file LUFS LRA dBTP reading
{r.rel} {r.loudness?.integrated?.toFixed(1) ?? "—"} {r.loudness?.lra?.toFixed(1) ?? "—"} target.truePeak ? "text-[var(--color-bad)]" : "text-[var(--color-dim)]" }`} > {r.loudness?.truePeak?.toFixed(1) ?? "—"} {v ? ( <> {v.line} {v.flags.map((f) => ( {f} ))} ) : ( )}
{error &&

{error}

}
); }