Compare commits

..
5 Commits
Author SHA1 Message Date
LucasDLTG 4f2bce1f5f fix: preset renaming 2026-09-13 17:13:03 +02:00
LucasDLTG 8c0b272ab9 refactor: reorder tauri commands and highlight missing 2026-09-13 16:24:08 +02:00
LucasDLTG 772a331f04 fix: hanging notifications 2026-09-13 15:55:11 +02:00
LucasDLTG 1643a887af feat: add peq output pages 2026-09-13 15:48:26 +02:00
LucasDLTG 8e9a59d026 feat: finalize input section 2026-09-13 14:25:00 +02:00
9 changed files with 435 additions and 335 deletions
+226 -196
View File
@@ -109,18 +109,6 @@ pub async fn get_dsp_snapshot(
}) })
} }
#[tauri::command]
pub async fn connect_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), String> {
state.with_device_mut(id, |device| device.dsp.connect().map_err(|e| e.to_string()))
}
#[tauri::command]
pub async fn disconnect_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), String> {
state.with_device_mut(id, |device| {
device.dsp.disconnect().map_err(|e| e.to_string())
})
}
#[tauri::command] #[tauri::command]
pub async fn get_dsp_state( pub async fn get_dsp_state(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -133,57 +121,33 @@ pub async fn get_dsp_state(
}) })
} }
// Commands
#[tauri::command] #[tauri::command]
pub async fn set_channel_gain( pub async fn connect_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), String> {
state: State<'_, AppState>, state.with_device_mut(id, |device| device.dsp.connect().map_err(|e| e.to_string()))
id: u64, }
channel: dsp_thomann::dsp408::types::Channel,
db: f32,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
let r = device
.dsp
.set_channel_gain(channel, db)
.map_err(|e| e.to_string())?;
Ok(r) // Command `0x11`
#[tauri::command]
pub async fn disconnect_dsp408(state: State<'_, AppState>, id: u64) -> Result<(), String> {
state.with_device_mut(id, |device| {
device.dsp.disconnect().map_err(|e| e.to_string())
}) })
} }
// Command `0x15`
#[tauri::command] #[tauri::command]
pub async fn set_mute( pub async fn set_delay_unit(
state: State<'_, AppState>, state: State<'_, AppState>,
id: u64, id: u64,
channel: dsp_thomann::dsp408::types::Channel, unit: types::DelayUnit,
muted: bool,
) -> Result<bool, String> { ) -> Result<bool, String> {
state.with_device_mut(id, |device| { state.with_device_mut(id, |device| {
let r = device device.dsp.set_delay_unit(unit).map_err(|e| e.to_string())
.dsp
.set_mute(channel, muted)
.map_err(|e| e.to_string())?;
Ok(r)
})
}
#[tauri::command]
pub async fn set_channel_inverse_gain(
state: State<'_, AppState>,
id: u64,
channel: dsp_thomann::dsp408::types::Channel,
inverted: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
let r = device
.dsp
.set_channel_inverse_gain(channel, inverted)
.map_err(|e| e.to_string())?;
Ok(r)
}) })
} }
// Command `0x20`
#[tauri::command] #[tauri::command]
pub async fn recall_preset( pub async fn recall_preset(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -200,16 +164,35 @@ pub async fn recall_preset(
}) })
} }
// Command `0x21` -- TODO
#[tauri::command] #[tauri::command]
pub async fn get_meter_levels( pub async fn store_current_configuration(
state: State<'_, AppState>, state: State<'_, AppState>,
id: u64, id: u64,
) -> Result<(types::InputMeters, types::OutputMeters), String> { preset_index: usize,
force: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| { state.with_device_mut(id, |device| {
device.dsp.get_meter_levels().map_err(|e| e.to_string()) device
.dsp
.store_current_configuration(preset_index, force)
.map_err(|e| e.to_string())
}) })
} }
// Command `0x23` -- TODO
#[tauri::command]
pub async fn factory_reset(
state: State<'_, AppState>,
id: u64,
force: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device.dsp.factory_reset(force).map_err(|e| e.to_string())
})
}
// Command `0x26`
#[tauri::command] #[tauri::command]
pub async fn set_current_preset_name( pub async fn set_current_preset_name(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -224,24 +207,26 @@ pub async fn set_current_preset_name(
}) })
} }
// Command `0x2A` -- TODO
#[tauri::command] #[tauri::command]
pub async fn set_gate( pub async fn copy_channel(
state: State<'_, AppState>, state: State<'_, AppState>,
id: u64, id: u64,
channel: types::InputChannel, channel_1: types::Channel,
threshold: f32, channel_2: types::Channel,
attack: u16,
hold: u16,
release: u16,
) -> Result<bool, String> { ) -> Result<bool, String> {
state.with_device_mut(id, |device| { state.with_device_mut(id, |device| {
device device
.dsp .dsp
.set_gate(channel, threshold, attack, hold, release) .copy_channel(channel_1, channel_2)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}) })
} }
// Command `0x2D` -- TODO
// Command `0x2F` -- TODO
// Command `0x30`
#[tauri::command] #[tauri::command]
pub async fn set_compressor( pub async fn set_compressor(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -261,112 +246,7 @@ pub async fn set_compressor(
}) })
} }
#[tauri::command] // Command `0x31`
pub async fn set_limiter(
state: State<'_, AppState>,
id: u64,
channel: types::OutputChannel,
threshold: f32,
attack: u16,
release: u16,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_limiter(channel, threshold, attack, release)
.map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn set_delay_unit(
state: State<'_, AppState>,
id: u64,
unit: types::DelayUnit,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device.dsp.set_delay_unit(unit).map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn set_delay(
state: State<'_, AppState>,
id: u64,
channel: types::Channel,
delay: f32,
unit: DelayUnit,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_delay(channel, delay, unit)
.map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn set_matrix_routing(
state: State<'_, AppState>,
id: u64,
output: types::OutputChannel,
inputs: Vec<types::InputChannel>,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_matrix_routing(output, inputs)
.map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn set_matrix_gain(
state: State<'_, AppState>,
id: u64,
input_channel: types::InputChannel,
output_channel: types::OutputChannel,
gain: f32,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_matrix_gain(input_channel, output_channel, gain)
.map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn set_geq_band(
state: State<'_, AppState>,
id: u64,
input_channel: types::InputChannel,
frequency_band: types::DiscreteFrequency,
gain: f32,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_geq_band(input_channel, frequency_band, gain)
.map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn set_geq_bypass(
state: State<'_, AppState>,
id: u64,
input_channel: types::InputChannel,
bypass: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_geq_bypass(input_channel, bypass)
.map_err(|e| e.to_string())
})
}
#[tauri::command] #[tauri::command]
pub async fn set_low_pass( pub async fn set_low_pass(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -383,6 +263,7 @@ pub async fn set_low_pass(
}) })
} }
// Command `0x32`
#[tauri::command] #[tauri::command]
pub async fn set_high_pass( pub async fn set_high_pass(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -399,6 +280,7 @@ pub async fn set_high_pass(
}) })
} }
// Command `0x33`
#[tauri::command] #[tauri::command]
pub async fn set_peq_band( pub async fn set_peq_band(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -419,47 +301,78 @@ pub async fn set_peq_band(
}) })
} }
// Command `0x34`
#[tauri::command] #[tauri::command]
pub async fn store_current_configuration( pub async fn set_channel_gain(
state: State<'_, AppState>, state: State<'_, AppState>,
id: u64, id: u64,
preset_index: usize, channel: dsp_thomann::dsp408::types::Channel,
force: bool, db: f32,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
let r = device
.dsp
.set_channel_gain(channel, db)
.map_err(|e| e.to_string())?;
Ok(r)
})
}
// Command `0x35`
#[tauri::command]
pub async fn set_mute(
state: State<'_, AppState>,
id: u64,
channel: dsp_thomann::dsp408::types::Channel,
muted: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
let r = device
.dsp
.set_mute(channel, muted)
.map_err(|e| e.to_string())?;
Ok(r)
})
}
// Command `0x36`
#[tauri::command]
pub async fn set_channel_inverse_gain(
state: State<'_, AppState>,
id: u64,
channel: dsp_thomann::dsp408::types::Channel,
inverted: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
let r = device
.dsp
.set_channel_inverse_gain(channel, inverted)
.map_err(|e| e.to_string())?;
Ok(r)
})
}
// Command `0x38`
#[tauri::command]
pub async fn set_delay(
state: State<'_, AppState>,
id: u64,
channel: types::Channel,
delay: f32,
unit: DelayUnit,
) -> Result<bool, String> { ) -> Result<bool, String> {
state.with_device_mut(id, |device| { state.with_device_mut(id, |device| {
device device
.dsp .dsp
.store_current_configuration(preset_index, force) .set_delay(channel, delay, unit)
.map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn factory_reset(
state: State<'_, AppState>,
id: u64,
force: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device.dsp.factory_reset(force).map_err(|e| e.to_string())
})
}
#[tauri::command]
pub async fn copy_channel(
state: State<'_, AppState>,
id: u64,
channel_1: types::Channel,
channel_2: types::Channel,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.copy_channel(channel_1, channel_2)
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}) })
} }
// Command `0x39` -- TODO
#[tauri::command] #[tauri::command]
pub async fn set_input_source( pub async fn set_input_source(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -474,6 +387,23 @@ pub async fn set_input_source(
}) })
} }
// Command `0x3A`
#[tauri::command]
pub async fn set_matrix_routing(
state: State<'_, AppState>,
id: u64,
output: types::OutputChannel,
inputs: Vec<types::InputChannel>,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_matrix_routing(output, inputs)
.map_err(|e| e.to_string())
})
}
// Command `0x3B` -- TODO
#[tauri::command] #[tauri::command]
pub async fn link_channels( pub async fn link_channels(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -489,6 +419,7 @@ pub async fn link_channels(
}) })
} }
// Command `0x3C`
#[tauri::command] #[tauri::command]
pub async fn set_bypass_peq( pub async fn set_bypass_peq(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -504,6 +435,7 @@ pub async fn set_bypass_peq(
}) })
} }
// Command `0x3D` -- TODO
#[tauri::command] #[tauri::command]
pub async fn set_channel_name( pub async fn set_channel_name(
state: State<'_, AppState>, state: State<'_, AppState>,
@@ -518,3 +450,101 @@ pub async fn set_channel_name(
.map_err(|e| e.to_string()) .map_err(|e| e.to_string())
}) })
} }
// Command `0x3E`
#[tauri::command]
pub async fn set_gate(
state: State<'_, AppState>,
id: u64,
channel: types::InputChannel,
threshold: f32,
attack: u16,
hold: u16,
release: u16,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_gate(channel, threshold, attack, hold, release)
.map_err(|e| e.to_string())
})
}
// Command `0x3F`
#[tauri::command]
pub async fn set_limiter(
state: State<'_, AppState>,
id: u64,
channel: types::OutputChannel,
threshold: f32,
attack: u16,
release: u16,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_limiter(channel, threshold, attack, release)
.map_err(|e| e.to_string())
})
}
// Command `0x40`
#[tauri::command]
pub async fn get_meter_levels(
state: State<'_, AppState>,
id: u64,
) -> Result<(types::InputMeters, types::OutputMeters), String> {
state.with_device_mut(id, |device| {
device.dsp.get_meter_levels().map_err(|e| e.to_string())
})
}
// Command `0x41`
#[tauri::command]
pub async fn set_matrix_gain(
state: State<'_, AppState>,
id: u64,
input_channel: types::InputChannel,
output_channel: types::OutputChannel,
gain: f32,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_matrix_gain(input_channel, output_channel, gain)
.map_err(|e| e.to_string())
})
}
// Command `0x48`
#[tauri::command]
pub async fn set_geq_band(
state: State<'_, AppState>,
id: u64,
input_channel: types::InputChannel,
frequency_band: types::DiscreteFrequency,
gain: f32,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_geq_band(input_channel, frequency_band, gain)
.map_err(|e| e.to_string())
})
}
// Command `0x49`
#[tauri::command]
pub async fn set_geq_bypass(
state: State<'_, AppState>,
id: u64,
input_channel: types::InputChannel,
bypass: bool,
) -> Result<bool, String> {
state.with_device_mut(id, |device| {
device
.dsp
.set_geq_bypass(input_channel, bypass)
.map_err(|e| e.to_string())
})
}
+19 -9
View File
@@ -1,4 +1,3 @@
/* ChoiceSelect.css */
.choice-select { .choice-select {
position: relative; position: relative;
display: flex; display: flex;
@@ -11,12 +10,12 @@
.choice-select__input { .choice-select__input {
flex: 1 1 auto; flex: 1 1 auto;
height: var(--card-control-height); height: var(--card-control-height);
padding: 0 var(--space-6) 0 var(--space-3); padding: 0 var(--space-6) 0 var(--space-3);
background: var(--bg-panel); background: transparent;
color: var(--text-primary); color: var(--text-primary);
border: var(--border-width) solid var(--border-hairline); border: var(--border-width) solid var(--border-hairline);
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
@@ -43,13 +42,24 @@
.choice-select__chevron { .choice-select__chevron {
position: absolute; position: absolute;
right: var(--space-2); top: 0;
top: 50%; right: 5%;
height: 45%; bottom: 0;
aspect-ratio: 1;
transform: translateY(-50%); width: 4cqh;
padding-top: 3%;
padding-bottom: 3%;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none; pointer-events: none;
color: var(--accent-brand); color: var(--accent-brand);
font-size: var(--font-sm); }
.choice-select__chevron svg {
width: 100%;
height: 100%;
display: block;
} }
@@ -1,7 +1,7 @@
.notifications { .notifications {
position: fixed; position: fixed;
right: var(--space-5); right: var(--space-5);
bottom: var(--space-5); top: var(--space-5);
z-index: 9999; z-index: 9999;
display: flex; display: flex;
+10 -7
View File
@@ -46,8 +46,8 @@ function AppCombobox({
appOptions: SidebarAppOption[]; appOptions: SidebarAppOption[];
selectedAppId?: string; selectedAppId?: string;
appName: string; appName: string;
onAppChange?: (id: string) => void; onAppChange: (id: string) => void;
onAppRename?: (name: string) => void; onAppRename: (name: string) => void;
}) { }) {
const currentOption = appOptions.find((o) => o.id === selectedAppId) ?? appOptions[0]; const currentOption = appOptions.find((o) => o.id === selectedAppId) ?? appOptions[0];
const canRename = currentOption?.id !== 'f00'; const canRename = currentOption?.id !== 'f00';
@@ -95,6 +95,7 @@ function AppCombobox({
if (!canRename) return; if (!canRename) return;
setInputValue((currentOption?.name ?? appName).trim());
setEditing(true); setEditing(true);
setOpen(false); setOpen(false);
@@ -124,13 +125,13 @@ function AppCombobox({
if (matched) { if (matched) {
if (matched.id !== selectedAppId) { if (matched.id !== selectedAppId) {
onAppChange?.(matched.id); onAppChange(matched.id);
} }
return; return;
} }
if (trimmed !== currentOption?.name) { if (trimmed !== currentOption?.name) {
onAppRename?.(trimmed); onAppRename(trimmed);
} }
} }
@@ -186,6 +187,7 @@ function AppCombobox({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
aria-haspopup="listbox" aria-haspopup="listbox"
aria-expanded={open} aria-expanded={open}
title={canRename ? 'Double click to rename' : undefined}
> >
<span className="sidebar-app-trigger-label">{currentOption?.name ?? appName}</span> <span className="sidebar-app-trigger-label">{currentOption?.name ?? appName}</span>
<svg className="sidebar-app-trigger-caret" viewBox="0 0 24 24" width="14" height="14"> <svg className="sidebar-app-trigger-caret" viewBox="0 0 24 24" width="14" height="14">
@@ -209,7 +211,8 @@ function AppCombobox({
onMouseEnter={() => setHighlight(i)} onMouseEnter={() => setHighlight(i)}
onClick={() => pick(opt)} onClick={() => pick(opt)}
> >
{opt.name} <span className="sidebar-app-option-id">{opt.id + '-'}</span>
<span>{opt.name}</span>
</li> </li>
))} ))}
</ul> </ul>
@@ -236,8 +239,8 @@ export function Sidebar<T extends string>({
appName: string; appName: string;
appOptions?: SidebarAppOption[]; appOptions?: SidebarAppOption[];
selectedAppId?: string; selectedAppId?: string;
onAppChange?: (id: string) => void; onAppChange: (id: string) => void;
onAppRename?: (name: string) => void; onAppRename: (name: string) => void;
expanded: boolean; expanded: boolean;
onToggle: () => void; onToggle: () => void;
}) { }) {
@@ -11,7 +11,7 @@ import LoadingOverlay from '../../../../components/LoadingOverlay/LoadingOverlay
import { InputChannel, OutputChannel } from '../../types'; import { InputChannel, OutputChannel } from '../../types';
import MatrixSection from '../Matrixsection/Matrixsection'; import MatrixSection from '../Matrixsection/Matrixsection';
import GeqSection from '../GeqSection/GeqSection'; import GeqSection from '../GeqSection/GeqSection';
import InputSection from '../InputSection/InputSection'; import ChannelSection from '../InputSection/ChannelSection';
import { DSP408Api } from '../../dsp408'; import { DSP408Api } from '../../dsp408';
import { DSP408 } from '../../../../types'; import { DSP408 } from '../../../../types';
import { SidebarId } from '../../constants'; import { SidebarId } from '../../constants';
@@ -134,54 +134,6 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
case 'delay': case 'delay':
return <DelaySection dsp={dsp} setDelay={api.setDelay} setDelayUnit={api.setDelayUnit} />; return <DelaySection dsp={dsp} setDelay={api.setDelay} setDelayUnit={api.setDelayUnit} />;
case 'ina':
return (
<InputSection
dsp={dsp}
channel={InputChannel.InA}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'inb':
return (
<InputSection
dsp={dsp}
channel={InputChannel.InB}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'inc':
return (
<InputSection
dsp={dsp}
channel={InputChannel.InC}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'ind':
return (
<InputSection
dsp={dsp}
channel={InputChannel.InD}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'matrix': case 'matrix':
return ( return (
<MatrixSection <MatrixSection
@@ -195,6 +147,66 @@ function DSP408Panel({ dsp, api }: { dsp: DSP408; api: DSP408Api }) {
case 'geq': case 'geq':
return <GeqSection dsp={dsp} setGeqBand={api.setGeqBand} setGeqBypass={api.setGeqBypass} />; return <GeqSection dsp={dsp} setGeqBand={api.setGeqBand} setGeqBypass={api.setGeqBypass} />;
case 'ina':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InA }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'inb':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InB }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'inc':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InC }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'ind':
return (
<ChannelSection
dsp={dsp}
channel={{ Input: InputChannel.InD }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
case 'out1':
return (
<ChannelSection
dsp={dsp}
channel={{ Output: OutputChannel.Out1 }}
setLowPass={api.setLowPass}
setHighPass={api.setHighPass}
setPeq={api.setPeqBand}
setPeqBypass={api.setPEQBypass}
/>
);
default: default:
return null; return null;
} }
@@ -1,5 +1,5 @@
/* InputSection.css */ /* InputSection.css */
.input-graph-section { .channel-graph-section {
flex: 0 0 calc(100% - var(--dsp-control-heigth)); flex: 0 0 calc(100% - var(--dsp-control-heigth));
min-height: 0; min-height: 0;
@@ -15,7 +15,8 @@
-webkit-user-select: none; -webkit-user-select: none;
} }
.input-graph-panel { .channel-graph-panel {
position: relative;
flex: 1 1 auto; flex: 1 1 auto;
min-width: 0; min-width: 0;
min-height: 0; min-height: 0;
@@ -27,7 +28,28 @@
border-radius: var(--radius-md); border-radius: var(--radius-md);
} }
.input-graph__title { .channel-bypass-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
z-index: 2;
}
.channel-bypass-overlay span {
font-size: 8cqh;
font-weight: 800;
letter-spacing: 0.15em;
color: #f87171;
text-shadow: 0 0 12px rgba(0, 0, 0, 0.6);
transform: rotate(-8deg);
opacity: 0.85;
user-select: none;
}
.channel-graph__title {
flex: 0 0 auto; flex: 0 0 auto;
text-align: center; text-align: center;
@@ -38,7 +60,7 @@
font-weight: var(--font-weight-semibold); font-weight: var(--font-weight-semibold);
} }
.input-graph-panel > .container { .channel-graph-panel > .container {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
} }
@@ -46,7 +68,7 @@
/* Bottom control bar — same pattern as .compressor-section: absolute, /* Bottom control bar — same pattern as .compressor-section: absolute,
bottom-anchored, panels flow in a row and scroll horizontally instead bottom-anchored, panels flow in a row and scroll horizontally instead
of squeezing into a fixed max-width. */ of squeezing into a fixed max-width. */
.input-control-section { .channel-control-section {
container-type: size; container-type: size;
position: absolute; position: absolute;
left: 0; left: 0;
@@ -71,7 +93,7 @@
-webkit-user-select: none; -webkit-user-select: none;
} }
.input-control-panel { .channel-control-panel {
display: grid; display: grid;
grid-auto-flow: column; grid-auto-flow: column;
height: 100%; height: 100%;
@@ -84,9 +106,9 @@
} }
/* Peq Sliders */ /* Peq Sliders */
.input-panel { .channel-panel {
position: relative; position: relative;
height: 10; height: 100%;
aspect-ratio: 0.8; aspect-ratio: 0.8;
flex: 0 0 auto; flex: 0 0 auto;
@@ -102,7 +124,7 @@
background: var(--bg-void); background: var(--bg-void);
} }
.input-panel__title { .channel-panel__title {
flex: 0 0 auto; flex: 0 0 auto;
width: 100%; width: 100%;
text-align: center; text-align: center;
@@ -118,7 +140,7 @@
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.input-panel__grid { .channel-panel__grid {
flex: 1 1 auto; flex: 1 1 auto;
min-height: 0; min-height: 0;
@@ -132,7 +154,7 @@
box-sizing: border-box; box-sizing: border-box;
} }
.input-panel__cell { .channel-panel__cell {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@@ -142,7 +164,7 @@
gap: 1cqh; gap: 1cqh;
} }
.input-panel__cell-label { .channel-panel__cell-label {
flex: 0 0 auto; flex: 0 0 auto;
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 3.2cqh; font-size: 3.2cqh;
@@ -200,11 +222,20 @@
} }
.crossover-panel__select { .crossover-panel__select {
flex: 0 0 auto;
width: 85%;
font-family: var(--font-ui);
font-size: 4cqh; font-size: 4cqh;
} }
.crossover-panel__select .choice-select__input { .crossover-panel__select .choice-select__input,
.crossover-panel__select .choice-select__input option {
height: 100%;
padding: 0;
padding-left: 2cqh;
font-size: 4cqh; font-size: 4cqh;
color: var(--accent-brand);
border-radius: var(--radius-sm);
} }
.peq-band-list { .peq-band-list {
@@ -223,17 +254,16 @@
padding: 1cqh; padding: 1cqh;
overflow: hidden; overflow: hidden;
aspect-ratio: 2;
} }
.peq-band-list__row { .peq-band-list__row {
flex: 1 1 0; flex: 1 1 auto;
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
display: grid; display: grid;
grid-template-columns: 1fr 1.2fr 1fr 1fr 1.6fr 1.2fr; grid-template-columns: 0.4fr 0.55fr 0.4fr 0.5fr 1.2fr 0.5fr;
align-items: stretch; align-items: stretch;
gap: 1cqh; gap: 1cqh;
@@ -307,14 +337,14 @@
font-size: 4cqh; font-size: 4cqh;
} }
.input-graph-panel__magnitude-wrap { .channel-graph-panel__magnitude-wrap {
flex: 1 1 0; flex: 1 1 0;
min-height: 0; min-height: 0;
min-width: 0; min-width: 0;
position: relative; position: relative;
} }
.input-graph__title-row { .channel-graph__title-row {
flex: 0 0 auto; flex: 0 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -322,11 +352,11 @@
padding: 1cqh 1.5cqh; padding: 1cqh 1.5cqh;
} }
.input-graph__title-row .input-graph__title { .channel-graph__title-row .channel-graph__title {
padding: 0; /* was on the standalone title element; row now owns spacing */ padding: 0; /* was on the standalone title element; row now owns spacing */
} }
.input-graph-view-toggle { .channel-graph-view-toggle {
display: flex; display: flex;
gap: 0.4cqh; gap: 0.4cqh;
border: var(--border-width) solid var(--border-hairline); border: var(--border-width) solid var(--border-hairline);
@@ -335,7 +365,7 @@
background: var(--bg-void); background: var(--bg-void);
} }
.input-graph-view-toggle__btn { .channel-graph-view-toggle__btn {
font-family: var(--font-ui); font-family: var(--font-ui);
font-size: 2cqh; font-size: 2cqh;
padding: 0.6cqh 1.2cqh; padding: 0.6cqh 1.2cqh;
@@ -346,8 +376,8 @@
cursor: pointer; cursor: pointer;
} }
.input-graph-view-toggle__btn.active { .channel-graph-view-toggle__btn.active {
background: var(--accent, #4dd0e1); background: var(--accent-brand);
color: var(--bg-void); color: var(--bg-void);
} }
@@ -361,4 +391,9 @@
color: var(--accent-brand); color: var(--accent-brand);
background: transparent; background: transparent;
text-align: center;
text-align-last: center;
padding: 0;
margin: 0;
} }
@@ -1,4 +1,4 @@
// InputSection.tsx // ChannelSection.tsx
import VerticalSlider, { import VerticalSlider, {
makeLogStep, makeLogStep,
makeLogQuantize, makeLogQuantize,
@@ -12,12 +12,12 @@ import {
PEQ_Q_RANGES, PEQ_Q_RANGES,
PEAK_Q_VALUES, PEAK_Q_VALUES,
} from '../../constants'; } from '../../constants';
import { InputChannel, Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types'; import { Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types';
import { DSP408 as DSP } from '../../../../types'; import { DSP408 as DSP } from '../../../../types';
import { CrossoverFilter } from '../../types'; import { CrossoverFilter } from '../../types';
import { LinearGraph, LinearGraphHandle } from '../../../../components/LinearGraph/LinearGraph'; import { LinearGraph, LinearGraphHandle } from '../../../../components/LinearGraph/LinearGraph';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import './InputSection.css'; import './ChannelSection.css';
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
import Button from '../../../../components/Button/Button'; import Button from '../../../../components/Button/Button';
import { import {
@@ -50,12 +50,12 @@ function GraphViewToggle({
]; ];
return ( return (
<div className="input-graph-view-toggle"> <div className="channel-graph-view-toggle">
{options.map((opt) => ( {options.map((opt) => (
<button <button
key={opt.value} key={opt.value}
type="button" type="button"
className={`input-graph-view-toggle__btn${view === opt.value ? ' active' : ''}`} className={`channel-graph-view-toggle__btn${view === opt.value ? ' active' : ''}`}
onClick={() => onChange(opt.value)} onClick={() => onChange(opt.value)}
> >
{opt.label} {opt.label}
@@ -65,7 +65,7 @@ function GraphViewToggle({
); );
} }
function InputActions({ function ChannelActions({
bypass, bypass,
resetting, resetting,
onBypass, onBypass,
@@ -135,11 +135,11 @@ function PeqPanelSliders({
}; };
return ( return (
<div className="input-panel"> <div className="channel-panel">
<div className="input-panel__title">PEQ Parameters</div> <div className="channel-panel__title">PEQ Parameters</div>
<div className="input-panel__grid"> <div className="channel-panel__grid">
<div className="input-panel__cell input-panel__cell--frequency"> <div className="channel-panel__cell channel-panel__cell--frequency">
<VerticalSlider <VerticalSlider
min={FREQUENCY_MIN} min={FREQUENCY_MIN}
max={FREQUENCY_MAX} max={FREQUENCY_MAX}
@@ -156,10 +156,10 @@ function PeqPanelSliders({
style={{ width: '100%', flex: '1 1 auto' }} style={{ width: '100%', flex: '1 1 auto' }}
/> />
<div className="input-panel__cell-label">Frequency</div> <div className="channel-panel__cell-label">Frequency</div>
</div> </div>
<div className="input-panel__cell input-panel__cell--q"> <div className="channel-panel__cell channel-panel__cell--q">
<VerticalSlider <VerticalSlider
min={qMin} min={qMin}
max={qMax} max={qMax}
@@ -171,10 +171,10 @@ function PeqPanelSliders({
style={{ width: '100%', flex: '1 1 auto' }} style={{ width: '100%', flex: '1 1 auto' }}
/> />
<div className="input-panel__cell-label">Q</div> <div className="channel-panel__cell-label">Q</div>
</div> </div>
<div className="input-panel__cell input-panel__cell--gain"> <div className="channel-panel__cell channel-panel__cell--gain">
<VerticalSlider <VerticalSlider
min={-12} min={-12}
max={12} max={12}
@@ -187,7 +187,7 @@ function PeqPanelSliders({
style={{ width: '100%', flex: '1 1 auto' }} style={{ width: '100%', flex: '1 1 auto' }}
/> />
<div className="input-panel__cell-label">Gain</div> <div className="channel-panel__cell-label">Gain</div>
</div> </div>
</div> </div>
</div> </div>
@@ -295,6 +295,7 @@ function PeqBandList({
activeBand, activeBand,
setActiveBand, setActiveBand,
setPEQ, setPEQ,
bandsOrder,
}: { }: {
bands: Record<PEQBand, PEQ>; bands: Record<PEQBand, PEQ>;
liveBands: Partial<Record<PEQBand, PEQ>>; liveBands: Partial<Record<PEQBand, PEQ>>;
@@ -308,6 +309,7 @@ function PeqBandList({
filterType: PEQFilter, filterType: PEQFilter,
bypass: boolean bypass: boolean
) => Promise<boolean>; ) => Promise<boolean>;
bandsOrder: PEQBand[];
}) { }) {
const commit = (band: PEQBand, overrides: Partial<PEQ>) => { const commit = (band: PEQBand, overrides: Partial<PEQ>) => {
const peq = liveBands[band] ?? bands[band]; const peq = liveBands[band] ?? bands[band];
@@ -326,7 +328,7 @@ function PeqBandList({
<div className="peq-band-list__cell">Bypass</div> <div className="peq-band-list__cell">Bypass</div>
</div> </div>
{PEQ_BANDS_ORDER.map((band, i) => { {bandsOrder.map((band, i) => {
const peq = liveBands[band] ?? bands[band]; const peq = liveBands[band] ?? bands[band];
const isActive = band === activeBand; const isActive = band === activeBand;
@@ -395,7 +397,7 @@ function PeqBandList({
variant="toggle" variant="toggle"
className={peq.bypass ? 'active-warning' : ''} className={peq.bypass ? 'active-warning' : ''}
onClick={() => commit(band, { bypass: !peq.bypass })} onClick={() => commit(band, { bypass: !peq.bypass })}
style={{ minHeight: 0 }} style={{ minHeight: 0, fontSize: '4cqh', paddingInline: 0 }}
> >
{peq.bypass ? 'Bypassed' : 'Bypass'} {peq.bypass ? 'Bypassed' : 'Bypass'}
</Button> </Button>
@@ -412,7 +414,7 @@ type SetFilterFn = (
filterType: CrossoverFilter filterType: CrossoverFilter
) => Promise<boolean>; ) => Promise<boolean>;
function InputSection({ function ChannelSection({
dsp, dsp,
channel, channel,
setHighPass, setHighPass,
@@ -421,7 +423,7 @@ function InputSection({
setPeqBypass, setPeqBypass,
}: { }: {
dsp: DSP; dsp: DSP;
channel: InputChannel; channel: Channel;
setHighPass: SetFilterFn; setHighPass: SetFilterFn;
setLowPass: SetFilterFn; setLowPass: SetFilterFn;
setPeq: ( setPeq: (
@@ -448,18 +450,25 @@ function InputSection({
const magnitudeGraphRef = useRef<LinearGraphHandle>(null); const magnitudeGraphRef = useRef<LinearGraphHandle>(null);
// DSP values // DSP values
const activeInputState = dsp.state.current_config.input_states[channel]; const isInput = 'Input' in channel;
const ActiveHighPassFilter = activeInputState.crossover.high_pass;
const ActiveLowPassFilter = activeInputState.crossover.low_pass; const activeChannelState = isInput
const ActivePeq = activeInputState.peq_chain; ? dsp.state.current_config.input_states[channel.Input]
: dsp.state.current_config.output_states[channel.Output];
const ActiveHighPassFilter = activeChannelState.crossover.high_pass;
const ActiveLowPassFilter = activeChannelState.crossover.low_pass;
const ActivePeq = activeChannelState.peq_chain;
const bandsOrder = isInput ? PEQ_BANDS_ORDER.slice(0, -1) : PEQ_BANDS_ORDER;
const handleResetPeq = async () => { const handleResetPeq = async () => {
if (resetting) return; if (resetting) return;
setResetting(true); setResetting(true);
for (const band of PEQ_BANDS_ORDER) { for (const band of bandsOrder) {
const current = displayPeq[band]; const current = displayPeq[band];
if (current.gain === 0 || current.filter_type === PEQFilter.Peak) continue; // skip bands already flat // Skip only if already flat — nothing to change.
if (current.gain === 0 && current.filter_type === PEQFilter.Peak) continue;
await commitPEQ(band, 0, current.frequency, current.q, PEQFilter.Peak, current.bypass); await commitPEQ(band, 0, current.frequency, current.q, PEQFilter.Peak, current.bypass);
} }
@@ -468,7 +477,7 @@ function InputSection({
}; };
const handleBypassClick = () => { const handleBypassClick = () => {
setPeqBypass({ Input: channel }, !ActivePeq.bypass); setPeqBypass(channel, !ActivePeq.bypass);
}; };
// Live handlers // Live handlers
@@ -489,7 +498,7 @@ function InputSection({
// Real dsp value handlers // Real dsp value handlers
const commitHighPass = async (freq: number, filterType: CrossoverFilter) => { const commitHighPass = async (freq: number, filterType: CrossoverFilter) => {
const success = await setHighPass({ Input: channel }, freq, filterType); const success = await setHighPass(channel, freq, filterType);
if (success) { if (success) {
setLiveHpFreq(freq); setLiveHpFreq(freq);
} }
@@ -497,7 +506,7 @@ function InputSection({
}; };
const commitLowPass = async (freq: number, filterType: CrossoverFilter) => { const commitLowPass = async (freq: number, filterType: CrossoverFilter) => {
const success = await setLowPass({ Input: channel }, freq, filterType); const success = await setLowPass(channel, freq, filterType);
if (success) { if (success) {
setLiveLpFreq(freq); setLiveLpFreq(freq);
} }
@@ -512,7 +521,7 @@ function InputSection({
filter_type: PEQFilter, filter_type: PEQFilter,
bypass: boolean bypass: boolean
) => { ) => {
const success = await setPeq({ Input: channel }, band, gain, frequency, q, filter_type, bypass); const success = await setPeq(channel, band, gain, frequency, q, filter_type, bypass);
if (success) { if (success) {
setLivePeq((prev) => { setLivePeq((prev) => {
const next = { ...prev }; const next = { ...prev };
@@ -536,15 +545,15 @@ function InputSection({
return ( return (
<> <>
<div className="input-graph-section"> <div className="channel-graph-section">
<div className="input-graph-panel"> <div className="channel-graph-panel">
<div className="input-graph__title-row"> <div className="channel-graph__title-row">
<div className="input-graph__title">{activeInputState.name} - Filters</div> <div className="channel-graph__title">{activeChannelState.name} - Filters</div>
<GraphViewToggle view={graphView} onChange={setGraphView} /> <GraphViewToggle view={graphView} onChange={setGraphView} />
</div> </div>
{(graphView === 'both' || graphView === 'magnitude') && ( {(graphView === 'both' || graphView === 'magnitude') && (
<div className="input-graph-panel__magnitude-wrap"> <div className="channel-graph-panel__magnitude-wrap">
<LinearGraph <LinearGraph
ref={magnitudeGraphRef} ref={magnitudeGraphRef}
xMin={GRAPH_X_MIN} xMin={GRAPH_X_MIN}
@@ -623,11 +632,17 @@ function InputSection({
}} }}
/> />
)} )}
{ActivePeq.bypass && (
<div className="channel-bypass-overlay">
<span>BYPASSED</span>
</div>
)}
</div> </div>
</div> </div>
<div className="input-control-section"> <div className="channel-control-section">
<div className="input-control-panel"> <div className="channel-control-panel">
<ChannelGroup title="PEQ"> <ChannelGroup title="PEQ">
<PeqBandList <PeqBandList
bands={ActivePeq.bands} bands={ActivePeq.bands}
@@ -635,6 +650,7 @@ function InputSection({
activeBand={activeBand} activeBand={activeBand}
setActiveBand={setActiveBand} setActiveBand={setActiveBand}
setPEQ={commitPEQ} setPEQ={commitPEQ}
bandsOrder={bandsOrder}
/> />
<PeqPanelSliders <PeqPanelSliders
peq={ActivePeq.bands[activeBand]} peq={ActivePeq.bands[activeBand]}
@@ -661,7 +677,7 @@ function InputSection({
</ChannelGroup> </ChannelGroup>
<ChannelGroup title="Actions"> <ChannelGroup title="Actions">
<InputActions <ChannelActions
bypass={ActivePeq.bypass} bypass={ActivePeq.bypass}
resetting={resetting} resetting={resetting}
onBypass={handleBypassClick} onBypass={handleBypassClick}
@@ -674,4 +690,4 @@ function InputSection({
); );
} }
export default InputSection; export default ChannelSection;
+4 -10
View File
@@ -4,28 +4,21 @@ import { NotificationType, Notification } from '../types';
export function useNotifications() { export function useNotifications() {
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
const hovered = useRef<Set<number>>(new Set()); const hovered = useRef<Set<number>>(new Set());
const nextId = useRef(0); // monotonically increasing counter
const notify = useCallback( const notify = useCallback(
(message: string, type: NotificationType = 'error', dspName?: string) => { (message: string, type: NotificationType = 'error', dspName?: string) => {
const id = Date.now(); const id = nextId.current++;
const fullMessage = dspName ? `${dspName}: ${message}` : message; const fullMessage = dspName ? `${dspName}: ${message}` : message;
setNotifications((prev) => [ setNotifications((prev) => [...prev, { id, message: fullMessage, type }]);
...prev,
{
id,
message: fullMessage,
type,
},
]);
const removeLater = () => { const removeLater = () => {
if (hovered.current.has(id)) { if (hovered.current.has(id)) {
setTimeout(removeLater, 1000); setTimeout(removeLater, 1000);
return; return;
} }
setNotifications((prev) => prev.filter((n) => n.id !== id)); setNotifications((prev) => prev.filter((n) => n.id !== id));
}; };
@@ -35,6 +28,7 @@ export function useNotifications() {
); );
const removeNotification = useCallback((id: number) => { const removeNotification = useCallback((id: number) => {
hovered.current.delete(id); // prevent leaking stale hover state
setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, removing: true } : n))); setNotifications((prev) => prev.map((n) => (n.id === id ? { ...n, removing: true } : n)));
setTimeout(() => { setTimeout(() => {