Files
sound-analyze/services/frontend/src/widgets/audioLive/ui/AudioMeter.tsx

97 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from "react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "../../../shared/ui/card";
type Props = {
rmsDb: number | null;
peakHoldDb3s: number | null;
};
function clamp(v: number, min: number, max: number) {
return Math.max(min, Math.min(max, v));
}
function colorByDb(db: number): string {
// default thresholds from spec:
// green: < -20, yellow: < -10, red: >= -10
if (db < -20) return "bg-emerald-500";
if (db < -10) return "bg-amber-500";
return "bg-rose-500";
}
export function AudioMeter({ rmsDb, peakHoldDb3s }: Props) {
const minDb = -50;
const maxDb = 0;
const fillPct = useMemo(() => {
if (rmsDb === null) return 0;
const v = clamp(rmsDb, minDb, maxDb);
return ((v - minDb) / (maxDb - minDb)) * 100;
}, [rmsDb]);
const peakTopPct = useMemo(() => {
if (peakHoldDb3s === null) return null;
const v = clamp(peakHoldDb3s, minDb, maxDb);
const pct = ((v - minDb) / (maxDb - minDb)) * 100;
return 100 - pct; // from top
}, [peakHoldDb3s]);
const barColor = rmsDb === null ? "bg-slate-300" : colorByDb(rmsDb);
return (
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-base">Аудио измеритель</CardTitle>
</CardHeader>
<CardContent className="flex items-center gap-4">
{/* Scale + meter */}
<div className="flex items-stretch gap-3">
{/* ticks */}
<div className="flex flex-col justify-between text-xs text-muted-foreground h-56 select-none">
<div>0 дБ</div>
<div>-10</div>
<div>-20</div>
<div>-30</div>
<div>-40</div>
<div>-50</div>
</div>
<div className="relative h-56 w-10 rounded-md bg-slate-200 overflow-hidden">
<div
className={`absolute bottom-0 left-0 right-0 ${barColor} transition-[height] duration-100`}
style={{ height: `${fillPct}%` }}
/>
{peakTopPct !== null && (
<div
className="absolute left-0 right-0 h-[2px] bg-slate-900/80"
style={{ top: `${peakTopPct}%` }}
title={`Peak hold (3s): ${peakHoldDb3s?.toFixed(1)} dB`}
/>
)}
</div>
</div>
{/* Values */}
<div className="min-w-40">
<div className="text-sm text-muted-foreground">Сейчас</div>
<div className="text-2xl font-semibold tabular-nums">
{rmsDb === null ? "—" : `${rmsDb.toFixed(1)} dB`}
</div>
<div className="mt-3 text-sm text-muted-foreground">
Пик (3с)
</div>
<div className="text-lg font-medium tabular-nums">
{peakHoldDb3s === null ? "—" : `${peakHoldDb3s.toFixed(1)} dB`}
</div>
</div>
</CardContent>
</Card>
);
}