import { useEffect, useRef, useState } from "react"; import { Activity, FileAudio, Gauge, Trash2, Waves } from "lucide-react"; import { Button } from "../components/Button"; import { FileDropzone } from "../components/FileDropzone"; import { analyzeAudioFile, type AudioAnalysis } from "../helpers/audioAnalyzer"; import styles from "./analyze.module.css"; import { MobileNav } from "../components/MobileNav"; type AnalysisItem = { id: string; file: File; status: "analyzing" | "done" | "error"; analysis?: AudioAnalysis; error?: string; }; function formatHz(value: number) { return value >= 1000 ? `${(value / 1000).toFixed(1)} kHz` : `${Math.round(value)} Hz`; } function formatDuration(value: number) { const seconds = Math.max(0, Math.round(value)); return `${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, "0")}`; } function Spectrogram({ data }: { data: number[][] }) { const ref = useRef(null); useEffect(() => { const canvas = ref.current; if (!canvas || !data.length || !data[0]?.length) return; const width = data.length; const height = data[0].length; canvas.width = width; canvas.height = height; const context = canvas.getContext("2d"); if (!context) return; const image = context.createImageData(width, height); for (let x = 0; x < width; x += 1) { for (let y = 0; y < height; y += 1) { const value = data[x][height - 1 - y] || 0; const index = (y * width + x) * 4; image.data[index] = Math.round(20 + 30 * value); image.data[index + 1] = Math.round(30 + 190 * value); image.data[index + 2] = Math.round(45 + 205 * value); image.data[index + 3] = 255; } } context.putImageData(image, 0, 0); }, [data]); return ; } export default function AnalyzePage() { const [items, setItems] = useState([]); const [working, setWorking] = useState(false); async function addFiles(files: File[]) { const added = files.map((file) => ({ id: `${file.name}-${file.size}-${file.lastModified}`, file, status: "analyzing" as const, })); setItems((current) => [ ...current, ...added.filter((entry) => !current.some((item) => item.id === entry.id)), ]); setWorking(true); for (const entry of added) { try { const analysis = await analyzeAudioFile(entry.file); setItems((current) => current.map((item) => item.id === entry.id ? { ...item, status: "done", analysis } : item, ), ); } catch (reason) { setItems((current) => current.map((item) => item.id === entry.id ? { ...item, status: "error", error: reason instanceof Error ? reason.message : "Analysis failed.", } : item, ), ); } } setWorking(false); } return (
Audio Analyzer
LOCAL AUDIO TOOLS

Audio Analyzer

Inspect frequency content and loudness statistics entirely on this device.

{items.length ? ( ) : null}
} title="Drop audio files to analyze" subtitle="Spectrogram, peak, RMS, crest factor and estimated frequency cutoff" />
{items.map((item) => (
{item.file.name}{(item.file.size / 1024 / 1024).toFixed(1)} MB
{item.status === "analyzing" ? "Analyzing…" : item.status === "error" ? "Error" : item.analysis?.qualityLabel}
{item.error ?
{item.error}
: null} {item.analysis ? (
Peak{item.analysis.peakDbfs.toFixed(1)} dBFS
RMS{item.analysis.rmsDbfs.toFixed(1)} dBFS
Crest{item.analysis.crestFactorDb.toFixed(1)} dB
Cutoff{formatHz(item.analysis.estimatedCutoffHz)}
Sample rate{formatHz(item.analysis.sampleRate)}
Duration{formatDuration(item.analysis.durationSeconds)}
) : null}
))}
); }