feat: add compressor + fix ui for all sections

This commit is contained in:
2026-08-06 14:13:04 +02:00
parent e110a28f80
commit 78ea04453e
23 changed files with 1721 additions and 665 deletions
+111
View File
@@ -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%;
}
+165
View File
@@ -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;