feat: start input section
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
.channel-group {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
/* flex: 1 1 0; */
|
||||
min-height: 0;
|
||||
|
||||
display: flex;
|
||||
|
||||
@@ -2,6 +2,8 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import './VerticalSlider.css';
|
||||
|
||||
type StepFn = (value: number) => number;
|
||||
type QuantizeFn = (value: number) => number;
|
||||
type Scale = 'linear' | 'log';
|
||||
|
||||
type VerticalSliderProps = {
|
||||
min: number;
|
||||
@@ -9,6 +11,17 @@ type VerticalSliderProps = {
|
||||
|
||||
step?: StepFn;
|
||||
|
||||
/**
|
||||
* Optional hard snap function applied after every value change (drag,
|
||||
* keyboard, wheel, or typed input) — takes precedence over the built-in
|
||||
* step-based rounding. Use this when the underlying hardware/DSP only
|
||||
* accepts a fixed, non-uniformly-spaced set of values (e.g. a 0..N raw
|
||||
* index mapped through a log curve) and the slider must always land on
|
||||
* one of those exact values rather than an interpolated approximation.
|
||||
* See makeLogQuantize().
|
||||
*/
|
||||
quantize?: QuantizeFn;
|
||||
|
||||
value: number;
|
||||
onChange?: (value: number) => Promise<boolean>;
|
||||
onInput?: (value: number) => void;
|
||||
@@ -18,6 +31,9 @@ type VerticalSliderProps = {
|
||||
|
||||
showTicks?: boolean;
|
||||
|
||||
/** 'linear' (default) or 'log'. Log scale requires min > 0 and max > 0. */
|
||||
scale?: Scale;
|
||||
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
@@ -37,16 +53,86 @@ function getDecimals(n: number) {
|
||||
return i === -1 ? 0 : s.length - i - 1;
|
||||
}
|
||||
|
||||
// --- scale helpers -----------------------------------------------------
|
||||
|
||||
/** Guards against log(0) / log(negative); falls back to a tiny positive epsilon. */
|
||||
function safeLogBound(n: number) {
|
||||
return n > 0 ? n : Number.EPSILON;
|
||||
}
|
||||
|
||||
/** Maps a real value to a 0..1 position, honoring the chosen scale. */
|
||||
function valueToPercent(v: number, min: number, max: number, scale: Scale) {
|
||||
if (scale === 'log') {
|
||||
const logMin = Math.log(safeLogBound(min));
|
||||
const logMax = Math.log(safeLogBound(max));
|
||||
if (logMax === logMin) return 0;
|
||||
const logV = Math.log(safeLogBound(v));
|
||||
return (logV - logMin) / (logMax - logMin);
|
||||
}
|
||||
if (max === min) return 0;
|
||||
return (v - min) / (max - min);
|
||||
}
|
||||
|
||||
/** Maps a 0..1 position back to a real value, honoring the chosen scale. */
|
||||
function percentToValue(percent: number, min: number, max: number, scale: Scale) {
|
||||
if (scale === 'log') {
|
||||
const logMin = Math.log(safeLogBound(min));
|
||||
const logMax = Math.log(safeLogBound(max));
|
||||
return Math.exp(logMin + percent * (logMax - logMin));
|
||||
}
|
||||
return min + percent * (max - min);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a StepFn that yields roughly `points` evenly-spaced steps between
|
||||
* `min` and `max` on a logarithmic scale — e.g. makeLogStep(19.7, 20160, 300)
|
||||
* gives ~300 points across a 20 Hz - 20 kHz range, with step size growing
|
||||
* proportionally to the current value (fine resolution at low frequencies,
|
||||
* coarser at high ones — matches how the ear perceives frequency).
|
||||
*
|
||||
* This is an approximation used to size keyboard/wheel nudges — it does NOT
|
||||
* guarantee landing on the same exact grid a real device would produce.
|
||||
* Pair it with `quantize` (see makeLogQuantize) when you need the slider to
|
||||
* always land exactly on the device's own value set.
|
||||
*/
|
||||
export function makeLogStep(min: number, max: number, points: number): StepFn {
|
||||
const ratio = Math.pow(max / min, 1 / Math.max(1, points));
|
||||
const stepSize = ratio - 1;
|
||||
return (value: number) => Math.max(min, value) * stepSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a QuantizeFn that snaps to the exact same log-spaced grid a device
|
||||
* using a `raw` 0..points index would produce, e.g. for a frequency raw
|
||||
* value stored as:
|
||||
* raw = round(log(Hz / min) / log(max / min) * points)
|
||||
* Hz = min * (max / min) ** (raw / points)
|
||||
* makeLogQuantize(19.7, 20160, 300) reproduces exactly that 301-point set
|
||||
* (raw 0..300), so the slider only ever settles on values the device itself
|
||||
* would encode/decode to — no drift from an approximate step size.
|
||||
*/
|
||||
export function makeLogQuantize(min: number, max: number, points: number): QuantizeFn {
|
||||
const logRatio = Math.log(max / min);
|
||||
return (value: number) => {
|
||||
const v = Math.max(min, Math.min(max, value));
|
||||
const raw = Math.round((Math.log(v / min) / logRatio) * points);
|
||||
const clampedRaw = Math.min(points, Math.max(0, raw));
|
||||
return min * Math.pow(max / min, clampedRaw / points);
|
||||
};
|
||||
}
|
||||
|
||||
export default function VerticalSlider({
|
||||
min,
|
||||
max,
|
||||
step = defaultStep,
|
||||
quantize,
|
||||
value,
|
||||
onChange,
|
||||
onInput,
|
||||
unit = '',
|
||||
formatValue,
|
||||
showTicks = true,
|
||||
scale = 'linear',
|
||||
className,
|
||||
style,
|
||||
}: VerticalSliderProps) {
|
||||
@@ -60,10 +146,25 @@ export default function VerticalSlider({
|
||||
const dragging = useRef(false);
|
||||
const thumbRef = useRef<HTMLDivElement>(null);
|
||||
const keyboardEditing = useRef(false);
|
||||
const wheelValue = useRef<number | null>(null);
|
||||
const wheelEditing = useRef(false);
|
||||
const wheelCommitTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const [tickCount, setTickCount] = useState(MIN_TICKS);
|
||||
|
||||
if (scale === 'log' && (min <= 0 || max <= 0)) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
'VerticalSlider: scale="log" requires min > 0 and max > 0. Falling back to linear behavior for out-of-range bounds.'
|
||||
);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (dragging.current || keyboardEditing.current || editingText !== null) {
|
||||
if (
|
||||
dragging.current ||
|
||||
keyboardEditing.current ||
|
||||
wheelEditing.current ||
|
||||
editingText !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setDisplayValue(value);
|
||||
@@ -99,6 +200,11 @@ export default function VerticalSlider({
|
||||
|
||||
function normalizeValue(input: number) {
|
||||
const clamped = Math.min(max, Math.max(min, input));
|
||||
|
||||
if (quantize) {
|
||||
return Number(Math.min(max, Math.max(min, quantize(clamped))).toFixed(6));
|
||||
}
|
||||
|
||||
const currentStep = step(clamped);
|
||||
const snapped = min + Math.round((clamped - min) / currentStep) * currentStep;
|
||||
return Number(Math.min(max, Math.max(min, snapped)).toFixed(6));
|
||||
@@ -119,7 +225,11 @@ export default function VerticalSlider({
|
||||
keyboardEditing.current = true;
|
||||
e.preventDefault();
|
||||
|
||||
const base = keyboardValue.current ?? value;
|
||||
// Base off displayValue (our own last-known-good local state), not the
|
||||
// `value` prop: if onChange's round trip through the parent/audio engine
|
||||
// hasn't landed yet, `value` can still be stale, which made repeated
|
||||
// key presses appear to snap back to the pre-press position.
|
||||
const base = keyboardValue.current ?? displayValue;
|
||||
const currentStep = step(base);
|
||||
|
||||
let next = base;
|
||||
@@ -147,11 +257,65 @@ export default function VerticalSlider({
|
||||
}
|
||||
}
|
||||
|
||||
function handleWheel(e: WheelEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
wheelEditing.current = true;
|
||||
|
||||
const base = wheelValue.current ?? displayValue;
|
||||
const currentStep = step(base);
|
||||
// deltaY < 0 means scrolling up/away from the user — treat that as
|
||||
// increasing the value, matching the vertical slider's "up = more".
|
||||
const direction = e.deltaY < 0 ? 1 : -1;
|
||||
|
||||
const next = normalizeValue(base + direction * currentStep);
|
||||
wheelValue.current = next;
|
||||
updateValue(next);
|
||||
|
||||
if (wheelCommitTimeout.current) clearTimeout(wheelCommitTimeout.current);
|
||||
wheelCommitTimeout.current = setTimeout(() => {
|
||||
void commitWheel();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function commitWheel() {
|
||||
if (wheelValue.current == null) return;
|
||||
|
||||
const next = wheelValue.current;
|
||||
const success = await onChange?.(next);
|
||||
|
||||
wheelEditing.current = false;
|
||||
wheelValue.current = null;
|
||||
|
||||
if (success === false) {
|
||||
setDisplayValue(value);
|
||||
dragValue.current = value;
|
||||
onInput?.(value);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const el = sliderRef.current;
|
||||
if (!el) return;
|
||||
|
||||
// React attaches onWheel passively by default, so preventDefault()
|
||||
// wouldn't stop the page from scrolling underneath the slider — attach
|
||||
// natively with { passive: false } instead.
|
||||
el.addEventListener('wheel', handleWheel, { passive: false });
|
||||
return () => el.removeEventListener('wheel', handleWheel);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (wheelCommitTimeout.current) clearTimeout(wheelCommitTimeout.current);
|
||||
};
|
||||
}, []);
|
||||
|
||||
function valueFromPointer(clientY: number) {
|
||||
if (!sliderRef.current) return;
|
||||
const rect = sliderRef.current.getBoundingClientRect();
|
||||
const percent = 1 - (clientY - rect.top) / rect.height;
|
||||
const raw = min + percent * (max - min);
|
||||
const raw = percentToValue(percent, min, max, scale);
|
||||
updateValue(raw);
|
||||
}
|
||||
|
||||
@@ -212,7 +376,7 @@ export default function VerticalSlider({
|
||||
setEditingText(null);
|
||||
}
|
||||
|
||||
const percent = (displayValue - min) / (max - min);
|
||||
const percent = valueToPercent(displayValue, min, max, scale);
|
||||
|
||||
return (
|
||||
<div className={['gauge-viewport', className].filter(Boolean).join(' ')} style={style}>
|
||||
|
||||
@@ -139,12 +139,10 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
|
||||
<InputSection
|
||||
dsp={dsp}
|
||||
channel={InputChannel.InA}
|
||||
set_low_pass={api.setLowPass}
|
||||
set_high_pass={api.setHighPass}
|
||||
setLowPass={api.setLowPass}
|
||||
setHighPass={api.setHighPass}
|
||||
setPeq={api.setPeqBand}
|
||||
setGain={api.setGain}
|
||||
setMute={api.setMute}
|
||||
setInverse={api.setInverse}
|
||||
setPeqBypass={api.setPEQBypass}
|
||||
/>
|
||||
);
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: center; /* ← add this */
|
||||
gap: var(--space-4);
|
||||
|
||||
box-sizing: border-box;
|
||||
@@ -219,7 +220,7 @@
|
||||
.input-panel__cell-label {
|
||||
flex: 0 0 auto;
|
||||
font-family: var(--font-ui);
|
||||
font-size: 2.5cqh;
|
||||
font-size: 3.2cqh;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -271,72 +272,92 @@
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.gain-meter-panel {
|
||||
container-type: size;
|
||||
|
||||
position: relative;
|
||||
height: 100%;
|
||||
aspect-ratio: 1 / 2;
|
||||
flex: 0 0 auto;
|
||||
.peq-band-list {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1.5cqh;
|
||||
|
||||
border: var(--border-width) solid var(--border-hairline);
|
||||
border-radius: var(--radius-md);
|
||||
box-sizing: border-box;
|
||||
background: var(--bg-void);
|
||||
padding: 1cqh;
|
||||
|
||||
overflow: hidden;
|
||||
|
||||
aspect-ratio: 2;
|
||||
}
|
||||
|
||||
.gain-meter-panel__title {
|
||||
.peq-band-list__row {
|
||||
flex: 1 1 0;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1.2fr 1fr 1fr 1.4fr 1.2fr;
|
||||
align-items: stretch;
|
||||
gap: 1cqh;
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.peq-band-list__row--header {
|
||||
flex: 0 0 auto;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
|
||||
font-family: var(--font-ui);
|
||||
font-size: 4cqh;
|
||||
color: var(--text-primary);
|
||||
text-align: center;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
padding-top: 2cqh;
|
||||
padding-bottom: 2cqh;
|
||||
|
||||
border-bottom: var(--border-width) solid var(--border-hairline);
|
||||
|
||||
padding-top: 1cqh;
|
||||
padding-bottom: 1cqh;
|
||||
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.gain-meter-panel__grid {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
padding: 2cqh;
|
||||
box-sizing: border-box;
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
/* slider/meter row gets the bulk of the height, button row stays compact */
|
||||
grid-template-rows: 1fr auto;
|
||||
gap: 1.5cqh;
|
||||
.peq-band-list__row--active {
|
||||
background: var(--bg-panel);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.gain-meter-panel__cell {
|
||||
.peq-band-list__cell {
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.gain-meter-panel__cell--slider,
|
||||
.gain-meter-panel__cell--meter {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.gain-meter-panel__cell--btn {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.gain-meter-panel__cell--meter {
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
justify-content: center;
|
||||
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.peq-band-list__input,
|
||||
.peq-band-list__select {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
|
||||
font-family: var(--font-ui);
|
||||
font-size: 2.2cqh;
|
||||
padding: 0.5cqh;
|
||||
|
||||
text-align: center;
|
||||
|
||||
background: var(--bg-panel);
|
||||
color: var(--text-primary);
|
||||
|
||||
border: var(--border-width) solid var(--border-hairline);
|
||||
border-radius: var(--radius-sm);
|
||||
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
// InputSection.tsx
|
||||
import VerticalSlider from '../../../../components/VerticalSlider/VerticalSlider';
|
||||
import VerticalSlider, {
|
||||
makeLogStep,
|
||||
makeLogQuantize,
|
||||
} from '../../../../components/VerticalSlider/VerticalSlider';
|
||||
// ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping
|
||||
// input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values
|
||||
// if the actual enum differs.
|
||||
@@ -13,9 +16,23 @@ import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
||||
import Button from '../../../../components/Button/Button';
|
||||
import MeterBar from '../../../../components/MeterBar/MeterBar';
|
||||
|
||||
const FREQ_MIN = 20;
|
||||
const FREQ_MAX = 20000;
|
||||
const FREQ_STEP = 1;
|
||||
const PEQ_BANDS: PEQBand[] = [
|
||||
PEQBand.B1,
|
||||
PEQBand.B2,
|
||||
PEQBand.B3,
|
||||
PEQBand.B4,
|
||||
PEQBand.B5,
|
||||
PEQBand.B6,
|
||||
PEQBand.B7,
|
||||
PEQBand.B8,
|
||||
];
|
||||
|
||||
// ASSUMPTION: adjust these to match the real PEQFilter enum in ../../types
|
||||
const PEQ_FILTER_TYPE_OPTIONS: { value: PEQFilter; label: string }[] = [
|
||||
{ value: PEQFilter.Peak, label: 'Peak' },
|
||||
{ value: PEQFilter.LowShelf, label: 'Low Shelf' },
|
||||
{ value: PEQFilter.HighShelf, label: 'High Shelf' },
|
||||
];
|
||||
|
||||
const FILTER_TYPE_OPTIONS: { value: CrossoverFilter; label: string }[] = [
|
||||
{ value: CrossoverFilter.Bw6, label: 'BW-6' },
|
||||
@@ -112,6 +129,12 @@ function PeqPanelSliders({
|
||||
);
|
||||
}
|
||||
|
||||
const FREQ_MIN = 19.7;
|
||||
const FREQ_MAX = 20160;
|
||||
const FREQ_POINTS = 300;
|
||||
const FREQ_STEP = makeLogStep(FREQ_MIN, FREQ_MAX, FREQ_POINTS);
|
||||
const FREQ_QUANTIZE = makeLogQuantize(FREQ_MIN, FREQ_MAX, FREQ_POINTS);
|
||||
|
||||
function CrossoverPanel({
|
||||
channel,
|
||||
dsp,
|
||||
@@ -124,8 +147,7 @@ function CrossoverPanel({
|
||||
setCrossover: (
|
||||
channel: Channel,
|
||||
frequency: number,
|
||||
filter_type: CrossoverFilter,
|
||||
bypass: boolean
|
||||
filter_type: CrossoverFilter
|
||||
) => Promise<boolean>;
|
||||
}) {
|
||||
const channelState =
|
||||
@@ -145,7 +167,7 @@ function CrossoverPanel({
|
||||
bypass?: boolean;
|
||||
}) => {
|
||||
const next = { frequency, filter_type, bypass, ...overrides };
|
||||
return setCrossover(channel, next.frequency, next.filter_type, next.bypass);
|
||||
return setCrossover(channel, next.frequency, next.filter_type);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -153,9 +175,11 @@ function CrossoverPanel({
|
||||
<div className="crossover-panel__title">{title}</div>
|
||||
|
||||
<VerticalSlider
|
||||
min={20}
|
||||
max={20000}
|
||||
step={() => 1}
|
||||
min={FREQ_MIN}
|
||||
max={FREQ_MAX}
|
||||
scale="log"
|
||||
step={FREQ_STEP}
|
||||
quantize={FREQ_QUANTIZE}
|
||||
unit=" Hz"
|
||||
value={frequency}
|
||||
onChange={(value) => commit({ frequency: value })}
|
||||
@@ -194,99 +218,117 @@ function CrossoverPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function GainMeterPanel({
|
||||
dsp,
|
||||
function PeqBandList({
|
||||
channel,
|
||||
title,
|
||||
meter,
|
||||
setGain,
|
||||
setMute,
|
||||
setInverse,
|
||||
dsp,
|
||||
activeBand,
|
||||
setActiveBand,
|
||||
setPEQ,
|
||||
}: {
|
||||
channel: InputChannel;
|
||||
dsp: DSP;
|
||||
channel: Channel;
|
||||
title: string;
|
||||
meter: Meter;
|
||||
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||
activeBand: PEQBand;
|
||||
setActiveBand: (band: PEQBand) => void;
|
||||
setPEQ: SetPEQFn;
|
||||
}) {
|
||||
const channelState =
|
||||
'Input' in channel
|
||||
? dsp.state?.current_config.input_states[channel.Input]
|
||||
: dsp.state?.current_config.output_states[channel.Output];
|
||||
const inputState = dsp.state.current_config.input_states[channel];
|
||||
|
||||
const muted = channelState?.mute ?? false;
|
||||
const inverted = channelState?.phase_inverted ?? false;
|
||||
const gain = channelState?.gain ?? 0;
|
||||
|
||||
const toggleMute = async () => {
|
||||
await setMute(channel, !muted);
|
||||
};
|
||||
|
||||
const toggleInverse = async () => {
|
||||
await setInverse(channel, !inverted);
|
||||
const commit = (band: PEQBand, overrides: Partial<PEQValues>) => {
|
||||
const peq = inputState.peq_chain.bands[band] as PEQ;
|
||||
const next = { ...peq, ...overrides };
|
||||
return setPEQ(
|
||||
{ Input: channel },
|
||||
band,
|
||||
next.gain,
|
||||
next.frequency,
|
||||
next.q,
|
||||
next.filter_type,
|
||||
next.bypass
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="gain-meter-panel">
|
||||
<div className="gain-meter-panel__title">{title}</div>
|
||||
<div className="peq-band-list">
|
||||
<div className="peq-band-list__row peq-band-list__row--header">
|
||||
<div className="peq-band-list__cell">Band</div>
|
||||
<div className="peq-band-list__cell">Freq</div>
|
||||
<div className="peq-band-list__cell">Q</div>
|
||||
<div className="peq-band-list__cell">Gain</div>
|
||||
<div className="peq-band-list__cell">Type</div>
|
||||
<div className="peq-band-list__cell">Bypass</div>
|
||||
</div>
|
||||
|
||||
<div className="gain-meter-panel__grid">
|
||||
<div className="gain-meter-panel__cell gain-meter-panel__cell--slider">
|
||||
<VerticalSlider
|
||||
min={-60}
|
||||
max={12}
|
||||
step={(v) => (v >= -20 ? 0.1 : 0.5)}
|
||||
unit=" dB"
|
||||
value={gain}
|
||||
onChange={(value) => setGain(channel, value)}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
{PEQ_BANDS.map((band, i) => {
|
||||
const peq = inputState.peq_chain.bands[band] as PEQ;
|
||||
const isActive = band === activeBand;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`peq-band-list__row${isActive ? ' peq-band-list__row--active' : ''}`}
|
||||
key={band}
|
||||
>
|
||||
<Button
|
||||
variant="toggle"
|
||||
className={isActive ? 'active' : ''}
|
||||
onClick={() => setActiveBand(band)}
|
||||
style={{ minHeight: 0 }}
|
||||
>
|
||||
{`B${i + 1}`}
|
||||
</Button>
|
||||
|
||||
<input
|
||||
className="peq-band-list__input"
|
||||
type="number"
|
||||
step={0.5}
|
||||
min={19.7}
|
||||
max={20160}
|
||||
value={peq.frequency}
|
||||
onChange={(e) => commit(band, { frequency: Number(e.target.value) })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="gain-meter-panel__cell gain-meter-panel__cell--meter">
|
||||
<MeterBar label="" meter={meter} />
|
||||
</div>
|
||||
<input
|
||||
className="peq-band-list__input"
|
||||
type="number"
|
||||
step={1}
|
||||
min={1}
|
||||
max={999}
|
||||
value={peq.q}
|
||||
onChange={(e) => commit(band, { q: Number(e.target.value) })}
|
||||
/>
|
||||
|
||||
<input
|
||||
className="peq-band-list__input"
|
||||
type="number"
|
||||
step={0.1}
|
||||
min={-12}
|
||||
max={12}
|
||||
value={peq.gain}
|
||||
onChange={(e) => commit(band, { gain: Number(e.target.value) })}
|
||||
/>
|
||||
|
||||
<select
|
||||
className="peq-band-list__select"
|
||||
value={peq.filter_type}
|
||||
onChange={(e) => commit(band, { filter_type: e.target.value as PEQFilter })}
|
||||
>
|
||||
{PEQ_FILTER_TYPE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<div className="gain-meter-panel__cell gain-meter-panel__cell--btn">
|
||||
<Button
|
||||
variant="toggle"
|
||||
className={inverted ? 'active-warning' : ''}
|
||||
onClick={toggleInverse}
|
||||
style={{
|
||||
width: '100%',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
fontSize: '3.5cqh',
|
||||
paddingTop: '1cqh',
|
||||
paddingBottom: '1cqh',
|
||||
minHeight: 0,
|
||||
}}
|
||||
className={peq.bypass ? 'active-warning' : ''}
|
||||
onClick={() => commit(band, { bypass: !peq.bypass })}
|
||||
style={{ minHeight: 0 }}
|
||||
>
|
||||
{inverted ? 'Inverted' : 'Invert'}
|
||||
{peq.bypass ? 'Bypassed' : 'Bypass'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="gain-meter-panel__cell gain-meter-panel__cell--btn">
|
||||
<Button
|
||||
variant="toggle"
|
||||
className={muted ? 'active-danger' : ''}
|
||||
onClick={toggleMute}
|
||||
style={{
|
||||
width: '100%',
|
||||
paddingLeft: 0,
|
||||
paddingRight: 0,
|
||||
fontSize: '3.5cqh',
|
||||
paddingTop: '1cqh',
|
||||
paddingBottom: '1cqh',
|
||||
minHeight: 0,
|
||||
}}
|
||||
>
|
||||
{muted ? 'Muted' : 'Mute'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -310,23 +352,17 @@ type setPeqBandFn = (
|
||||
function InputSection({
|
||||
dsp,
|
||||
channel,
|
||||
set_high_pass: setHighPass,
|
||||
set_low_pass,
|
||||
setHighPass,
|
||||
setLowPass,
|
||||
setPeq,
|
||||
// setPeqBypass,
|
||||
setGain,
|
||||
setMute,
|
||||
setInverse,
|
||||
setPeqBypass,
|
||||
}: {
|
||||
dsp: DSP;
|
||||
channel: InputChannel;
|
||||
set_high_pass: SetFilterFn;
|
||||
set_low_pass: SetFilterFn;
|
||||
setHighPass: SetFilterFn;
|
||||
setLowPass: SetFilterFn;
|
||||
setPeq: setPeqBandFn;
|
||||
// setPeqBypass;
|
||||
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||
setPeqBypass: (channel: Channel, bypass: boolean) => Promise<boolean>;
|
||||
}) {
|
||||
const [activeBand, setActiveBand] = useState<PEQBand>(PEQBand.B1);
|
||||
|
||||
@@ -408,7 +444,14 @@ function InputSection({
|
||||
<div className="input-control-section">
|
||||
<div className="input-control-panel">
|
||||
<ChannelGroup title="PEQ">
|
||||
<PeqPanelSliders channel={channel} dsp={dsp} band={PEQBand.B1} setPEQ={setPeq} />
|
||||
<PeqBandList
|
||||
channel={channel}
|
||||
dsp={dsp}
|
||||
activeBand={activeBand}
|
||||
setActiveBand={setActiveBand}
|
||||
setPEQ={setPeq}
|
||||
/>
|
||||
<PeqPanelSliders channel={channel} dsp={dsp} band={activeBand} setPEQ={setPeq} />
|
||||
</ChannelGroup>
|
||||
|
||||
<ChannelGroup title="CrossOver">
|
||||
@@ -425,18 +468,6 @@ function InputSection({
|
||||
setCrossover={setHighPass}
|
||||
/>
|
||||
</ChannelGroup>
|
||||
|
||||
<ChannelGroup title="Gain">
|
||||
<GainMeterPanel
|
||||
dsp={dsp}
|
||||
channel={{ Input: channel }}
|
||||
title={dsp.state.current_config.input_states.InA.name}
|
||||
meter={dsp.meters.channels[0]}
|
||||
setGain={setGain}
|
||||
setMute={setMute}
|
||||
setInverse={setInverse}
|
||||
/>
|
||||
</ChannelGroup>
|
||||
{/* <div className="input-control-panel__sliders">
|
||||
<div className="input-control-panel__cell input-control-panel__cell--high-pass">
|
||||
<VerticalSlider
|
||||
|
||||
@@ -106,15 +106,15 @@ export enum DiscreteFrequency {
|
||||
}
|
||||
|
||||
export enum PEQBand {
|
||||
B1 = 1,
|
||||
B2 = 2,
|
||||
B3 = 3,
|
||||
B4 = 4,
|
||||
B5 = 5,
|
||||
B6 = 6,
|
||||
B7 = 7,
|
||||
B8 = 8,
|
||||
B9 = 9,
|
||||
B1 = 'B1',
|
||||
B2 = 'B2',
|
||||
B3 = 'B3',
|
||||
B4 = 'B4',
|
||||
B5 = 'B5',
|
||||
B6 = 'B6',
|
||||
B7 = 'B7',
|
||||
B8 = 'B8',
|
||||
B9 = 'B9',
|
||||
}
|
||||
|
||||
// ---------------- STATE ----------------
|
||||
@@ -304,9 +304,7 @@ const createDefaultPEQ = (): PEQ => ({
|
||||
|
||||
const createDefaultPEQChain = (): PEQChain => ({
|
||||
bands: Object.fromEntries(
|
||||
Object.values(PEQBand)
|
||||
.filter((band): band is PEQBand => typeof band === 'number')
|
||||
.map((band) => [band, createDefaultPEQ()])
|
||||
Object.values(PEQBand).map((band) => [band, createDefaultPEQ()])
|
||||
) as Record<PEQBand, PEQ>,
|
||||
bypass: false,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user