diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index aa3630d..5b6834a 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -400,4 +400,64 @@ pub async fn set_geq_bypass( .set_geq_bypass(input_channel, bypass) .map_err(|e| e.to_string()) }) -} \ No newline at end of file +} + +#[tauri::command] +pub async fn set_low_pass( + state: State<'_, AppState>, + id: u64, + channel: types::Channel, + freq: f32, + filter_type: types::CrossoverFilter, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_low_pass(channel, freq, filter_type) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn set_high_pass( + state: State<'_, AppState>, + id: u64, + channel: types::Channel, + freq: f32, + filter_type: types::CrossoverFilter, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_high_pass(channel, freq, filter_type) + .map_err(|e| e.to_string()) + }) +} + +#[tauri::command] +pub async fn set_peq_band( + state: State<'_, AppState>, + id: u64, + channel: types::Channel, + band: types::PEQBand, + gain_db: f32, + freq_hz: f32, + q: f32, + filter_type: types::PEQFilter, + bypass: bool, +) -> Result { + state.with_device_mut(id, |device| { + device + .dsp + .set_peq_band( + channel, + band, + gain_db, + freq_hz, + q, + filter_type, + bypass, + ) + .map_err(|e| e.to_string()) + }) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index ecca4ee..0fb7299 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -29,6 +29,9 @@ pub fn run() { commands::set_matrix_gain, commands::set_geq_band, commands::set_geq_bypass, + commands::set_low_pass, + commands::set_high_pass, + commands::set_peq_band, ] ) .run(tauri::generate_context!()) diff --git a/src/assets/icons/AudioIcons.tsx b/src/assets/icons/AudioIcons.tsx index 776429c..7480b1f 100644 --- a/src/assets/icons/AudioIcons.tsx +++ b/src/assets/icons/AudioIcons.tsx @@ -64,3 +64,23 @@ export function KnobGainIcon(props: React.SVGProps) { ); } + +export function TextIcon({ + children, + ...props +}: React.SVGProps & { children: React.ReactNode }) { + return ( + + + {children} + + + ); +} diff --git a/src/components/dsp408/CompressorSection/CompressorSection.tsx b/src/components/dsp408/CompressorSection/CompressorSection.tsx index c3a6885..a6c09fb 100644 --- a/src/components/dsp408/CompressorSection/CompressorSection.tsx +++ b/src/components/dsp408/CompressorSection/CompressorSection.tsx @@ -220,7 +220,7 @@ function CompressorPanel({ param.step} unit={param.unit} value={comp[param.key]} onInput={(value) => onLiveValue(channel, param.key, value)} diff --git a/src/components/dsp408/DelaySection/DelaySection.css b/src/components/dsp408/DelaySection/DelaySection.css index 5f36692..cb3504b 100644 --- a/src/components/dsp408/DelaySection/DelaySection.css +++ b/src/components/dsp408/DelaySection/DelaySection.css @@ -31,7 +31,7 @@ .delay-panel { position: relative; height: 100%; - aspect-ratio: 1 / 4; + aspect-ratio: 0.26; flex: 0 0 auto; display: flex; diff --git a/src/components/dsp408/DelaySection/DelaySection.tsx b/src/components/dsp408/DelaySection/DelaySection.tsx index 3b6258a..332f650 100644 --- a/src/components/dsp408/DelaySection/DelaySection.tsx +++ b/src/components/dsp408/DelaySection/DelaySection.tsx @@ -64,7 +64,7 @@ function DelayPanel({ DELAY_STEP[unit]} unit={' ' + UNIT_LABEL[unit]} value={state.delay} onInput={(value) => onLiveValue(channel, value)} diff --git a/src/components/dsp408/GainSection/GainSection.tsx b/src/components/dsp408/GainSection/GainSection.tsx index 7fef529..5a5694a 100644 --- a/src/components/dsp408/GainSection/GainSection.tsx +++ b/src/components/dsp408/GainSection/GainSection.tsx @@ -45,9 +45,7 @@ function GainPanel({ (v >= -20 ? 0.1 : 0.5)} unit=" dB" value={gain} onChange={(value) => setGain(channel, value)} diff --git a/src/components/dsp408/GateSection/GateSection.tsx b/src/components/dsp408/GateSection/GateSection.tsx index cc10120..dca75d5 100644 --- a/src/components/dsp408/GateSection/GateSection.tsx +++ b/src/components/dsp408/GateSection/GateSection.tsx @@ -63,7 +63,7 @@ function GatePanel({ param.step} unit={param.unit} value={gate[param.key]} onInput={(value) => onLiveValue(channel, param.key, value)} diff --git a/src/components/dsp408/GeqSection/GeqSection.tsx b/src/components/dsp408/GeqSection/GeqSection.tsx index c19caed..2331b1f 100644 --- a/src/components/dsp408/GeqSection/GeqSection.tsx +++ b/src/components/dsp408/GeqSection/GeqSection.tsx @@ -83,10 +83,11 @@ function GeqBandPanel({ GEQ_GAIN_STEP} unit="" showTicks={false} value={geq.gains[frequency]} + formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))} onInput={(value) => onLiveValue(channel, frequency, value)} onChange={(value) => setBand(channel, frequency, value)} style={{ width: '100%', flex: '1 1 auto' }} diff --git a/src/components/dsp408/InputSection/InputSection.css b/src/components/dsp408/InputSection/InputSection.css new file mode 100644 index 0000000..c7eca5e --- /dev/null +++ b/src/components/dsp408/InputSection/InputSection.css @@ -0,0 +1,225 @@ +/* InputSection.css */ +.input-graph-section { + flex: 0 0 calc(100% - var(--dsp-control-heigth)); + min-height: 0; + + display: flex; + flex-direction: row; + gap: 1cqh; + padding: 0.5cqh; + + box-sizing: border-box; + background: var(--bg-panel); +} + +.input-graph-panel { + flex: 1 1 auto; + min-width: 0; + min-height: 0; + + display: flex; + flex-direction: column; + + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-md); +} + +.input-graph__title { + flex: 0 0 auto; + + text-align: center; + padding: 1cqh; + + font-family: var(--font-ui); + font-size: 2.5cqh; + font-weight: var(--font-weight-semibold); +} + +.input-graph-panel > .container { + flex: 1; + min-height: 0; +} + +.input-control-section { + container-type: size; + position: absolute; + left: 0; + right: 0; + bottom: 0; + + height: var(--dsp-control-heigth); + + display: flex; + align-items: center; + justify-content: center; + + box-sizing: border-box; + + background: var(--bg-panel); + + z-index: 20; + + border-top: var(--border-width) solid var(--border-hairline); +} + +.input-control-panel { + height: 100%; + width: 100%; + max-width: 60cqw; + + display: flex; + align-items: stretch; + gap: var(--space-4); + + box-sizing: border-box; + padding: 2cqh; +} + +.input-control-panel__sliders { + flex: 1 1 auto; + min-width: 0; + + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4cqh; +} + +.input-control-panel__cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + min-height: 0; + min-width: 0; + gap: 1cqh; +} + +.input-control-panel__cell-label { + flex: 0 0 auto; + font-family: var(--font-ui); + font-size: 2.5cqh; + color: var(--text-muted); + white-space: nowrap; +} + +.input-control-panel__side { + flex: 0 0 auto; + width: 16cqh; + + display: flex; + flex-direction: column; + justify-content: flex-end; + gap: 2cqh; +} + +.input-control-panel__select { + display: flex; + flex-direction: column; + gap: 0.5cqh; +} + +.input-control-panel__select-label { + font-family: var(--font-ui); + font-size: 2.2cqh; + color: var(--text-muted); +} + +.input-control-panel__select select { + font-family: var(--font-ui); + font-size: 2.5cqh; + padding: 0.8cqh; + background: var(--bg-void); + color: var(--text-primary); + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-md); +} + +.input-control-panel__select select:disabled { + opacity: 0.5; +} + +.input-control-panel__bypass-btn { + font-family: var(--font-ui); + font-size: 2.5cqh; + font-weight: var(--font-weight-semibold); + padding: 1cqh; + + background: var(--bg-void); + color: var(--text-primary); + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-md); + cursor: pointer; +} + +.input-control-panel__bypass-btn--active { + background: var(--accent, #f87171); + color: var(--bg-void); + border-color: var(--accent, #f87171); +} + +/* Peq Sliders */ +.input-panel { + position: relative; + height: 10; + + aspect-ratio: 0.75; + flex: 0 0 auto; + + display: flex; + flex-direction: column; + align-items: stretch; + justify-content: flex-start; + + border: var(--border-width) solid var(--border-hairline); + border-radius: var(--radius-md); + box-sizing: border-box; + background: var(--bg-void); +} + +.input-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; +} + +.input-panel__grid { + flex: 1 1 auto; + min-height: 0; + + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-template-rows: 1fr; + + gap: 2cqh; + + padding: 2cqh; + box-sizing: border-box; +} + +.input-panel__cell { + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-start; + min-height: 0; + min-width: 0; + gap: 1cqh; +} + +.input-panel__cell-label { + flex: 0 0 auto; + font-family: var(--font-ui); + font-size: 2.5cqh; + color: var(--text-muted); + white-space: nowrap; +} diff --git a/src/components/dsp408/InputSection/InputSection.tsx b/src/components/dsp408/InputSection/InputSection.tsx new file mode 100644 index 0000000..73bdc3a --- /dev/null +++ b/src/components/dsp408/InputSection/InputSection.tsx @@ -0,0 +1,292 @@ +// InputSection.tsx +import VerticalSlider from '../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. +import { InputChannel, Channel, PEQBand, PEQFilter, PEQ } from '../../../types/dsp408State'; +import { DSP } from '../../../types/types'; +import { CrossoverFilter } from '../../../types/dsp408State'; +import { LinearGraph } from '../LinearGraph/LinearGraph'; +import { useRef, useState } from 'react'; +import './InputSection.css'; +import ChannelGroup from '../ChannelGroup/ChannelGroup'; + +const FREQ_MIN = 20; +const FREQ_MAX = 20000; +const FREQ_STEP = 1; + +const FILTER_TYPE_OPTIONS: { value: CrossoverFilter; label: string }[] = [ + { value: CrossoverFilter.Bw6, label: 'BW 6 dB/oct' }, + { value: CrossoverFilter.Bw12, label: 'BW 12 dB/oct' }, + { value: CrossoverFilter.Bw18, label: 'BW 18 dB/oct' }, + { value: CrossoverFilter.Bw24, label: 'BW 24 dB/oct' }, + { value: CrossoverFilter.Bw30, label: 'BW 30 dB/oct' }, + { value: CrossoverFilter.Bw36, label: 'BW 36 dB/oct' }, + { value: CrossoverFilter.Bw42, label: 'BW 42 dB/oct' }, + { value: CrossoverFilter.Bw48, label: 'BW 48 dB/oct' }, + { value: CrossoverFilter.Lk12, label: 'LR 12 dB/oct' }, + { value: CrossoverFilter.Lk24, label: 'LR 24 dB/oct' }, + { value: CrossoverFilter.Lk36, label: 'LR 36 dB/oct' }, + { value: CrossoverFilter.Lk48, label: 'LR 48 dB/oct' }, +]; + +const DEFAULT_FILTER_TYPE = CrossoverFilter.Bw12; + +const PEQ_SLIDER_PARAMS = [ + { key: 'frequency', label: 'Frequency', min: 19.7, max: 20.16, step: 0.5, unit: ' Hz' }, + { key: 'q', label: 'Q', min: 1, max: 999, step: 1, unit: '' }, + { key: 'gain', label: 'Gain', min: -12, max: +12, step: 0.1, unit: ' dB' }, +] as const; + +type SetPEQFn = ( + channel: Channel, + band: PEQBand, + gainDb: number, + freqHz: number, + q: number, + filterType: PEQFilter, + bypass: boolean +) => Promise; + +type PeqParamKey = 'frequency' | 'q' | 'gain'; + +type PEQValues = { + frequency: number; + q: number; + gain: number; + filter_type: PEQFilter; + bypass: boolean; +}; + +function PeqPanelSliders({ + channel, + dsp, + band, + setPEQ, + // onLiveValue, +}: { + channel: InputChannel; + dsp: DSP; + band: PEQBand; + setPEQ: SetPEQFn; + // onLiveValue: (channel: InputChannel, key: PeqParamKey, value: number) => void; +}) { + const inputState = dsp.state.current_config.input_states[channel]; + const peq = inputState.peq_chain.bands[band] as PEQ; + + const commit = (overrides: Partial) => { + const next = { ...peq, ...overrides }; + return setPEQ( + { Input: channel }, + band, + next.gain, + next.frequency, + next.q, + next.filter_type, + next.bypass + ); + }; + + return ( +
+
{'PEQ Parameters'}
+ +
+ {PEQ_SLIDER_PARAMS.map((param) => ( +
+ param.step} + unit={param.unit} + value={peq[param.key]} + // onInput={(value) => onLiveValue(channel, param.key, value)} + onChange={(value) => commit({ [param.key]: value } as Partial)} + style={{ width: '100%', flex: '1 1 auto' }} + /> +
{param.label}
+
+ ))} +
+
+ ); +} + +type SetFilterFn = ( + channel: Channel, + freq: number, + filterType: CrossoverFilter +) => Promise; + +type setPeqBandFn = ( + channel: Channel, + band: PEQBand, + gainDb: number, + freqHz: number, + q: number, + filterType: PEQFilter, + bypass: boolean +) => Promise; + +function InputSection({ + dsp, + channel, + set_high_pass, + set_low_pass, + setPeq, + // setPeqBypass, +}: { + dsp: DSP; + channel: InputChannel; + set_high_pass: SetFilterFn; + set_low_pass: SetFilterFn; + setPeq: setPeqBandFn; + // setPeqBypass; +}) { + const [activeBand, setActiveBand] = useState(PEQBand.B1); + + const [liveHpFreq, setLiveHpFreq] = useState(19.7); + const [liveLpFreq, setLiveLpFreq] = useState(20.16); + const [livePeq, setLivePeq] = useState>>({}); + + const inputState = dsp.state.current_config.input_states[channel]; + + const hp = inputState.crossover.high_pass; + const lp = inputState.crossover.low_pass; + const activePeq = livePeq[activeBand] ?? (inputState.peq_chain.bands[activeBand] as PEQValues); + + const bypassed = hp.slope === CrossoverFilter.Bypass; + const activeFilterType = bypassed && lp.slope !== CrossoverFilter.Bypass ? lp.slope : hp.slope; + + // Remembers the last non-bypass slope so the Bypass button can restore it. + const lastNonBypassType = useRef( + activeFilterType === CrossoverFilter.Bypass ? DEFAULT_FILTER_TYPE : activeFilterType + ); + if (activeFilterType !== CrossoverFilter.Bypass) { + lastNonBypassType.current = activeFilterType; + } + + const hpFreq = liveHpFreq ?? hp.frequency; + const lpFreq = liveLpFreq ?? lp.frequency; + + // const commitHighPass = async (freq: number, filterType: CrossoverFilter) => { + // const success = await set_high_pass({ Input: channel }, freq, filterType); + // if (success) setLiveHpFreq(null); + // return success; + // }; + + // const commitLowPass = async (freq: number, filterType: CrossoverFilter) => { + // const success = await set_low_pass({ Input: channel }, freq, filterType); + // if (success) setLiveLpFreq(null); + // return success; + // }; + + // const handleFilterTypeChange = (filterType: CrossoverFilter) => { + // // Applying a slope change always takes the crossover out of bypass. + // void commitHighPass(hpFreq, filterType); + // void commitLowPass(lpFreq, filterType); + // }; + + // const handleBypassToggle = () => { + // if (bypassed) { + // const restore = lastNonBypassType.current; + // void commitHighPass(hpFreq, restore); + // void commitLowPass(lpFreq, restore); + // } else { + // void commitHighPass(hpFreq, CrossoverFilter.Bypass); + // void commitLowPass(lpFreq, CrossoverFilter.Bypass); + // } + // }; + + return ( + <> +
+
+
{inputState.name} - Filters
+ + {/* TODO: draw HP/LP transfer curves here once filter math is defined, + same pattern as LimiterSection's `draw` prop. */} + Math.round(v).toString()} + formatY={(v) => Math.round(v).toString()} + draw={() => {}} + /> +
+
+ +
+
+ + + + {/*
+
+ setLiveHpFreq(value)} + onChange={(value) => commitHighPass(value, activeFilterType)} + style={{ width: '100%', flex: '1 1 auto' }} + /> +
High Pass
+
+ +
+ setLiveLpFreq(value)} + onChange={(value) => commitLowPass(value, activeFilterType)} + style={{ width: '100%', flex: '1 1 auto' }} + /> +
Low Pass
+
+
+ +
+ + + +
*/} +
+
+ + ); +} + +export default InputSection; diff --git a/src/components/dsp408/LimiterSection/LimiterSection.tsx b/src/components/dsp408/LimiterSection/LimiterSection.tsx index 12bbabe..625430c 100644 --- a/src/components/dsp408/LimiterSection/LimiterSection.tsx +++ b/src/components/dsp408/LimiterSection/LimiterSection.tsx @@ -73,7 +73,7 @@ function LimiterPanel({ param.step} unit={param.unit} value={lim[param.key]} onInput={(value) => onLiveValue(channel, param.key, value)} diff --git a/src/components/dsp408/VerticalSlider/VerticalSlider.tsx b/src/components/dsp408/VerticalSlider/VerticalSlider.tsx index bf9b35c..88dbf9d 100644 --- a/src/components/dsp408/VerticalSlider/VerticalSlider.tsx +++ b/src/components/dsp408/VerticalSlider/VerticalSlider.tsx @@ -1,20 +1,20 @@ import { useEffect, useMemo, useRef, useState } from 'react'; - import './VerticalSlider.css'; +type StepFn = (value: number) => number; + type VerticalSliderProps = { min: number; max: number; - step: number; - switchStepValue?: number; - switchStep?: number; + step?: StepFn; value: number; onChange?: (value: number) => Promise; onInput?: (value: number) => void; unit?: string; + formatValue?: (value: number, decimals: number) => string; showTicks?: boolean; @@ -22,6 +22,12 @@ type VerticalSliderProps = { style?: React.CSSProperties; }; +const defaultStep: StepFn = () => 0.1; + +function formatValueDefault(v: number, decimals: number) { + return v.toFixed(decimals); +} + const MIN_TICKS = 3; const MAX_TICK_SPACING_PX = 16; @@ -34,13 +40,12 @@ function getDecimals(n: number) { export default function VerticalSlider({ min, max, - step, - switchStepValue, - switchStep, + step = defaultStep, value, onChange, onInput, unit = '', + formatValue, showTicks = true, className, style, @@ -85,26 +90,16 @@ export default function VerticalSlider({ return () => observer.disconnect(); }, []); - function getStep(value: number) { - if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) { - return switchStep; - } - - return step; - } - const decimals = useMemo( - () => - Math.min( - 3, - Math.max(getDecimals(step), switchStep !== undefined ? getDecimals(switchStep) : 0) - ), - [step, switchStep] + () => Math.min(3, getDecimals(step(displayValue))), + [step, displayValue] ); + const formattedValue = (formatValue ?? formatValueDefault)(displayValue, decimals); + function normalizeValue(input: number) { const clamped = Math.min(max, Math.max(min, input)); - const currentStep = getStep(clamped); + const currentStep = step(clamped); const snapped = min + Math.round((clamped - min) / currentStep) * currentStep; return Number(Math.min(max, Math.max(min, snapped)).toFixed(6)); } @@ -125,17 +120,11 @@ export default function VerticalSlider({ e.preventDefault(); const base = keyboardValue.current ?? value; - const currentStep = getStep(base); + const currentStep = step(base); let next = base; - - if (e.key === 'ArrowUp' || e.key === 'ArrowRight') { - next += currentStep; - } - - if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') { - next -= currentStep; - } + if (e.key === 'ArrowUp' || e.key === 'ArrowRight') next += currentStep; + if (e.key === 'ArrowDown' || e.key === 'ArrowLeft') next -= currentStep; next = normalizeValue(next); keyboardValue.current = next; @@ -265,9 +254,9 @@ export default function VerticalSlider({ ref={inputRef} type="text" inputMode="decimal" - value={editingText ?? displayValue.toFixed(decimals)} + value={editingText ?? formattedValue} style={{ width: '100%' }} - onFocus={() => setEditingText(value.toFixed(decimals))} + onFocus={() => setEditingText(value.toFixed(decimals))} // plain number while editing onChange={(e) => setEditingText(e.target.value)} onBlur={commitEditing} onKeyDown={(e) => { diff --git a/src/hooks/useDSP408.ts b/src/hooks/useDSP408.ts index 6ea9815..e0389f0 100644 --- a/src/hooks/useDSP408.ts +++ b/src/hooks/useDSP408.ts @@ -15,6 +15,9 @@ import { Meters, DSPState, DiscreteFrequency, + CrossoverFilter, + PEQFilter, + PEQBand, } from '../types/dsp408State'; type NotifyFunction = (message: string, type: 'success' | 'error', dspName?: string) => void; @@ -70,6 +73,17 @@ export type DSP408Device = { gain: number ) => Promise; setGeqBypass: (inputChannel: InputChannel, bypass: boolean) => Promise; + setLowPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => Promise; + setHighPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => Promise; + setPeqBand: ( + channel: Channel, + band: PEQBand, + gainDb: number, + freqHz: number, + q: number, + filterType: PEQFilter, + bypass: boolean + ) => Promise; }; // Polling @@ -1127,6 +1141,267 @@ export function useDSP408( }); } + async function setLowPass( + dsp: DSP, + channel: Channel, + freq: number, + filterType: CrossoverFilter + ): Promise { + return withPollingPaused(dsp, async () => { + try { + const success = await invoke('set_low_pass', { + id: dsp.id, + channel, + freq, + filterType, + }); + + if (!success) { + notify('Failed to set low-pass filter', 'error', dsp.name); + return false; + } + + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const currentConfig = d.state.current_config; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...currentConfig, + input_states: { + ...currentConfig.input_states, + [channel.Input]: { + ...currentConfig.input_states[channel.Input], + low_pass: { + frequency: freq, + filter_type: filterType, + }, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...currentConfig, + output_states: { + ...currentConfig.output_states, + [channel.Output]: { + ...currentConfig.output_states[channel.Output], + low_pass: { + frequency: freq, + filter_type: filterType, + }, + }, + }, + }, + }, + }; + }); + + notify('Low-pass filter updated', 'success', dsp.name); + return true; + } catch (err) { + notify(`Low-pass update failed: ${String(err)}`, 'error', dsp.name); + return false; + } + }); + } + + async function setHighPass( + dsp: DSP, + channel: Channel, + freq: number, + filterType: CrossoverFilter + ): Promise { + return withPollingPaused(dsp, async () => { + try { + const success = await invoke('set_high_pass', { + id: dsp.id, + channel, + freq, + filterType, + }); + + if (!success) { + notify('Failed to set high-pass filter', 'error', dsp.name); + return false; + } + + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const currentConfig = d.state.current_config; + + if ('Input' in channel) { + return { + ...d, + state: { + ...d.state, + current_config: { + ...currentConfig, + input_states: { + ...currentConfig.input_states, + [channel.Input]: { + ...currentConfig.input_states[channel.Input], + high_pass: { + frequency: freq, + filter_type: filterType, + }, + }, + }, + }, + }, + }; + } + + return { + ...d, + state: { + ...d.state, + current_config: { + ...currentConfig, + output_states: { + ...currentConfig.output_states, + [channel.Output]: { + ...currentConfig.output_states[channel.Output], + high_pass: { + frequency: freq, + filter_type: filterType, + }, + }, + }, + }, + }, + }; + }); + + notify('High-pass filter updated', 'success', dsp.name); + + return true; + } catch (err) { + notify(`High-pass update failed: ${String(err)}`, 'error', dsp.name); + + return false; + } + }); + } + + async function setPeqBand( + dsp: DSP, + channel: Channel, + band: PEQBand, + gainDb: number, + freqHz: number, + q: number, + filterType: PEQFilter, + bypass: boolean + ): Promise { + return withPollingPaused(dsp, async () => { + try { + const success = await invoke('set_peq_band', { + id: dsp.id, + channel, + band, + gainDb, + freqHz, + q, + filterType, + bypass, + }); + + if (!success) { + notify('Failed to set PEQ band', 'error', dsp.name); + return false; + } + + updateDSP(setDsps, dsp.id, (d) => { + if (!d.state) return d; + + const currentConfig = d.state.current_config; + + const peqBand = { + gain: gainDb, + frequency: freqHz, + q, + filter_type: filterType, + bypass, + }; + + if ('Input' in channel) { + const inputState = currentConfig.input_states[channel.Input]; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...currentConfig, + input_states: { + ...currentConfig.input_states, + [channel.Input]: { + ...inputState, + peq_chain: { + ...inputState.peq_chain, + bands: { + ...inputState.peq_chain.bands, + [band]: peqBand, + }, + }, + }, + }, + }, + }, + }; + } + + const outputState = currentConfig.output_states[channel.Output]; + + return { + ...d, + state: { + ...d.state, + current_config: { + ...currentConfig, + output_states: { + ...currentConfig.output_states, + [channel.Output]: { + ...outputState, + peq_chain: { + ...outputState.peq_chain, + bands: { + ...outputState.peq_chain.bands, + [band]: peqBand, + }, + }, + }, + }, + }, + }, + }; + }); + + notify('PEQ band updated', 'success', dsp.name); + + return true; + } catch (err) { + notify(`PEQ update failed: ${String(err)}`, 'error', dsp.name); + + return false; + } + }); + } + // Return API function forDSP(dsp: DSP): DSP408Device { return { @@ -1169,6 +1444,19 @@ export function useDSP408( setGeqBand(dsp, inputChannel, frequencyBand, gain), setGeqBypass: (inputChannel: InputChannel, bypass: boolean) => setGeqBypass(dsp, inputChannel, bypass), + setLowPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => + setLowPass(dsp, channel, freq, filterType), + setHighPass: (channel: Channel, freq: number, filterType: CrossoverFilter) => + setHighPass(dsp, channel, freq, filterType), + setPeqBand: ( + channel: Channel, + band: PEQBand, + gainDb: number, + freqHz: number, + q: number, + filterType: PEQFilter, + bypass: boolean + ) => setPeqBand(dsp, channel, band, gainDb, freqHz, q, filterType, bypass), }; } @@ -1181,9 +1469,6 @@ export function useDSP408( connectDSP, disconnectDSP, - recallPreset, - setCurrentPresetName, - setDSPState, }; } diff --git a/src/pages/DspPage/DspPage.tsx b/src/pages/DspPage/DspPage.tsx index 5d0309d..591d372 100644 --- a/src/pages/DspPage/DspPage.tsx +++ b/src/pages/DspPage/DspPage.tsx @@ -1,7 +1,7 @@ import { useState } from 'react'; import { DSP } from '../../types/types'; import Sidebar from '../../components/Sidebar/Sidebar'; -import { InfoIcon, KnobGainIcon } from '../../assets/icons/AudioIcons'; +import { InfoIcon, KnobGainIcon, TextIcon } from '../../assets/icons/AudioIcons'; import ConnectionPanel from '../../components/dsp408/ConnectionSection/ConnectionSection'; import GainSection from '../../components/dsp408/GainSection/GainSection'; import CompressorSection from '../../components/dsp408/CompressorSection/CompressorSection'; @@ -14,6 +14,7 @@ import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay'; import { InputChannel, OutputChannel } from '../../types/dsp408State'; import MatrixSection from '../../components/dsp408/Matrixsection/Matrixsection'; import GeqSection from '../../components/dsp408/GeqSection/GeqSection'; +import InputSection from '../../components/dsp408/InputSection/InputSection'; function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) { const [activeSidebar, setActiveSidebar] = useState('overview'); @@ -65,69 +66,69 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) { }, { id: 'ina', - label: `In A (${inputStates?.[InputChannel.InA]?.name})`, - icon: , + label: `${inputStates?.[InputChannel.InA]?.name}`, + icon: In A, }, { id: 'inb', - label: `In B (${inputStates?.[InputChannel.InB]?.name})`, - icon: , + label: `${inputStates?.[InputChannel.InB]?.name}`, + icon: In B, }, { id: 'inc', - label: `In C (${inputStates?.[InputChannel.InC]?.name})`, - icon: , + label: `${inputStates?.[InputChannel.InC]?.name}`, + icon: In C, }, { id: 'ind', - label: `In D (${inputStates?.[InputChannel.InD]?.name})`, - icon: , + label: `${inputStates?.[InputChannel.InD]?.name}`, + icon: In D, }, { id: 'out1', - label: `Out 1 (${outputStates?.[OutputChannel.Out1]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out1]?.name}`, + icon: Out 1, }, { id: 'out2', - label: `Out 2 (${outputStates?.[OutputChannel.Out2]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out2]?.name}`, + icon: Out 2, }, { id: 'out3', - label: `Out 3 (${outputStates?.[OutputChannel.Out3]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out3]?.name}`, + icon: Out 3, }, { id: 'out4', - label: `Out 4 (${outputStates?.[OutputChannel.Out4]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out4]?.name}`, + icon: Out 4, }, { id: 'out5', - label: `Out 5 (${outputStates?.[OutputChannel.Out5]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out5]?.name}`, + icon: Out 5, }, { id: 'out6', - label: `Out 6 (${outputStates?.[OutputChannel.Out6]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out6]?.name}`, + icon: Out 6, }, { id: 'out7', - label: `Out 7 (${outputStates?.[OutputChannel.Out7]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out7]?.name}`, + icon: Out 7, }, { id: 'out8', - label: `Out 8 (${outputStates?.[OutputChannel.Out8]?.name})`, - icon: , + label: `${outputStates?.[OutputChannel.Out8]?.name}`, + icon: Out 8, }, { id: 'miscellaneous', label: 'Miscellaneous', - icon: , + icon: Out 1, }, ]; const presets = dsp.state?.presets.names @@ -200,6 +201,17 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) { /> ); + case 'ina': + return ( + + ); + case 'matrix': return ( ; bypass: boolean; }; @@ -291,7 +303,11 @@ const createDefaultPEQ = (): PEQ => ({ }); const createDefaultPEQChain = (): PEQChain => ({ - bands: Array.from({ length: 8 }, createDefaultPEQ), + bands: Object.fromEntries( + Object.values(PEQBand) + .filter((band): band is PEQBand => typeof band === 'number') + .map((band) => [band, createDefaultPEQ()]) + ) as Record, bypass: false, });