242 lines
7.2 KiB
TypeScript
242 lines
7.2 KiB
TypeScript
import VerticalSlider from '../Slider/VerticalSlider';
|
|
import { invoke } from '@tauri-apps/api/core';
|
|
import { InputChannel, Channel, OutputChannel } from '../../../types/dsp408State';
|
|
import { DSP } from '../../../types/types';
|
|
import { NotificationType } from '../../../types/types';
|
|
import Button from '../../Button/Button';
|
|
import ChannelGroup from '../ChannelGroup/ChannelGroup';
|
|
import { LinearGraph } from '../LinearGraph/LinearGraph';
|
|
import { useEffect, useRef, useState } from 'react';
|
|
import './GainSection.css';
|
|
|
|
const PANEL_DESIGN_WIDTH = 90;
|
|
const PANEL_DESIGN_HEIGHT = 400;
|
|
|
|
function ChannelGroupScaled({ title, children }: { title: string; children: React.ReactNode }) {
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
const [scale, setScale] = useState(1);
|
|
|
|
useEffect(() => {
|
|
const el = wrapperRef.current;
|
|
if (!el) return;
|
|
const observer = new ResizeObserver((entries) => {
|
|
const { height } = entries[0].contentRect;
|
|
if (height === 0) return;
|
|
setScale(height / PANEL_DESIGN_HEIGHT); // height is the only free axis here
|
|
});
|
|
observer.observe(el);
|
|
return () => observer.disconnect();
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={wrapperRef} style={{ height: '100%', '--panel-scale': scale } as React.CSSProperties}>
|
|
<ChannelGroup title={title}>{children}</ChannelGroup>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GainPanel({
|
|
dsp,
|
|
notify,
|
|
channel,
|
|
title,
|
|
}: {
|
|
dsp: DSP;
|
|
notify: (message: string, type?: NotificationType, dspName?: string) => void;
|
|
channel: Channel;
|
|
title: string;
|
|
}) {
|
|
const setGain = async (value: number) => {
|
|
try {
|
|
const success = await invoke<boolean>('set_channel_gain', {
|
|
id: dsp.id,
|
|
channel,
|
|
db: value,
|
|
});
|
|
|
|
if (success) {
|
|
notify(`Input A gain set to ${value} dB`, 'success', dsp.name);
|
|
} else {
|
|
notify('Failed to set input gain', 'error', dsp.name);
|
|
}
|
|
} catch (err) {
|
|
notify(`Gain update failed: ${String(err)}`, 'error', dsp.name);
|
|
}
|
|
};
|
|
|
|
const setMute = async (muted: boolean) => {
|
|
try {
|
|
const success = await invoke<boolean>('set_mute', {
|
|
id: dsp.id,
|
|
channel,
|
|
muted,
|
|
});
|
|
|
|
if (success) {
|
|
notify(`Input A ${muted ? 'muted' : 'unmuted'}`, 'success', dsp.name);
|
|
} else {
|
|
notify('Failed to change mute state', 'error', dsp.name);
|
|
}
|
|
} catch (err) {
|
|
notify(`Mute update failed: ${String(err)}`, 'error', dsp.name);
|
|
}
|
|
};
|
|
|
|
const setInverse = async (inverted: boolean) => {
|
|
try {
|
|
const success = await invoke<boolean>('set_channel_inverse_gain', {
|
|
id: dsp.id,
|
|
channel,
|
|
inverted,
|
|
});
|
|
|
|
if (success) {
|
|
notify(`Input A phase ${inverted ? 'inverted' : 'normal'}`, 'success', dsp.name);
|
|
} else {
|
|
notify('Failed to change phase', 'error', dsp.name);
|
|
}
|
|
} catch (err) {
|
|
notify(`Phase update failed: ${String(err)}`, 'error', dsp.name);
|
|
}
|
|
};
|
|
|
|
const [muted, setMuted] = useState(false);
|
|
const [inverted, setInverted] = useState(false);
|
|
const viewportRef = useRef<HTMLDivElement>(null);
|
|
const [scale, setScale] = useState(1);
|
|
|
|
const toggleMute = async () => {
|
|
const next = !muted;
|
|
setMuted(next);
|
|
await setMute(next);
|
|
};
|
|
|
|
const toggleInverse = async () => {
|
|
const next = !inverted;
|
|
setInverted(next);
|
|
await setInverse(next);
|
|
};
|
|
|
|
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 (
|
|
<div className="gain-panel-viewport" ref={viewportRef}>
|
|
<div
|
|
className="gain-panel"
|
|
style={{
|
|
width: PANEL_DESIGN_WIDTH,
|
|
height: PANEL_DESIGN_HEIGHT,
|
|
transform: `translate(-50%, -50%) scale(${scale})`,
|
|
}}
|
|
>
|
|
<div className="gain-panel__title">{title}</div>
|
|
|
|
<VerticalSlider
|
|
min={-60}
|
|
max={12}
|
|
step={0.5}
|
|
switchStepValue={-10}
|
|
switchStep={0.1}
|
|
unit=" dB"
|
|
onChange={setGain}
|
|
style={{ width: 70, height: 260, flex: '0 0 auto' }} // pin to design size, disable its own auto-shrink
|
|
/>
|
|
|
|
<Button variant="toggle" className={muted ? 'active-danger' : ''} onClick={toggleMute}>
|
|
{muted ? 'Muted' : 'Mute'}
|
|
</Button>
|
|
<Button
|
|
variant="toggle"
|
|
className={inverted ? 'active-warning' : ''}
|
|
onClick={toggleInverse}
|
|
>
|
|
{inverted ? 'Inverted' : 'Invert'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function GainSection({
|
|
dsp,
|
|
notify,
|
|
}: {
|
|
dsp: DSP;
|
|
notify: (message: string, type?: NotificationType, dspName?: string) => void;
|
|
}) {
|
|
const inputs: { channel: Channel; title: string }[] = [
|
|
{ channel: { Input: InputChannel.InA }, title: 'In A' },
|
|
{ channel: { Input: InputChannel.InB }, title: 'In B' },
|
|
{ channel: { Input: InputChannel.InC }, title: 'In C' },
|
|
{ channel: { Input: InputChannel.InD }, title: 'In D' },
|
|
];
|
|
|
|
const outputs: { channel: Channel; title: string }[] = [
|
|
{ channel: { Output: OutputChannel.Out1 }, title: 'Out 1' },
|
|
{ channel: { Output: OutputChannel.Out2 }, title: 'Out 2' },
|
|
{ channel: { Output: OutputChannel.Out3 }, title: 'Out 3' },
|
|
{ channel: { Output: OutputChannel.Out4 }, title: 'Out 4' },
|
|
{ channel: { Output: OutputChannel.Out5 }, title: 'Out 5' },
|
|
{ channel: { Output: OutputChannel.Out6 }, title: 'Out 6' },
|
|
{ channel: { Output: OutputChannel.Out7 }, title: 'Out 7' },
|
|
{ channel: { Output: OutputChannel.Out8 }, title: 'Out 8' },
|
|
];
|
|
|
|
return (
|
|
<>
|
|
<div className="linear-graph-section">
|
|
<LinearGraph
|
|
xMin={20}
|
|
xMax={20000}
|
|
yMin={-24}
|
|
yMax={24}
|
|
yStep={6}
|
|
xLabel="Hz"
|
|
yLabel="dB"
|
|
draw={({ ctx, toPx }) => {
|
|
// placeholder flat 0 dB response — swap for real EQ/filter data.
|
|
// NOTE: this maps x linearly; for a real frequency response you'll
|
|
// likely want a log-frequency x-axis instead (see chat notes).
|
|
ctx.strokeStyle = '#4ade80';
|
|
ctx.lineWidth = 2;
|
|
const [x0, y0] = toPx(20, 0);
|
|
const [x1, y1] = toPx(20000, 0);
|
|
ctx.beginPath();
|
|
ctx.moveTo(x0, y0);
|
|
ctx.lineTo(x1, y1);
|
|
ctx.stroke();
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div className="gain-section">
|
|
<div className="gain-panels">
|
|
<ChannelGroup title="Input">
|
|
{inputs.map(({ channel, title }) => (
|
|
<GainPanel key={title} dsp={dsp} notify={notify} channel={channel} title={title} />
|
|
))}
|
|
</ChannelGroup>
|
|
|
|
<ChannelGroup title="Output">
|
|
{outputs.map(({ channel, title }) => (
|
|
<GainPanel key={title} dsp={dsp} notify={notify} channel={channel} title={title} />
|
|
))}
|
|
</ChannelGroup>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default GainSection;
|