feat: start in tab

This commit is contained in:
2026-08-10 17:58:55 +02:00
parent 0f66ed72f4
commit b572e1c794
2 changed files with 352 additions and 23 deletions
@@ -223,3 +223,120 @@
color: var(--text-muted); color: var(--text-muted);
white-space: nowrap; white-space: nowrap;
} }
.crossover-panel {
position: relative;
height: 100%;
aspect-ratio: 0.28;
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: flex-start;
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-md);
box-sizing: border-box;
gap: 2cqh;
background: var(--bg-void);
padding-bottom: 2cqh;
}
.crossover-panel__title {
flex: 0 0 auto;
width: 100%;
text-align: center;
font-family: var(--font-ui);
font-size: 4cqh;
font-weight: var(--font-weight-semibold);
padding-top: 2cqh;
padding-bottom: 2cqh;
border-bottom: var(--border-width) solid var(--border-hairline);
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
}
.crossover-panel__select {
flex: 0 0 auto;
width: 85%;
font-family: var(--font-ui);
font-size: 3cqh;
padding: 1cqh;
background: var(--bg-panel);
color: inherit;
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm);
}
.gain-meter-panel {
container-type: size;
position: relative;
height: 100%;
aspect-ratio: 1 / 2;
flex: 0 0 auto;
display: flex;
flex-direction: column;
align-items: center;
border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-md);
box-sizing: border-box;
background: var(--bg-void);
}
.gain-meter-panel__title {
flex: 0 0 auto;
width: 100%;
text-align: center;
font-family: var(--font-ui);
font-size: 4cqh;
font-weight: var(--font-weight-semibold);
padding-top: 2cqh;
padding-bottom: 2cqh;
border-bottom: var(--border-width) solid var(--border-hairline);
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;
}
.gain-meter-panel__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;
}
@@ -3,37 +3,37 @@ import VerticalSlider from '../VerticalSlider/VerticalSlider';
// ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping // ASSUMPTION: an InputChannel enum exists alongside OutputChannel, mapping
// input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values // input pages 1-8 similarly (In1 -> 0, In2 -> 1, ...). Adjust import/values
// if the actual enum differs. // if the actual enum differs.
import { InputChannel, Channel, PEQBand, PEQFilter, PEQ } from '../../../types/dsp408State'; import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Meter } from '../../../types/dsp408State';
import { DSP } from '../../../types/types'; import { DSP } from '../../../types/types';
import { CrossoverFilter } from '../../../types/dsp408State'; import { CrossoverFilter } from '../../../types/dsp408State';
import { LinearGraph } from '../LinearGraph/LinearGraph'; import { LinearGraph } from '../LinearGraph/LinearGraph';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import './InputSection.css'; import './InputSection.css';
import ChannelGroup from '../ChannelGroup/ChannelGroup'; import ChannelGroup from '../ChannelGroup/ChannelGroup';
import Button from '../../Button/Button';
import MeterBar from '../../MeterBar/MeterBar';
const FREQ_MIN = 20; const FREQ_MIN = 20;
const FREQ_MAX = 20000; const FREQ_MAX = 20000;
const FREQ_STEP = 1; const FREQ_STEP = 1;
const FILTER_TYPE_OPTIONS: { value: CrossoverFilter; label: string }[] = [ const FILTER_TYPE_OPTIONS: { value: CrossoverFilter; label: string }[] = [
{ value: CrossoverFilter.Bw6, label: 'BW 6 dB/oct' }, { value: CrossoverFilter.Bw6, label: 'BW-6' },
{ value: CrossoverFilter.Bw12, label: 'BW 12 dB/oct' }, { value: CrossoverFilter.Bw12, label: 'BW-12' },
{ value: CrossoverFilter.Bw18, label: 'BW 18 dB/oct' }, { value: CrossoverFilter.Bw18, label: 'BW-18' },
{ value: CrossoverFilter.Bw24, label: 'BW 24 dB/oct' }, { value: CrossoverFilter.Bw24, label: 'BW-24' },
{ value: CrossoverFilter.Bw30, label: 'BW 30 dB/oct' }, { value: CrossoverFilter.Bw30, label: 'BW-30' },
{ value: CrossoverFilter.Bw36, label: 'BW 36 dB/oct' }, { value: CrossoverFilter.Bw36, label: 'BW-36' },
{ value: CrossoverFilter.Bw42, label: 'BW 42 dB/oct' }, { value: CrossoverFilter.Bw42, label: 'BW-42' },
{ value: CrossoverFilter.Bw48, label: 'BW 48 dB/oct' }, { value: CrossoverFilter.Bw48, label: 'BW-48' },
{ value: CrossoverFilter.Lk12, label: 'LR 12 dB/oct' }, { value: CrossoverFilter.Lk12, label: 'LR-12' },
{ value: CrossoverFilter.Lk24, label: 'LR 24 dB/oct' }, { value: CrossoverFilter.Lk24, label: 'LR-24' },
{ value: CrossoverFilter.Lk36, label: 'LR 36 dB/oct' }, { value: CrossoverFilter.Lk36, label: 'LR-36' },
{ value: CrossoverFilter.Lk48, label: 'LR 48 dB/oct' }, { value: CrossoverFilter.Lk48, label: 'LR-48' },
]; ];
const DEFAULT_FILTER_TYPE = CrossoverFilter.Bw12;
const PEQ_SLIDER_PARAMS = [ const PEQ_SLIDER_PARAMS = [
{ key: 'frequency', label: 'Frequency', min: 19.7, max: 20.16, step: 0.5, unit: ' Hz' }, { key: 'frequency', label: 'Frequency', min: 19.7, max: 20160, step: 0.5, unit: ' Hz' },
{ key: 'q', label: 'Q', min: 1, max: 999, step: 1, unit: '' }, { key: 'q', label: 'Q', min: 1, max: 999, step: 1, unit: '' },
{ key: 'gain', label: 'Gain', min: -12, max: +12, step: 0.1, unit: ' dB' }, { key: 'gain', label: 'Gain', min: -12, max: +12, step: 0.1, unit: ' dB' },
] as const; ] as const;
@@ -112,6 +112,185 @@ function PeqPanelSliders({
); );
} }
function CrossoverPanel({
channel,
dsp,
title,
setCrossover,
}: {
channel: Channel;
dsp: DSP;
title: string;
setCrossover: (
channel: Channel,
frequency: number,
filter_type: CrossoverFilter,
bypass: boolean
) => Promise<boolean>;
}) {
const channelState =
'Input' in channel
? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output];
const crossover = channelState?.crossover;
const frequency = crossover.high_pass.frequency;
const filter_type = crossover.high_pass.slope;
const bypass = false;
const commit = (overrides: {
frequency?: number;
filter_type?: CrossoverFilter;
bypass?: boolean;
}) => {
const next = { frequency, filter_type, bypass, ...overrides };
return setCrossover(channel, next.frequency, next.filter_type, next.bypass);
};
return (
<div className="crossover-panel">
<div className="crossover-panel__title">{title}</div>
<VerticalSlider
min={20}
max={20000}
step={() => 1}
unit=" Hz"
value={frequency}
onChange={(value) => commit({ frequency: value })}
style={{ width: '100%', flex: '1 1 auto' }}
/>
<select
className="crossover-panel__select"
value={filter_type}
onChange={(e) => commit({ filter_type: e.target.value as CrossoverFilter })}
>
{FILTER_TYPE_OPTIONS.map((opt) => (
<option key={opt.value} value={opt.value}>
{opt.label}
</option>
))}
</select>
<Button
variant="toggle"
className={bypass ? 'active-warning' : ''}
onClick={() => commit({ bypass: !bypass })}
style={{
width: '85%',
paddingLeft: 0,
paddingRight: 0,
fontSize: '4cqh',
paddingTop: '2cqh',
paddingBottom: '2cqh',
minHeight: 0,
}}
>
{bypass ? 'Bypassed' : 'Bypass'}
</Button>
</div>
);
}
function GainMeterPanel({
dsp,
channel,
title,
meter,
setGain,
setMute,
setInverse,
}: {
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>;
}) {
const channelState =
'Input' in channel
? dsp.state?.current_config.input_states[channel.Input]
: dsp.state?.current_config.output_states[channel.Output];
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);
};
return (
<div className="gain-meter-panel">
<div className="gain-meter-panel__title">{title}</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%' }}
/>
</div>
<div className="gain-meter-panel__cell gain-meter-panel__cell--meter">
<MeterBar label="" meter={meter} />
</div>
<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,
}}
>
{inverted ? 'Inverted' : 'Invert'}
</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>
);
}
type SetFilterFn = ( type SetFilterFn = (
channel: Channel, channel: Channel,
freq: number, freq: number,
@@ -131,10 +310,13 @@ type setPeqBandFn = (
function InputSection({ function InputSection({
dsp, dsp,
channel, channel,
set_high_pass, set_high_pass: setHighPass,
set_low_pass, set_low_pass,
setPeq, setPeq,
// setPeqBypass, // setPeqBypass,
setGain,
setMute,
setInverse,
}: { }: {
dsp: DSP; dsp: DSP;
channel: InputChannel; channel: InputChannel;
@@ -142,6 +324,9 @@ function InputSection({
set_low_pass: SetFilterFn; set_low_pass: SetFilterFn;
setPeq: setPeqBandFn; setPeq: setPeqBandFn;
// setPeqBypass; // setPeqBypass;
setGain: (channel: Channel, gain: number) => Promise<boolean>;
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
}) { }) {
const [activeBand, setActiveBand] = useState<PEQBand>(PEQBand.B1); const [activeBand, setActiveBand] = useState<PEQBand>(PEQBand.B1);
@@ -159,12 +344,12 @@ function InputSection({
const activeFilterType = bypassed && lp.slope !== CrossoverFilter.Bypass ? lp.slope : hp.slope; const activeFilterType = bypassed && lp.slope !== CrossoverFilter.Bypass ? lp.slope : hp.slope;
// Remembers the last non-bypass slope so the Bypass button can restore it. // Remembers the last non-bypass slope so the Bypass button can restore it.
const lastNonBypassType = useRef<CrossoverFilter>( // const lastNonBypassType = useRef<CrossoverFilter>(
activeFilterType === CrossoverFilter.Bypass ? DEFAULT_FILTER_TYPE : activeFilterType // activeFilterType === CrossoverFilter.Bypass ? DEFAULT_FILTER_TYPE : activeFilterType
); // );
if (activeFilterType !== CrossoverFilter.Bypass) { // if (activeFilterType !== CrossoverFilter.Bypass) {
lastNonBypassType.current = activeFilterType; // lastNonBypassType.current = activeFilterType;
} // }
const hpFreq = liveHpFreq ?? hp.frequency; const hpFreq = liveHpFreq ?? hp.frequency;
const lpFreq = liveLpFreq ?? lp.frequency; const lpFreq = liveLpFreq ?? lp.frequency;
@@ -225,6 +410,33 @@ function InputSection({
<ChannelGroup title="PEQ"> <ChannelGroup title="PEQ">
<PeqPanelSliders channel={channel} dsp={dsp} band={PEQBand.B1} setPEQ={setPeq} /> <PeqPanelSliders channel={channel} dsp={dsp} band={PEQBand.B1} setPEQ={setPeq} />
</ChannelGroup> </ChannelGroup>
<ChannelGroup title="CrossOver">
<CrossoverPanel
channel={{ Input: channel }}
dsp={dsp}
title="LowPass"
setCrossover={setHighPass}
/>
<CrossoverPanel
channel={{ Input: channel }}
dsp={dsp}
title="HighPass"
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__sliders">
<div className="input-control-panel__cell input-control-panel__cell--high-pass"> <div className="input-control-panel__cell input-control-panel__cell--high-pass">
<VerticalSlider <VerticalSlider