feat: add compressor + fix ui for all sections
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
.meter-bar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: left;
|
||||
gap: 0.3cqh;
|
||||
}
|
||||
|
||||
.meter-bar__title {
|
||||
font-size: 1.5cqh;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
justify-content: left;
|
||||
}
|
||||
|
||||
.meter-bar__body {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
gap: 0.3cqh;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
.meter-bar__leds-wrap {
|
||||
position: relative;
|
||||
width: 3cqh;
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
|
||||
/* All rows — the 2 indicator LEDs and every level segment — are direct
|
||||
siblings in this one flex column with equal flex weight, so they're
|
||||
guaranteed the exact same height. No row gets a hardcoded pixel height. */
|
||||
.meter-bar__leds {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5cqh;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.meter-bar__led {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
border-radius: 0.5cqh;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 60ms linear;
|
||||
}
|
||||
|
||||
.meter-bar__led--indicator {
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.06) inset;
|
||||
}
|
||||
|
||||
/* Peak-hold marker: a thin line overlaid on the ladder at the peak's position,
|
||||
independent of which LEDs are lit by rms. */
|
||||
.meter-bar__peak-marker {
|
||||
position: absolute;
|
||||
left: -0.5cqh;
|
||||
right: -0.5cqh;
|
||||
height: 0.3cqh;
|
||||
min-height: 1px;
|
||||
background-color: #e5e7eb;
|
||||
box-shadow: 0 0 3px rgba(255, 255, 255, 0.8);
|
||||
transform: translateY(0.25cqh);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Scale sits in the same row as leds-wrap, same height, so ticks positioned
|
||||
by percentage line up with the exact dB position on the ladder. */
|
||||
.meter-bar__scale {
|
||||
position: relative;
|
||||
width: 3.5cqh;
|
||||
font-size: 1.5cqh;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.meter-bar__scale-tick {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
transform: translateY(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meter-bar__scale-tick--end {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
|
||||
.meter-bar__value {
|
||||
padding-top: 0.5cqh;
|
||||
font-size: 1.2cqh;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.meter-bar__indicator-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.1cqh;
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
font-size: 1.5cqh;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meter-bar__indicator-row .meter-bar__led {
|
||||
flex: 0 0 auto;
|
||||
width: 3cqh;
|
||||
height: 100%;
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import './MeterBar.css';
|
||||
|
||||
export interface Meter {
|
||||
rms: number;
|
||||
peak: number;
|
||||
clip?: boolean;
|
||||
}
|
||||
|
||||
type MeterBarProps = {
|
||||
label: string;
|
||||
meter: Meter;
|
||||
min?: number;
|
||||
max?: number;
|
||||
segments?: number;
|
||||
/** Intermediate dB values to mark on the scale, e.g. [0] or [-20, -6, 0]. */
|
||||
ticks?: number[];
|
||||
};
|
||||
|
||||
function segmentColor(posRatioFromBottom: number) {
|
||||
if (posRatioFromBottom > 0.9) return '#f87171'; // red — near clip
|
||||
if (posRatioFromBottom > 0.7) return '#fbbf24'; // yellow — hot
|
||||
return '#4ade80'; // green — normal
|
||||
}
|
||||
|
||||
// Formats a dB value with an explicit '+' sign for positive values (e.g. +12, -60, 0)
|
||||
function formatDb(v: number, decimals = 0) {
|
||||
if (!Number.isFinite(v)) return '—';
|
||||
const fixed = v.toFixed(decimals);
|
||||
return v > 0 ? `+${fixed}` : fixed;
|
||||
}
|
||||
|
||||
function MeterBar({
|
||||
label,
|
||||
meter,
|
||||
min = -60,
|
||||
max = +10,
|
||||
segments = 7,
|
||||
ticks = [0, -50, -40, -30, -20, -10],
|
||||
}: MeterBarProps) {
|
||||
const { rms, peak, clip } = meter;
|
||||
|
||||
// Maps a raw dB value to a 0..1 ratio from the bottom of the ladder, clamped to range.
|
||||
const ratioOf = (v: number) => {
|
||||
const clamped = Math.min(max, Math.max(min, v));
|
||||
return (clamped - min) / (max - min);
|
||||
};
|
||||
|
||||
// --- RMS ladder (drives which LEDs are lit) ---
|
||||
const rmsRatio = ratioOf(rms);
|
||||
const litCount = Math.round(rmsRatio * segments);
|
||||
|
||||
// --- Peak marker (position only, doesn't affect lit count) ---
|
||||
const peakRatio = ratioOf(peak);
|
||||
|
||||
// --- Intermediate tick marks on the scale, e.g. a 0 dB reference line ---
|
||||
const visibleTicks = ticks.filter((t) => t > min && t < max);
|
||||
|
||||
// --- Red "over" LED: lit whenever rms or peak crosses max ---
|
||||
const isOverMax = rms >= max || peak >= max;
|
||||
|
||||
// --- Orange "limit" LED: lit strictly from the meter's own clip flag.
|
||||
// clip is optional — some meters simply don't report a limiter/clip state.
|
||||
const hasLimiter = clip !== undefined;
|
||||
const isClipping = clip === true;
|
||||
|
||||
// The over-led and limit-led are rendered as two extra rows in the very
|
||||
// same flex column as the level segments (see meter-bar__leds below), so
|
||||
// they get identical row height automatically instead of a hardcoded one.
|
||||
// Because of that, the ladder itself only occupies `segments / totalRows`
|
||||
// of the column — the rest of the math (peak marker, scale ticks) has to
|
||||
// scale down to match.
|
||||
const totalRows = segments + 2;
|
||||
const ladderFraction = segments / totalRows;
|
||||
const toColumnPercent = (ratio: number) => ratio * ladderFraction * 100;
|
||||
|
||||
return (
|
||||
<div className="meter-bar">
|
||||
<div className="meter-bar__title">{label}</div>
|
||||
|
||||
<div className="meter-bar__body">
|
||||
<div className="meter-bar__leds-wrap">
|
||||
<div className="meter-bar__leds">
|
||||
<div
|
||||
className="meter-bar__indicator-row"
|
||||
style={{ visibility: hasLimiter ? 'visible' : 'hidden' }}
|
||||
>
|
||||
<div
|
||||
className="meter-bar__led meter-bar__led--indicator"
|
||||
style={{
|
||||
backgroundColor: isClipping ? '#fb923c' : 'rgba(255,255,255,0.08)',
|
||||
}}
|
||||
title="Limit"
|
||||
/>
|
||||
<span>Limit</span>
|
||||
</div>
|
||||
|
||||
<div className="meter-bar__indicator-row">
|
||||
<div
|
||||
className="meter-bar__led meter-bar__led--indicator"
|
||||
style={{
|
||||
backgroundColor: isOverMax ? '#f87171' : 'rgba(255,255,255,0.08)',
|
||||
}}
|
||||
title="Clip"
|
||||
/>
|
||||
<span>Clip</span>
|
||||
</div>
|
||||
|
||||
{Array.from({ length: segments }).map((_, i) => {
|
||||
// i=0 renders at the top; convert to "position from bottom" for lighting + color
|
||||
const posFromBottom = segments - 1 - i;
|
||||
const lit = posFromBottom < litCount;
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="meter-bar__led"
|
||||
style={{
|
||||
backgroundColor: lit
|
||||
? segmentColor(posFromBottom / segments)
|
||||
: 'rgba(255,255,255,0.08)',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="meter-bar__peak-marker"
|
||||
style={{ bottom: `${toColumnPercent(peakRatio)}%` }}
|
||||
title={`Peak: ${formatDb(peak, 1)} dB`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="meter-bar__scale">
|
||||
<span
|
||||
className="meter-bar__scale-tick meter-bar__scale-tick--end"
|
||||
style={{ bottom: `${ladderFraction * 100}%` }}
|
||||
>
|
||||
{formatDb(max)}
|
||||
</span>
|
||||
{visibleTicks.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className="meter-bar__scale-tick"
|
||||
style={{ bottom: `${toColumnPercent(ratioOf(t))}%` }}
|
||||
>
|
||||
{formatDb(t)}
|
||||
</span>
|
||||
))}
|
||||
<span
|
||||
className="meter-bar__scale-tick meter-bar__scale-tick--end"
|
||||
style={{ bottom: '0%' }}
|
||||
>
|
||||
{formatDb(min)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="meter-bar__value">
|
||||
{Number.isFinite(rms) ? `${formatDb(rms, 1)} dB` : '—'}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default MeterBar;
|
||||
Reference in New Issue
Block a user