feat: gate sectioon
This commit is contained in:
@@ -5,3 +5,6 @@ This template should help get you started developing with Tauri, React and Types
|
|||||||
## Recommended IDE Setup
|
## Recommended IDE Setup
|
||||||
|
|
||||||
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)
|
||||||
|
|
||||||
|
# TODO
|
||||||
|
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
use tauri::State;
|
use tauri::State;
|
||||||
use crate::state::{AppState, DSPDevice};
|
use crate::state::{AppState, DSPDevice};
|
||||||
use dsp_thomann::dsp408::DSP408;
|
use dsp_thomann::dsp408::{DSP408, types};
|
||||||
use std::{net::IpAddr, str::FromStr};
|
use std::{net::IpAddr, str::FromStr};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -247,3 +247,21 @@ pub async fn set_current_preset_name(
|
|||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ pub fn run() {
|
|||||||
commands::recall_preset,
|
commands::recall_preset,
|
||||||
commands::get_meter_levels,
|
commands::get_meter_levels,
|
||||||
commands::set_current_preset_name,
|
commands::set_current_preset_name,
|
||||||
|
commands::set_gate,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
+18
-1
@@ -1,5 +1,5 @@
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { DSPState, Meters } from '../types/dsp408State';
|
import { DSPState, Meters, InputChannel } from '../types/dsp408State';
|
||||||
|
|
||||||
export const createDSP408 = (ip: string, port: number, deviceId: number) =>
|
export const createDSP408 = (ip: string, port: number, deviceId: number) =>
|
||||||
invoke<number>('create_dsp408', {
|
invoke<number>('create_dsp408', {
|
||||||
@@ -29,3 +29,20 @@ export const DSP408SetCurrentPresetName = (id: number, name: string) =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const getDSP408MeterLevels = (id: number) => invoke<Meters>('get_meter_levels', { id });
|
export const getDSP408MeterLevels = (id: number) => invoke<Meters>('get_meter_levels', { id });
|
||||||
|
|
||||||
|
export const DSP408SetGate = (
|
||||||
|
id: number,
|
||||||
|
channel: InputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
hold: number,
|
||||||
|
release: number
|
||||||
|
) =>
|
||||||
|
invoke<boolean>('set_gate', {
|
||||||
|
id,
|
||||||
|
channel,
|
||||||
|
threshold,
|
||||||
|
attack,
|
||||||
|
hold,
|
||||||
|
release,
|
||||||
|
});
|
||||||
|
|||||||
@@ -81,7 +81,12 @@ function GainPanel({
|
|||||||
style={{ width: 70, height: 250, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink
|
style={{ width: 70, height: 250, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button variant="toggle" className={muted ? 'active-danger' : ''} onClick={toggleMute} style={{ width: '70px', flex: '1 1 auto', minHeight: 0 }}>
|
<Button
|
||||||
|
variant="toggle"
|
||||||
|
className={muted ? 'active-danger' : ''}
|
||||||
|
onClick={toggleMute}
|
||||||
|
style={{ width: '70px', flex: '1 1 auto', minHeight: 0 }}
|
||||||
|
>
|
||||||
{muted ? 'Muted' : 'Mute '}
|
{muted ? 'Muted' : 'Mute '}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -1,4 +1,127 @@
|
|||||||
.rerere {
|
/* GateSection.css */
|
||||||
|
.gate-panel-viewport {
|
||||||
|
position: relative;
|
||||||
|
aspect-ratio: 160 / 400;
|
||||||
|
overflow: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-panel {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform-origin: center center;
|
||||||
|
|
||||||
|
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);
|
||||||
|
padding-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-panel__title {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
padding-top: var(--space-2);
|
||||||
|
padding-bottom: var(--space-2);
|
||||||
|
border-bottom: var(--border-width) solid var(--border-hairline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-panel__grid {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
gap: var(--space-2);
|
||||||
|
|
||||||
|
padding: var(--space-2);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-panel__cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-panel__cell-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: var(--font-xxs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Envelope graph */
|
||||||
|
|
||||||
|
.linear-graph-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--space-1);
|
||||||
|
margin-bottom: var(--space-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-graph__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
padding: 0 var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-graph__title {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-graph__channel {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container */
|
||||||
|
|
||||||
|
.gate-section {
|
||||||
|
container-type: size;
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
|
height: var(--dsp-control-heigth);
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
background: var(--bg-panel);
|
||||||
|
|
||||||
|
z-index: 20;
|
||||||
|
|
||||||
|
border-top: var(--border-width) solid var(--border-hairline);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gate-panels {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 10%;
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
overflow-x: auto;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,31 +1,237 @@
|
|||||||
|
// GateSection.tsx
|
||||||
import VerticalSlider from '../Slider/VerticalSlider';
|
import VerticalSlider from '../Slider/VerticalSlider';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { InputChannel, Channel } from '../../../types/dsp408State';
|
||||||
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
|
|
||||||
import { DSP } from '../../../types/types';
|
import { DSP } from '../../../types/types';
|
||||||
import { NotificationType } from '../../../types/types';
|
|
||||||
|
|
||||||
import ChannelGroup from '../ChannelGroup/ChannelGroup';
|
import ChannelGroup from '../ChannelGroup/ChannelGroup';
|
||||||
|
import { LinearGraph } from '../LinearGraph/LinearGraph';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import './GateSection.css';
|
import './GateSection.css';
|
||||||
|
|
||||||
function GateSection({
|
const PANEL_DESIGN_WIDTH = 160;
|
||||||
|
const PANEL_DESIGN_HEIGHT = 400;
|
||||||
|
|
||||||
|
const GATE_PARAMS = [
|
||||||
|
{ key: 'threshold', label: 'Threshold', min: -90, max: 0, step: 0.5, unit: ' dB' },
|
||||||
|
{ key: 'attack', label: 'Attack', min: 1, max: 999, step: 1, unit: ' ms' },
|
||||||
|
{ key: 'hold', label: 'Hold', min: 10, max: 999, step: 1, unit: ' ms' },
|
||||||
|
{ key: 'release', label: 'Release', min: 10, max: 3000, step: 1, unit: ' ms' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type GateParamKey = (typeof GATE_PARAMS)[number]['key'];
|
||||||
|
|
||||||
|
type GateValues = {
|
||||||
|
threshold: number;
|
||||||
|
attack: number;
|
||||||
|
hold: number;
|
||||||
|
release: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetGateFn = (
|
||||||
|
inputChannel: InputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
hold: number,
|
||||||
|
release: number
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
|
function GatePanel({
|
||||||
|
channel,
|
||||||
dsp,
|
dsp,
|
||||||
|
setGate,
|
||||||
|
onLiveValue,
|
||||||
}: {
|
}: {
|
||||||
|
channel: InputChannel;
|
||||||
dsp: DSP;
|
dsp: DSP;
|
||||||
|
setGate: SetGateFn;
|
||||||
|
onLiveValue: (channel: InputChannel, key: GateParamKey, value: number) => void;
|
||||||
}) {
|
}) {
|
||||||
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
|
const [scale, setScale] = useState(1);
|
||||||
|
|
||||||
|
const inputState = dsp.state.current_config.input_states[channel];
|
||||||
|
const gate = inputState.gate;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = viewportRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const observer = new ResizeObserver((entries) => {
|
||||||
|
const { width, height } = entries[0].contentRect;
|
||||||
|
if (width === 0 || height === 0) return;
|
||||||
|
setScale(Math.min(width / PANEL_DESIGN_WIDTH, height / PANEL_DESIGN_HEIGHT));
|
||||||
|
});
|
||||||
|
observer.observe(el);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rerere">
|
<div className="gate-panel-viewport" ref={viewportRef}>
|
||||||
|
<div
|
||||||
|
className="gate-panel"
|
||||||
|
style={{
|
||||||
|
width: PANEL_DESIGN_WIDTH,
|
||||||
|
height: PANEL_DESIGN_HEIGHT,
|
||||||
|
transform: `translate(-50%, -50%) scale(${scale})`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="gate-panel__title">{inputState.name}</div>
|
||||||
|
|
||||||
|
<div className="gate-panel__grid">
|
||||||
|
{GATE_PARAMS.map((param) => (
|
||||||
|
<div className="gate-panel__cell" key={param.key}>
|
||||||
<VerticalSlider
|
<VerticalSlider
|
||||||
min={-60}
|
min={param.min}
|
||||||
max={12}
|
max={param.max}
|
||||||
step={0.5}
|
step={param.step}
|
||||||
switchStepValue={-10}
|
unit={param.unit}
|
||||||
switchStep={0.1}
|
value={gate[param.key]}
|
||||||
unit=" dB"
|
onInput={(value) => onLiveValue(channel, param.key, value)}
|
||||||
value={0}
|
onChange={(value) => {
|
||||||
style={{ width: 70, height: 250, flex: '0 0 auto' }}
|
return setGate(
|
||||||
|
channel,
|
||||||
|
param.key === 'threshold' ? value : gate.threshold,
|
||||||
|
param.key === 'attack' ? value : gate.attack,
|
||||||
|
param.key === 'hold' ? value : gate.hold,
|
||||||
|
param.key === 'release' ? value : gate.release
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
style={{ width: '100%', flex: '1 1 auto' }}
|
||||||
|
maxAspectHeight={165}
|
||||||
/>
|
/>
|
||||||
|
<div className="gate-panel__cell-label">{param.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GateSection({ dsp, set_gate }: { dsp: DSP; set_gate: SetGateFn }) {
|
||||||
|
const inputs = [InputChannel.InA, InputChannel.InB, InputChannel.InC, InputChannel.InD];
|
||||||
|
|
||||||
|
// Tracks whichever channel's sliders were most recently touched, so the
|
||||||
|
// shared envelope graph always reflects the gate the user is currently editing.
|
||||||
|
const [activeChannel, setActiveChannel] = useState<InputChannel>(inputs[0]);
|
||||||
|
|
||||||
|
// Live, uncommitted override for whichever channel(s) are mid-drag. Sliders
|
||||||
|
// report every intermediate value here via onInput (synchronous), separate
|
||||||
|
// from the async onChange that actually round-trips to the backend. This is
|
||||||
|
// what makes the graph track the drag instead of lagging behind the commit.
|
||||||
|
const [liveGate, setLiveGate] = useState<Partial<Record<InputChannel, GateValues>>>({});
|
||||||
|
|
||||||
|
const handleLiveValue = (channel: InputChannel, key: GateParamKey, value: number) => {
|
||||||
|
setActiveChannel(channel);
|
||||||
|
setLiveGate((prev) => {
|
||||||
|
const base = prev[channel] ?? dsp.state.current_config.input_states[channel].gate;
|
||||||
|
return { ...prev, [channel]: { ...base, [key]: value } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetGate: SetGateFn = async (channel, threshold, attack, hold, release) => {
|
||||||
|
setActiveChannel(channel);
|
||||||
|
const success = await set_gate(channel, threshold, attack, hold, release);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Backend confirmed — drop the local override and let dsp.state (the
|
||||||
|
// source of truth) take over again.
|
||||||
|
setLiveGate((prev) => {
|
||||||
|
if (!(channel in prev)) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[channel];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeInputState = dsp.state.current_config.input_states[activeChannel];
|
||||||
|
const activeGate = liveGate[activeChannel] ?? activeInputState.gate;
|
||||||
|
|
||||||
|
// Layout the envelope in data space: closed -> attack -> hold (still open) ->
|
||||||
|
// release -> closed, with a little pre/post roll so the flat segments are visible.
|
||||||
|
const envelope = useMemo(() => {
|
||||||
|
const { threshold, attack, hold, release } = activeGate;
|
||||||
|
const roll = Math.max(20, (attack + hold + release) * 0.08);
|
||||||
|
const t0 = 0;
|
||||||
|
const t1 = t0 + roll; // threshold crossed, attack begins
|
||||||
|
const t2 = t1 + attack; // fully open
|
||||||
|
const t3 = t2 + hold; // hold elapses, release begins
|
||||||
|
const t4 = t3 + release; // fully closed
|
||||||
|
const t5 = t4 + roll;
|
||||||
|
return { threshold, t0, t1, t2, t3, t4, t5 };
|
||||||
|
}, [activeGate.threshold, activeGate.attack, activeGate.hold, activeGate.release]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="linear-graph-section">
|
||||||
|
<div className="gate-graph__header">
|
||||||
|
<span className="gate-graph__title">Gate envelope</span>
|
||||||
|
<span className="gate-graph__channel">{activeInputState.name}</span>
|
||||||
|
</div>
|
||||||
|
<LinearGraph
|
||||||
|
xMin={envelope.t0}
|
||||||
|
xMax={envelope.t5}
|
||||||
|
yMin={-90}
|
||||||
|
yMax={0}
|
||||||
|
xLabel="ms"
|
||||||
|
yLabel="dB"
|
||||||
|
formatX={(v) => Math.round(v).toString()}
|
||||||
|
formatY={(v) => Math.round(v).toString()}
|
||||||
|
draw={({ toPx, ctx }) => {
|
||||||
|
const { threshold, t0, t1, t2, t3, t4, t5 } = envelope;
|
||||||
|
|
||||||
|
// Threshold reference line
|
||||||
|
ctx.strokeStyle = '#fbbf24';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
const [tx0, ty] = toPx(t0, threshold);
|
||||||
|
const [tx1] = toPx(t5, threshold);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tx0, ty);
|
||||||
|
ctx.lineTo(tx1, ty);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// Gain envelope: closed -> attack -> hold -> release -> closed
|
||||||
|
ctx.strokeStyle = '#4ade80';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
const points: [number, number][] = [
|
||||||
|
[t0, -90],
|
||||||
|
[t1, -90],
|
||||||
|
[t2, 0],
|
||||||
|
[t3, 0],
|
||||||
|
[t4, -90],
|
||||||
|
[t5, -90],
|
||||||
|
];
|
||||||
|
ctx.beginPath();
|
||||||
|
points.forEach(([t, g], i) => {
|
||||||
|
const [px, py] = toPx(t, g);
|
||||||
|
if (i === 0) ctx.moveTo(px, py);
|
||||||
|
else ctx.lineTo(px, py);
|
||||||
|
});
|
||||||
|
ctx.stroke();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gate-section">
|
||||||
|
<div className="gate-panels">
|
||||||
|
<ChannelGroup title="Input">
|
||||||
|
{inputs.map((input) => (
|
||||||
|
<GatePanel
|
||||||
|
key={String(input)}
|
||||||
|
channel={input}
|
||||||
|
dsp={dsp}
|
||||||
|
setGate={handleSetGate}
|
||||||
|
onLiveValue={handleLiveValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ChannelGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
export default GateSection;
|
export default GateSection;
|
||||||
|
|||||||
@@ -196,11 +196,27 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
|
|||||||
}
|
}
|
||||||
|
|
||||||
// clip to plot area so user drawing can't bleed into labels
|
// clip to plot area so user drawing can't bleed into labels
|
||||||
|
const clipPad = 2;
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.rect(plot.x, plot.y, plot.width, plot.height);
|
ctx.rect(
|
||||||
|
plot.x - clipPad,
|
||||||
|
plot.y - clipPad,
|
||||||
|
plot.width + clipPad * 2,
|
||||||
|
plot.height + clipPad * 2
|
||||||
|
);
|
||||||
ctx.clip();
|
ctx.clip();
|
||||||
|
|
||||||
|
// draw the frame border first, so data drawn on the edge sits on top of it
|
||||||
|
ctx.strokeStyle = axisColor;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.strokeRect(
|
||||||
|
Math.round(plot.x) + 0.5,
|
||||||
|
Math.round(plot.y) + 0.5,
|
||||||
|
Math.round(plot.width) - 1,
|
||||||
|
Math.round(plot.height) - 1
|
||||||
|
);
|
||||||
|
|
||||||
if (showGrid) {
|
if (showGrid) {
|
||||||
ctx.strokeStyle = gridColor;
|
ctx.strokeStyle = gridColor;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1;
|
||||||
|
|||||||
@@ -10,17 +10,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.gauge-container {
|
.gauge-container {
|
||||||
--s: min(calc(100cqw / 70), calc(100cqh / 240));
|
--s: min(calc(100cqw / 70), calc(100cqh / var(--min-height-units, 180)));
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
||||||
width: calc(70 * var(--s));
|
width: calc(70 * var(--s));
|
||||||
height: calc(240 * var(--s));
|
height: 100%;
|
||||||
|
max-height: calc(var(--max-height-units, 240) * var(--s));
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
@@ -32,7 +34,9 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
width: calc(70 * var(--s));
|
width: calc(70 * var(--s));
|
||||||
height: calc(210 * var(--s));
|
flex: 1 1 auto;
|
||||||
|
max-height: calc(210 * var(--s));
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@@ -63,7 +67,7 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
background: var(--border-hairline);
|
background: var(--border-hairline);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: calc(6 * var(--s));
|
||||||
}
|
}
|
||||||
|
|
||||||
.thumb {
|
.thumb {
|
||||||
@@ -77,8 +81,8 @@
|
|||||||
|
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--bg-panel);
|
background: var(--bg-panel);
|
||||||
border: var(--border-width-active) solid var(--accent-brand);
|
border: calc(2 * var(--s)) solid var(--accent-brand);
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: 0 calc(2 * var(--s)) calc(6 * var(--s)) rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.ticks {
|
.ticks {
|
||||||
@@ -98,11 +102,15 @@
|
|||||||
height: calc(2 * var(--s));
|
height: calc(2 * var(--s));
|
||||||
|
|
||||||
background: var(--border-hairline);
|
background: var(--border-hairline);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: calc(6 * var(--s));
|
||||||
}
|
}
|
||||||
|
|
||||||
.left { left: calc(8 * var(--s)); }
|
.left {
|
||||||
.right { right: calc(8 * var(--s)); }
|
left: calc(8 * var(--s));
|
||||||
|
}
|
||||||
|
.right {
|
||||||
|
right: calc(8 * var(--s));
|
||||||
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -120,12 +128,12 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
color: var(--accent-brand);
|
color: var(--accent-brand);
|
||||||
font-size: var(--font-xxs);
|
font-size: calc(12 * var(--s));
|
||||||
font-family: var(--font-mono);
|
font-family: var(--font-mono);
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
|
|
||||||
box-shadow: var(--shadow-sm);
|
box-shadow: 0 calc(2 * var(--s)) calc(6 * var(--s)) rgba(0, 0, 0, 0.2);
|
||||||
border-radius: var(--radius-md);
|
border-radius: calc(8 * var(--s));
|
||||||
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
@@ -138,8 +146,8 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
|
|
||||||
border: var(--border-width) solid var(--border-hairline);
|
border: calc(1 * var(--s)) solid var(--border-hairline);
|
||||||
border-radius: var(--radius-sm);
|
border-radius: calc(6 * var(--s));
|
||||||
|
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
import './VerticalSlider.css';
|
import './VerticalSlider.css';
|
||||||
|
|
||||||
@@ -12,13 +12,33 @@ type VerticalSliderProps = {
|
|||||||
|
|
||||||
value: number;
|
value: number;
|
||||||
onChange?: (value: number) => Promise<boolean>;
|
onChange?: (value: number) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* Fires synchronously on every live value change — while dragging, on each
|
||||||
|
* keypress — *before* the value is committed via onChange. Use this to
|
||||||
|
* drive UI that should track the slider in real time (e.g. a preview
|
||||||
|
* graph), without waiting for the (async) commit round-trip.
|
||||||
|
*/
|
||||||
|
onInput?: (value: number) => void;
|
||||||
|
|
||||||
unit?: string;
|
unit?: string;
|
||||||
|
|
||||||
|
minAspectHeight?: number;
|
||||||
|
maxAspectHeight?: number;
|
||||||
|
|
||||||
className?: string;
|
className?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Design constants: at the original 70x240 ratio the track is 210 units tall
|
||||||
|
// and holds 15 ticks (2 units tall) laid out with space-between, which works
|
||||||
|
// out to a ~13 unit gap -> a ~15 unit center-to-center pitch. We reuse that
|
||||||
|
// pitch to decide how many ticks fit in whatever track height we actually get.
|
||||||
|
const DESIGN_TRACK_WIDTH_UNITS = 70;
|
||||||
|
const TICK_HEIGHT_UNITS = 2;
|
||||||
|
const TICK_PITCH_UNITS = 15;
|
||||||
|
const MIN_TICKS = 3;
|
||||||
|
const MAX_TICKS = 15;
|
||||||
|
|
||||||
function getDecimals(n: number) {
|
function getDecimals(n: number) {
|
||||||
const s = n.toString();
|
const s = n.toString();
|
||||||
const i = s.indexOf('.');
|
const i = s.indexOf('.');
|
||||||
@@ -33,19 +53,59 @@ export default function VerticalSlider({
|
|||||||
switchStep,
|
switchStep,
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
onInput,
|
||||||
unit = '',
|
unit = '',
|
||||||
|
minAspectHeight = 180,
|
||||||
|
maxAspectHeight = 240,
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
}: VerticalSliderProps) {
|
}: VerticalSliderProps) {
|
||||||
const [editingText, setEditingText] = useState<string | null>(null);
|
const [editingText, setEditingText] = useState<string | null>(null);
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const sliderRef = useRef<HTMLDivElement>(null);
|
const sliderRef = useRef<HTMLDivElement>(null);
|
||||||
|
const trackRef = useRef<HTMLDivElement>(null);
|
||||||
const keyboardValue = useRef<number | null>(null);
|
const keyboardValue = useRef<number | null>(null);
|
||||||
const [displayValue, setDisplayValue] = useState(value);
|
const [displayValue, setDisplayValue] = useState(value);
|
||||||
const dragValue = useRef(value);
|
const dragValue = useRef(value);
|
||||||
const dragging = useRef(false);
|
const dragging = useRef(false);
|
||||||
const thumbRef = useRef<HTMLDivElement>(null);
|
const thumbRef = useRef<HTMLDivElement>(null);
|
||||||
const keyboardEditing = useRef(false);
|
const keyboardEditing = useRef(false);
|
||||||
|
const [tickCount, setTickCount] = useState(MAX_TICKS);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dragging.current || keyboardEditing.current || editingText !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setDisplayValue(value);
|
||||||
|
dragValue.current = value;
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = trackRef.current;
|
||||||
|
if (!el || typeof ResizeObserver === 'undefined') return;
|
||||||
|
|
||||||
|
const recompute = () => {
|
||||||
|
const w = el.clientWidth;
|
||||||
|
const h = el.clientHeight;
|
||||||
|
if (!w || !h) return;
|
||||||
|
|
||||||
|
// .gauge-track's width is always calc(70 * --s), so this recovers --s in px.
|
||||||
|
const s = w / DESIGN_TRACK_WIDTH_UNITS;
|
||||||
|
const heightUnits = h / s;
|
||||||
|
|
||||||
|
const raw = 1 + (heightUnits - TICK_HEIGHT_UNITS) / TICK_PITCH_UNITS;
|
||||||
|
const n = Math.max(MIN_TICKS, Math.min(MAX_TICKS, Math.round(raw)));
|
||||||
|
|
||||||
|
setTickCount(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
recompute();
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(recompute);
|
||||||
|
observer.observe(el);
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
function getStep(value: number) {
|
function getStep(value: number) {
|
||||||
if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) {
|
if (switchStepValue !== undefined && switchStep !== undefined && value >= switchStepValue) {
|
||||||
@@ -75,6 +135,7 @@ export default function VerticalSlider({
|
|||||||
|
|
||||||
dragValue.current = next;
|
dragValue.current = next;
|
||||||
setDisplayValue(next);
|
setDisplayValue(next);
|
||||||
|
onInput?.(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
||||||
@@ -120,6 +181,7 @@ export default function VerticalSlider({
|
|||||||
if (success === false) {
|
if (success === false) {
|
||||||
setDisplayValue(value);
|
setDisplayValue(value);
|
||||||
dragValue.current = value;
|
dragValue.current = value;
|
||||||
|
onInput?.(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +217,7 @@ export default function VerticalSlider({
|
|||||||
if (success === false) {
|
if (success === false) {
|
||||||
setDisplayValue(value);
|
setDisplayValue(value);
|
||||||
dragValue.current = value;
|
dragValue.current = value;
|
||||||
|
onInput?.(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
||||||
@@ -170,12 +233,14 @@ export default function VerticalSlider({
|
|||||||
const next = normalizeValue(parsed);
|
const next = normalizeValue(parsed);
|
||||||
|
|
||||||
setDisplayValue(next);
|
setDisplayValue(next);
|
||||||
|
onInput?.(next);
|
||||||
|
|
||||||
const success = await onChange?.(next);
|
const success = await onChange?.(next);
|
||||||
|
|
||||||
if (success === false) {
|
if (success === false) {
|
||||||
setDisplayValue(value);
|
setDisplayValue(value);
|
||||||
dragValue.current = value;
|
dragValue.current = value;
|
||||||
|
onInput?.(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,17 +250,17 @@ export default function VerticalSlider({
|
|||||||
|
|
||||||
const percent = (displayValue - min) / (max - min);
|
const percent = (displayValue - min) / (max - min);
|
||||||
|
|
||||||
|
const containerStyle = {
|
||||||
|
'--min-height-units': minAspectHeight,
|
||||||
|
'--max-height-units': maxAspectHeight,
|
||||||
|
} as React.CSSProperties;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={['gauge-viewport', className].filter(Boolean).join(' ')} style={style}>
|
||||||
className={['gauge-viewport', className].filter(Boolean).join(' ')}
|
<div className="gauge-container" style={containerStyle}>
|
||||||
style={style}
|
<div className="gauge-track" ref={trackRef}>
|
||||||
>
|
|
||||||
<div
|
|
||||||
className="gauge-container"
|
|
||||||
>
|
|
||||||
<div className="gauge-track">
|
|
||||||
<div className="ticks left">
|
<div className="ticks left">
|
||||||
{Array.from({ length: 15 }).map((_, i) => (
|
{Array.from({ length: tickCount }).map((_, i) => (
|
||||||
<span key={i} />
|
<span key={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -216,7 +281,7 @@ export default function VerticalSlider({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ticks right">
|
<div className="ticks right">
|
||||||
{Array.from({ length: 15 }).map((_, i) => (
|
{Array.from({ length: tickCount }).map((_, i) => (
|
||||||
<span key={i} />
|
<span key={i} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -139,7 +139,7 @@
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
font-family: var(--font-ui);
|
font-family: var(--font-ui);
|
||||||
font-size: var(--font-md);
|
font-size: var(--font-xs);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scroll-snap-align: start;
|
scroll-snap-align: start;
|
||||||
|
|||||||
@@ -45,11 +45,7 @@ function Tab({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={[
|
className={['tab', active && 'tab--active', dimmed && 'tab--dimmed']
|
||||||
'tab',
|
|
||||||
active && 'tab--active',
|
|
||||||
dimmed && 'tab--dimmed',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ')}
|
.join(' ')}
|
||||||
data-step={step}
|
data-step={step}
|
||||||
@@ -62,11 +58,7 @@ function Tab({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="tab-led"
|
className="tab-led"
|
||||||
aria-label={
|
aria-label={isConnected ? `Disconnect ${dsp.name}` : `Connect ${dsp.name}`}
|
||||||
isConnected
|
|
||||||
? `Disconnect ${dsp.name}`
|
|
||||||
: `Connect ${dsp.name}`
|
|
||||||
}
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
|
|||||||
+70
-2
@@ -4,7 +4,7 @@ import { useEffect, useRef } from 'react';
|
|||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
|
|
||||||
import { DSP, AppPage } from '../types/types';
|
import { DSP, AppPage } from '../types/types';
|
||||||
import { Channel } from '../types/dsp408State';
|
import { Channel, InputChannel, defaultDSPState } from '../types/dsp408State';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createDSP408,
|
createDSP408,
|
||||||
@@ -37,6 +37,13 @@ export type DSP408Device = {
|
|||||||
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
setGain: (channel: Channel, gain: number) => Promise<boolean>;
|
||||||
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
setMute: (channel: Channel, muted: boolean) => Promise<boolean>;
|
||||||
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
setInverse: (channel: Channel, inverted: boolean) => Promise<boolean>;
|
||||||
|
setGate(
|
||||||
|
inputChannel: InputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
hold: number,
|
||||||
|
release: number
|
||||||
|
): Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -245,7 +252,7 @@ export function useDSP408(
|
|||||||
id,
|
id,
|
||||||
...dsp,
|
...dsp,
|
||||||
status: 'disconnected',
|
status: 'disconnected',
|
||||||
state: null,
|
state: defaultDSPState,
|
||||||
meters: null,
|
meters: null,
|
||||||
pollingPaused: true,
|
pollingPaused: true,
|
||||||
};
|
};
|
||||||
@@ -559,6 +566,60 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// Gate
|
||||||
|
// ==========================================================
|
||||||
|
async function setGate(
|
||||||
|
dsp: DSP,
|
||||||
|
inputChannel: InputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
hold: number,
|
||||||
|
release: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
return withPollingPaused(dsp, async () => {
|
||||||
|
try {
|
||||||
|
const success = await invoke<boolean>('set_gate', {
|
||||||
|
id: dsp.id,
|
||||||
|
channel: inputChannel,
|
||||||
|
threshold,
|
||||||
|
attack,
|
||||||
|
hold,
|
||||||
|
release,
|
||||||
|
});
|
||||||
|
if (!success) {
|
||||||
|
notify('Failed to change gate settings', 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
updateDSP(setDsps, dsp.id, (d) => {
|
||||||
|
if (!d.state) return d;
|
||||||
|
const currentConfig = d.state.current_config;
|
||||||
|
return {
|
||||||
|
...d,
|
||||||
|
state: {
|
||||||
|
...d.state,
|
||||||
|
current_config: {
|
||||||
|
...currentConfig,
|
||||||
|
input_states: {
|
||||||
|
...currentConfig.input_states,
|
||||||
|
[inputChannel]: {
|
||||||
|
...currentConfig.input_states[inputChannel],
|
||||||
|
gate: { threshold, attack, hold, release },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
notify('Gate updated', 'success', dsp.name);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
notify(`Gate update failed: ${String(err)}`, 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// Return API
|
// Return API
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
@@ -572,6 +633,13 @@ export function useDSP408(
|
|||||||
setGain: (channel: Channel, gain: number) => setGain(dsp, channel, gain),
|
setGain: (channel: Channel, gain: number) => setGain(dsp, channel, gain),
|
||||||
setMute: (channel: Channel, muted: boolean) => setMute(dsp, channel, muted),
|
setMute: (channel: Channel, muted: boolean) => setMute(dsp, channel, muted),
|
||||||
setInverse: (channel: Channel, inverted: boolean) => setInverse(dsp, channel, inverted),
|
setInverse: (channel: Channel, inverted: boolean) => setInverse(dsp, channel, inverted),
|
||||||
|
setGate: (
|
||||||
|
inputChannel: InputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
hold: number,
|
||||||
|
release: number
|
||||||
|
) => setGate(dsp, inputChannel, threshold, attack, hold, release),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
case 'gate':
|
case 'gate':
|
||||||
return <GateSection dsp={dsp} />;
|
return <GateSection dsp={dsp} set_gate={dsp408.setGate} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -195,3 +195,130 @@ export type DSPState = {
|
|||||||
export interface Meters {
|
export interface Meters {
|
||||||
levels: number[];
|
levels: number[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* -- Default Value -- */
|
||||||
|
|
||||||
|
const createDefaultGate = (): Gate => ({
|
||||||
|
attack: 10,
|
||||||
|
release: 10,
|
||||||
|
hold: 10,
|
||||||
|
threshold: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultGraphicEQ = (): GraphicEQ => ({
|
||||||
|
gains: Array(31).fill(0),
|
||||||
|
bypass: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultPEQ = (): PEQ => ({
|
||||||
|
gain: 0,
|
||||||
|
frequency: 1000,
|
||||||
|
q: 1,
|
||||||
|
filter_type: PEQFilter.Peak,
|
||||||
|
bypass: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultPEQChain = (): PEQChain => ({
|
||||||
|
bands: Array.from({ length: 8 }, createDefaultPEQ),
|
||||||
|
bypass: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultCrossover = (): Crossover => ({
|
||||||
|
frequency: 20000,
|
||||||
|
slope: CrossoverFilter.Bypass,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultCrossoverFilters = (): CrossoverFilters => ({
|
||||||
|
high_pass: createDefaultCrossover(),
|
||||||
|
low_pass: createDefaultCrossover(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultInputChannelState = (name: string): InputChannelState => ({
|
||||||
|
name,
|
||||||
|
gain: 0,
|
||||||
|
phase_inverted: false,
|
||||||
|
delay: 0,
|
||||||
|
|
||||||
|
gate: createDefaultGate(),
|
||||||
|
geq: createDefaultGraphicEQ(),
|
||||||
|
peq_chain: createDefaultPEQChain(),
|
||||||
|
crossover: createDefaultCrossoverFilters(),
|
||||||
|
|
||||||
|
linked_channels: [],
|
||||||
|
|
||||||
|
mute: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDefaultOutputChannelState = (name: string): OutputChannelState => ({
|
||||||
|
name,
|
||||||
|
|
||||||
|
matrix_routes: {
|
||||||
|
connected: [],
|
||||||
|
gains: [],
|
||||||
|
},
|
||||||
|
|
||||||
|
crossover: createDefaultCrossoverFilters(),
|
||||||
|
peq_chain: createDefaultPEQChain(),
|
||||||
|
|
||||||
|
compressor: {
|
||||||
|
threshold: 0,
|
||||||
|
ratio: Ratio.Ratio1_1,
|
||||||
|
attack: 0,
|
||||||
|
release: 0,
|
||||||
|
knee: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
limiter: {
|
||||||
|
threshold: 0,
|
||||||
|
attack: 0,
|
||||||
|
release: 0,
|
||||||
|
},
|
||||||
|
|
||||||
|
gain: 0,
|
||||||
|
phase_inverted: false,
|
||||||
|
delay: 0,
|
||||||
|
|
||||||
|
linked_channels: [],
|
||||||
|
|
||||||
|
mute: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const defaultDSPState: DSPState = {
|
||||||
|
name: 'Default',
|
||||||
|
flags: {
|
||||||
|
is_locked: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
presets: {
|
||||||
|
current_index: 0,
|
||||||
|
names: ['Default'],
|
||||||
|
modified: [false],
|
||||||
|
},
|
||||||
|
|
||||||
|
current_config: {
|
||||||
|
input_source: {
|
||||||
|
type: 'Analog',
|
||||||
|
frequency: '48kHz',
|
||||||
|
},
|
||||||
|
|
||||||
|
delay_unit: DelayUnit.Millisecond,
|
||||||
|
|
||||||
|
input_states: {
|
||||||
|
[InputChannel.InA]: createDefaultInputChannelState('In A'),
|
||||||
|
[InputChannel.InB]: createDefaultInputChannelState('In B'),
|
||||||
|
[InputChannel.InC]: createDefaultInputChannelState('In C'),
|
||||||
|
[InputChannel.InD]: createDefaultInputChannelState('In D'),
|
||||||
|
},
|
||||||
|
|
||||||
|
output_states: {
|
||||||
|
[OutputChannel.Out1]: createDefaultOutputChannelState('Out 1'),
|
||||||
|
[OutputChannel.Out2]: createDefaultOutputChannelState('Out 2'),
|
||||||
|
[OutputChannel.Out3]: createDefaultOutputChannelState('Out 3'),
|
||||||
|
[OutputChannel.Out4]: createDefaultOutputChannelState('Out 4'),
|
||||||
|
[OutputChannel.Out5]: createDefaultOutputChannelState('Out 5'),
|
||||||
|
[OutputChannel.Out6]: createDefaultOutputChannelState('Out 6'),
|
||||||
|
[OutputChannel.Out7]: createDefaultOutputChannelState('Out 7'),
|
||||||
|
[OutputChannel.Out8]: createDefaultOutputChannelState('Out 8'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ export type DSP = {
|
|||||||
port: number;
|
port: number;
|
||||||
deviceId: number;
|
deviceId: number;
|
||||||
status: DSPStatus;
|
status: DSPStatus;
|
||||||
state: DSPState | null;
|
state: DSPState;
|
||||||
meters: Meters | null;
|
meters: Meters | null;
|
||||||
pollingPaused: boolean;
|
pollingPaused: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user