feat: add limiter
This commit is contained in:
@@ -290,4 +290,21 @@ pub async fn set_compressor(
|
|||||||
)
|
)
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ pub fn run() {
|
|||||||
commands::set_current_preset_name,
|
commands::set_current_preset_name,
|
||||||
commands::set_gate,
|
commands::set_gate,
|
||||||
commands::set_compressor,
|
commands::set_compressor,
|
||||||
|
commands::set_limiter,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
.limiter-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-panels {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-2);
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-panel {
|
||||||
|
position: relative;
|
||||||
|
height: 10;
|
||||||
|
|
||||||
|
aspect-ratio: 0.5;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-panel__grid {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
grid-template-rows: repeat(2, 1fr);
|
||||||
|
grid-template-areas:
|
||||||
|
'threshold attack'
|
||||||
|
'threshold release';
|
||||||
|
gap: 2cqh;
|
||||||
|
|
||||||
|
padding: 2cqh;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-panel__cell--threshold {
|
||||||
|
grid-area: threshold;
|
||||||
|
}
|
||||||
|
.limiter-panel__cell--attack {
|
||||||
|
grid-area: attack;
|
||||||
|
}
|
||||||
|
.limiter-panel__cell--release {
|
||||||
|
grid-area: release;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-panel__cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
min-height: 0;
|
||||||
|
min-width: 0;
|
||||||
|
gap: 1cqh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-panel__cell-label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
font-family: var(--font-ui);
|
||||||
|
font-size: 2.5cqh;
|
||||||
|
color: var(--text-muted);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-linear-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-meter-panel {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
width: fit-content;
|
||||||
|
min-width: max-content;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
border: var(--border-width) solid var(--border-hairline);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-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);
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-graph__meters {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: stretch;
|
||||||
|
|
||||||
|
gap: 1cqh;
|
||||||
|
padding: 1cqh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.limiter-graph-panel > .container {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,238 @@
|
|||||||
|
// LimiterSection.tsx
|
||||||
|
import VerticalSlider from '../Slider/VerticalSlider';
|
||||||
|
import { OutputChannel } from '../../../types/dsp408State';
|
||||||
|
import { DSP } from '../../../types/types';
|
||||||
|
import ChannelGroup from '../ChannelGroup/ChannelGroup';
|
||||||
|
import { LinearGraph } from '../LinearGraph/LinearGraph';
|
||||||
|
import MeterBar from '../../MeterBar/MeterBar';
|
||||||
|
import { useMemo, useState } from 'react';
|
||||||
|
import './LimiterSection.css';
|
||||||
|
|
||||||
|
const LIMITER_SLIDER_PARAMS = [
|
||||||
|
{ key: 'threshold', label: 'Threshold', min: -90, max: 20, step: 0.5, unit: ' dB' },
|
||||||
|
{ key: 'attack', label: 'Attack', min: 1, max: 999, step: 1, unit: ' ms' },
|
||||||
|
{ key: 'release', label: 'Release', min: 10, max: 3000, step: 1, unit: ' ms' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
// ASSUMPTION: output meter indices follow the 4 input channels (0-3), i.e.
|
||||||
|
// Out1 -> 4, Out2 -> 5, etc. Same mapping as CompressorSection — adjust if
|
||||||
|
// dsp.meters.channels ordering differs.
|
||||||
|
const OUTPUT_METER_INDEX: Record<OutputChannel, number> = {
|
||||||
|
[OutputChannel.Out1]: 4,
|
||||||
|
[OutputChannel.Out2]: 5,
|
||||||
|
[OutputChannel.Out3]: 6,
|
||||||
|
[OutputChannel.Out4]: 7,
|
||||||
|
[OutputChannel.Out5]: 8,
|
||||||
|
[OutputChannel.Out6]: 9,
|
||||||
|
[OutputChannel.Out7]: 10,
|
||||||
|
[OutputChannel.Out8]: 11,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type LimiterParamKey = 'threshold' | 'attack' | 'release';
|
||||||
|
|
||||||
|
type LimiterValues = {
|
||||||
|
threshold: number;
|
||||||
|
attack: number;
|
||||||
|
release: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SetLimiterFn = (
|
||||||
|
outputChannel: OutputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
release: number
|
||||||
|
) => Promise<boolean>;
|
||||||
|
|
||||||
|
function LimiterPanel({
|
||||||
|
channel,
|
||||||
|
dsp,
|
||||||
|
setLimiter,
|
||||||
|
onLiveValue,
|
||||||
|
}: {
|
||||||
|
channel: OutputChannel;
|
||||||
|
dsp: DSP;
|
||||||
|
setLimiter: SetLimiterFn;
|
||||||
|
onLiveValue: (channel: OutputChannel, key: LimiterParamKey, value: number) => void;
|
||||||
|
}) {
|
||||||
|
const outputState = dsp.state.current_config.output_states[channel];
|
||||||
|
// ASSUMPTION: limiter settings live at output_states[channel].limiter
|
||||||
|
const lim = outputState.limiter as LimiterValues;
|
||||||
|
|
||||||
|
const commit = (overrides: Partial<LimiterValues>) => {
|
||||||
|
const next = { ...lim, ...overrides };
|
||||||
|
return setLimiter(channel, next.threshold, next.attack, next.release);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="limiter-panel">
|
||||||
|
<div className="limiter-panel__title">{outputState.name}</div>
|
||||||
|
|
||||||
|
<div className="limiter-panel__grid">
|
||||||
|
{LIMITER_SLIDER_PARAMS.map((param) => (
|
||||||
|
<div className={`limiter-panel__cell limiter-panel__cell--${param.key}`} key={param.key}>
|
||||||
|
<VerticalSlider
|
||||||
|
min={param.min}
|
||||||
|
max={param.max}
|
||||||
|
step={param.step}
|
||||||
|
unit={param.unit}
|
||||||
|
value={lim[param.key]}
|
||||||
|
onInput={(value) => onLiveValue(channel, param.key, value)}
|
||||||
|
onChange={(value) => commit({ [param.key]: value } as Partial<LimiterValues>)}
|
||||||
|
style={{ width: '100%', flex: '1 1 auto' }}
|
||||||
|
/>
|
||||||
|
<div className="limiter-panel__cell-label">{param.label}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard-knee brickwall limiter transfer curve, in dB.
|
||||||
|
function limiterOutputDb(input: number, threshold: number): number {
|
||||||
|
return input <= threshold ? input : threshold;
|
||||||
|
}
|
||||||
|
|
||||||
|
function LimiterSection({ dsp, set_limiter }: { dsp: DSP; set_limiter: SetLimiterFn }) {
|
||||||
|
const outputs = [
|
||||||
|
OutputChannel.Out1,
|
||||||
|
OutputChannel.Out2,
|
||||||
|
OutputChannel.Out3,
|
||||||
|
OutputChannel.Out4,
|
||||||
|
OutputChannel.Out5,
|
||||||
|
OutputChannel.Out6,
|
||||||
|
OutputChannel.Out7,
|
||||||
|
OutputChannel.Out8,
|
||||||
|
];
|
||||||
|
|
||||||
|
const [activeChannel, setActiveChannel] = useState<OutputChannel>(outputs[0]);
|
||||||
|
const [liveLim, setLiveLim] = useState<Partial<Record<OutputChannel, LimiterValues>>>({});
|
||||||
|
|
||||||
|
const handleLiveValue = (channel: OutputChannel, key: LimiterParamKey, value: number) => {
|
||||||
|
setActiveChannel(channel);
|
||||||
|
setLiveLim((prev) => {
|
||||||
|
const base =
|
||||||
|
prev[channel] ?? (dsp.state.current_config.output_states[channel].limiter as LimiterValues);
|
||||||
|
return { ...prev, [channel]: { ...base, [key]: value } };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetLimiter: SetLimiterFn = async (channel, threshold, attack, release) => {
|
||||||
|
setActiveChannel(channel);
|
||||||
|
const success = await set_limiter(channel, threshold, attack, release);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
setLiveLim((prev) => {
|
||||||
|
if (!(channel in prev)) return prev;
|
||||||
|
const next = { ...prev };
|
||||||
|
delete next[channel];
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activeOutputState = dsp.state.current_config.output_states[activeChannel];
|
||||||
|
const activeLim = liveLim[activeChannel] ?? (activeOutputState.limiter as LimiterValues);
|
||||||
|
|
||||||
|
const curvePoints = useMemo(() => {
|
||||||
|
const { threshold } = activeLim;
|
||||||
|
const steps = 120;
|
||||||
|
const points: [number, number][] = [];
|
||||||
|
for (let i = 0; i <= steps; i++) {
|
||||||
|
const x = -60 + (60 * i) / steps;
|
||||||
|
points.push([x, limiterOutputDb(x, threshold)]);
|
||||||
|
}
|
||||||
|
return points;
|
||||||
|
}, [activeLim.threshold]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="limiter-linear-graph-section">
|
||||||
|
<div className="limiter-graph-panel">
|
||||||
|
<div className="limiter-graph__title">{activeOutputState.name} - Limiter</div>
|
||||||
|
|
||||||
|
<LinearGraph
|
||||||
|
xMin={-60}
|
||||||
|
xMax={0}
|
||||||
|
yMin={-60}
|
||||||
|
yMax={0}
|
||||||
|
xLabel="in dB"
|
||||||
|
yLabel="out dB"
|
||||||
|
formatX={(v) => Math.round(v).toString()}
|
||||||
|
formatY={(v) => Math.round(v).toString()}
|
||||||
|
draw={({ ctx, toPx }) => {
|
||||||
|
// Unity reference line
|
||||||
|
ctx.strokeStyle = '#6b7280';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([4, 4]);
|
||||||
|
const [ux0, uy0] = toPx(-60, -60);
|
||||||
|
const [ux1, uy1] = toPx(0, 0);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(ux0, uy0);
|
||||||
|
ctx.lineTo(ux1, uy1);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// Threshold marker
|
||||||
|
ctx.strokeStyle = '#fbbf24';
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.setLineDash([2, 3]);
|
||||||
|
const [tx0, ty0] = toPx(activeLim.threshold, -60);
|
||||||
|
const [tx1, ty1] = toPx(activeLim.threshold, 0);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(tx0, ty0);
|
||||||
|
ctx.lineTo(tx1, ty1);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.setLineDash([]);
|
||||||
|
|
||||||
|
// Limiter curve
|
||||||
|
ctx.strokeStyle = '#f87171';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.lineJoin = 'round';
|
||||||
|
ctx.beginPath();
|
||||||
|
curvePoints.forEach(([x, y], i) => {
|
||||||
|
const [px, py] = toPx(x, y);
|
||||||
|
if (i === 0) ctx.moveTo(px, py);
|
||||||
|
else ctx.lineTo(px, py);
|
||||||
|
});
|
||||||
|
ctx.stroke();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="limiter-meter-panel">
|
||||||
|
<div className="limiter-graph__meters">
|
||||||
|
{outputs.map((output) => {
|
||||||
|
const outputState = dsp.state.current_config.output_states[output];
|
||||||
|
const idx = OUTPUT_METER_INDEX[output];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MeterBar key={output} label={outputState.name} meter={dsp.meters.channels[idx]} />
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="limiter-section">
|
||||||
|
<div className="limiter-panels">
|
||||||
|
<ChannelGroup title="Output">
|
||||||
|
{outputs.map((output) => (
|
||||||
|
<LimiterPanel
|
||||||
|
key={String(output)}
|
||||||
|
channel={output}
|
||||||
|
dsp={dsp}
|
||||||
|
setLimiter={handleSetLimiter}
|
||||||
|
onLiveValue={handleLiveValue}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ChannelGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LimiterSection;
|
||||||
@@ -170,9 +170,9 @@ export const LinearGraph = forwardRef<LinearGraphHandle, LinearGraphProps>(funct
|
|||||||
ctx.clearRect(0, 0, cssW, cssH);
|
ctx.clearRect(0, 0, cssW, cssH);
|
||||||
|
|
||||||
const plot = {
|
const plot = {
|
||||||
x: pad.left,
|
x: pad.left + cq.w * 2,
|
||||||
y: pad.top,
|
y: pad.top,
|
||||||
width: Math.max(1, cssW - pad.left - pad.right),
|
width: Math.max(1, cssW - pad.left - pad.right - cq.w * 2),
|
||||||
height: Math.max(1, cssH - pad.top - pad.bottom),
|
height: Math.max(1, cssH - pad.top - pad.bottom),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ export type DSP408Device = {
|
|||||||
knee: number,
|
knee: number,
|
||||||
threshold: number
|
threshold: number
|
||||||
) => Promise<boolean>;
|
) => Promise<boolean>;
|
||||||
|
setLimiter: (
|
||||||
|
outputChannel: OutputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
release: number
|
||||||
|
) => Promise<boolean>;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -702,6 +708,67 @@ export function useDSP408(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==========================================================
|
||||||
|
// Limiter
|
||||||
|
// ==========================================================
|
||||||
|
async function setLimiter(
|
||||||
|
dsp: DSP,
|
||||||
|
outputChannel: OutputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
release: number
|
||||||
|
): Promise<boolean> {
|
||||||
|
return withPollingPaused(dsp, async () => {
|
||||||
|
try {
|
||||||
|
const success = await invoke<boolean>('set_limiter', {
|
||||||
|
id: dsp.id,
|
||||||
|
channel: outputChannel,
|
||||||
|
threshold,
|
||||||
|
attack,
|
||||||
|
release,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
notify('Failed to change limiter 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,
|
||||||
|
output_states: {
|
||||||
|
...currentConfig.output_states,
|
||||||
|
[outputChannel]: {
|
||||||
|
...currentConfig.output_states[outputChannel],
|
||||||
|
limiter: {
|
||||||
|
threshold,
|
||||||
|
attack,
|
||||||
|
release,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
notify('Limiter updated', 'success', dsp.name);
|
||||||
|
return true;
|
||||||
|
} catch (err) {
|
||||||
|
notify(`Limiter update failed: ${String(err)}`, 'error', dsp.name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
// Return API
|
// Return API
|
||||||
// ==========================================================
|
// ==========================================================
|
||||||
@@ -730,6 +797,12 @@ export function useDSP408(
|
|||||||
knee: number,
|
knee: number,
|
||||||
threshold: number
|
threshold: number
|
||||||
) => setCompressor(dsp, outputChannel, ratio, attack, release, knee, threshold),
|
) => setCompressor(dsp, outputChannel, ratio, attack, release, knee, threshold),
|
||||||
|
setLimiter: (
|
||||||
|
outputChannel: OutputChannel,
|
||||||
|
threshold: number,
|
||||||
|
attack: number,
|
||||||
|
release: number
|
||||||
|
) => setLimiter(dsp, outputChannel, threshold, attack, release),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import CompressorSection from '../../components/dsp408/CompressorSection/Compres
|
|||||||
import { DSP408Device } from '../../hooks/useDSP408';
|
import { DSP408Device } from '../../hooks/useDSP408';
|
||||||
import './DspPage.css';
|
import './DspPage.css';
|
||||||
import GateSection from '../../components/dsp408/GateSection/GateSection';
|
import GateSection from '../../components/dsp408/GateSection/GateSection';
|
||||||
|
import LimiterSection from '../../components/dsp408/LimiterSection/LimiterSection';
|
||||||
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
|
import LoadingOverlay from '../../components/LoadingOverlay/LoadingOverlay';
|
||||||
import { InputChannel, OutputChannel } from '../../types/dsp408State';
|
import { InputChannel, OutputChannel } from '../../types/dsp408State';
|
||||||
|
|
||||||
@@ -183,6 +184,9 @@ function DSPPage({ dsp, dsp408 }: { dsp: DSP; dsp408: DSP408Device }) {
|
|||||||
case 'compressor':
|
case 'compressor':
|
||||||
return <CompressorSection dsp={dsp} set_compressor={dsp408.setCompressor} />;
|
return <CompressorSection dsp={dsp} set_compressor={dsp408.setCompressor} />;
|
||||||
|
|
||||||
|
case 'limiter':
|
||||||
|
return <LimiterSection dsp={dsp} set_limiter={dsp408.setLimiter} />;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user