diff --git a/README.md b/README.md index 6a7e457..1ac727b 100644 --- a/README.md +++ b/README.md @@ -7,4 +7,5 @@ This template should help get you started developing with Tauri, React and Types - [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 \ No newline at end of file +- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly +- Check render to improve perfoormances \ No newline at end of file diff --git a/src/features/dsp408/components/GainSection/GainSection.tsx b/src/features/dsp408/components/GainSection/GainSection.tsx index fa1991d..1c22c31 100644 --- a/src/features/dsp408/components/GainSection/GainSection.tsx +++ b/src/features/dsp408/components/GainSection/GainSection.tsx @@ -5,6 +5,8 @@ import Button from '../../../../components/Button/Button'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph'; import './GainSection.css'; +import { DiscreteFrequency } from '../../types'; +import { DISCRETE_FREQUENCIES } from '../../constants'; function GainPanel({ dsp, @@ -48,6 +50,7 @@ function GainPanel({ step={(v) => (v >= -20 ? 0.1 : 0.5)} unit=" dB" value={gain} + formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))} onChange={(value) => setGain(channel, value)} /> @@ -116,6 +119,8 @@ function GainSection({ { channel: { Output: OutputChannel.Out8 }, title: 'Out 8' }, ]; + const currentChannel = dsp.state.current_config.input_states[InputChannel.InA]; + return ( <>
@@ -131,13 +136,46 @@ function GainSection({ xLabel="Hz" yLabel="dB" draw={({ ctx, toPx }) => { + const geq = currentChannel.geq; + + if (geq.bypass) { + return; + } + + const points = Object.entries(geq.gains) + .map(([frequency, gain]) => ({ + frequency: DISCRETE_FREQUENCIES[frequency as DiscreteFrequency], + gain, + })) + .sort((a, b) => a.frequency - b.frequency) + .map(({ frequency, gain }) => { + const [x, y] = toPx(frequency, gain); + return { x, y }; + }); + + if (points.length < 2) { + return; + } + ctx.strokeStyle = '#4ade80'; ctx.lineWidth = 2; - const [x0, y0] = toPx(20, 0); - const [x1, y1] = toPx(20000, 0); + ctx.lineJoin = 'round'; + ctx.lineCap = 'round'; + ctx.beginPath(); - ctx.moveTo(x0, y0); - ctx.lineTo(x1, y1); + ctx.moveTo(points[0].x, points[0].y); + + for (let i = 1; i < points.length; i++) { + const previous = points[i - 1]; + const current = points[i]; + + const midX = (previous.x + current.x) / 2; + + ctx.quadraticCurveTo(previous.x, previous.y, midX, (previous.y + current.y) / 2); + + ctx.quadraticCurveTo(current.x, current.y, current.x, current.y); + } + ctx.stroke(); }} /> diff --git a/src/features/dsp408/components/GeqSection/GeqSection.css b/src/features/dsp408/components/GeqSection/GeqSection.css index 2ac883e..592e0ef 100644 --- a/src/features/dsp408/components/GeqSection/GeqSection.css +++ b/src/features/dsp408/components/GeqSection/GeqSection.css @@ -141,3 +141,29 @@ flex: 1; min-height: 0; } + +.geq-graph-canvas-wrap { + position: relative; +} + +.geq-bypass-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + z-index: 2; + padding-bottom: 40cqh; +} + +.geq-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; +} diff --git a/src/features/dsp408/components/GeqSection/GeqSection.tsx b/src/features/dsp408/components/GeqSection/GeqSection.tsx index a09ef16..593552a 100644 --- a/src/features/dsp408/components/GeqSection/GeqSection.tsx +++ b/src/features/dsp408/components/GeqSection/GeqSection.tsx @@ -4,54 +4,22 @@ import { DSP408 as DSP } from '../../../../types'; import Button from '../../../../components/Button/Button'; import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup'; import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph'; -import { useMemo, useState } from 'react'; +import { GEQ_BAND_HZ, buildGeqCurveDraw } from '../../utils/geqCurve'; +import { + INPUT_ORDER, + GRAPH_X_MIN, + GRAPH_X_MAX, + GRAPH_Y_MIN, + GRAPH_Y_MAX, + GRAPH_GEQ_CURVE_Q, + GEQ_GAIN_MIN, + GEQ_GAIN_MAX, + GEQ_GAIN_STEP, + GEQ_BANDS_LABEL, +} from '../../constants'; +import { useState } from 'react'; import './GeqSection.css'; -const GEQ_BANDS = [ - { frequency: DiscreteFrequency.F20, label: '20' }, - { frequency: DiscreteFrequency.F25, label: '25' }, - { frequency: DiscreteFrequency.F31_5, label: '31.5' }, - { frequency: DiscreteFrequency.F40, label: '40' }, - { frequency: DiscreteFrequency.F50, label: '50' }, - { frequency: DiscreteFrequency.F63, label: '63' }, - { frequency: DiscreteFrequency.F80, label: '80' }, - { frequency: DiscreteFrequency.F100, label: '100' }, - { frequency: DiscreteFrequency.F125, label: '125' }, - { frequency: DiscreteFrequency.F160, label: '160' }, - { frequency: DiscreteFrequency.F200, label: '200' }, - { frequency: DiscreteFrequency.F250, label: '250' }, - { frequency: DiscreteFrequency.F315, label: '315' }, - { frequency: DiscreteFrequency.F400, label: '400' }, - { frequency: DiscreteFrequency.F500, label: '500' }, - { frequency: DiscreteFrequency.F630, label: '630' }, - { frequency: DiscreteFrequency.F800, label: '800' }, - { frequency: DiscreteFrequency.F1000, label: '1k' }, - { frequency: DiscreteFrequency.F1250, label: '1.25k' }, - { frequency: DiscreteFrequency.F1600, label: '1.6k' }, - { frequency: DiscreteFrequency.F2000, label: '2k' }, - { frequency: DiscreteFrequency.F2500, label: '2.5k' }, - { frequency: DiscreteFrequency.F3150, label: '3.15k' }, - { frequency: DiscreteFrequency.F4000, label: '4k' }, - { frequency: DiscreteFrequency.F5000, label: '5k' }, - { frequency: DiscreteFrequency.F6300, label: '6.3k' }, - { frequency: DiscreteFrequency.F8000, label: '8k' }, - { frequency: DiscreteFrequency.F10000, label: '10k' }, - { frequency: DiscreteFrequency.F12500, label: '12.5k' }, - { frequency: DiscreteFrequency.F16000, label: '16k' }, - { frequency: DiscreteFrequency.F20000, label: '20k' }, -] as const; - -const GEQ_GAIN_MIN = -12; -const GEQ_GAIN_MAX = 12; -const GEQ_GAIN_STEP = 0.1; - -const INPUT_ORDER: InputChannel[] = [ - InputChannel.InA, - InputChannel.InB, - InputChannel.InC, - InputChannel.InD, -]; - type LiveBands = Partial>; type SetGeqBandFn = ( @@ -78,7 +46,7 @@ function GeqBandPanel({ return (
- {GEQ_BANDS.map(({ frequency, label }) => ( + {GEQ_BANDS_LABEL.map(({ frequency, label }) => (
{ - const map = new Map(); - GEQ_BANDS.forEach((band, i) => map.set(i + 1, band.label)); - return map; - }, []); - - const values = GEQ_BANDS.map( + const values = GEQ_BANDS_LABEL.map( ({ frequency }) => liveBands?.[frequency] ?? geq.gains[frequency] ?? 0 ); @@ -126,31 +88,31 @@ function GeqGraph({
{title}
labelByX.get(Math.round(v)) ?? ''} - formatY={(v) => v.toFixed(0)} - draw={({ ctx, toPx, plot }) => { - const barWidth = (plot.width / GEQ_BANDS.length) * 0.6; - - values.forEach((gain, i) => { - const x = i + 1; - const [xPx, zeroY] = toPx(x, 0); - const [, gainY] = toPx(x, gain); - - ctx.fillStyle = gain >= 0 ? '#4ade80' : '#f87171'; - const top = Math.min(zeroY, gainY); - const height = Math.abs(gainY - zeroY); - ctx.fillRect(xPx - barWidth / 2, top, barWidth, height); - }); - }} + draw={buildGeqCurveDraw({ + values, + bandHz: GEQ_BAND_HZ, + q: GRAPH_GEQ_CURVE_Q * 0.5, + xMin: GRAPH_X_MIN, + xMax: GRAPH_X_MAX, + yMin: GRAPH_Y_MIN, + yMax: GRAPH_Y_MAX, + dimmed: geq.bypass, + })} /> + {geq.bypass && ( +
+ BYPASSED +
+ )}
); } @@ -274,7 +236,7 @@ function GeqSection({ if (resetting) return; setResetting(true); - for (const { frequency } of GEQ_BANDS) { + for (const { frequency } of GEQ_BANDS_LABEL) { // Skip bands already at 0 to save a round trip. const current = dsp.state.current_config.input_states[channel].geq.gains[frequency] ?? 0; if (current === 0) continue; diff --git a/src/features/dsp408/constants.ts b/src/features/dsp408/constants.ts new file mode 100644 index 0000000..1eedcc2 --- /dev/null +++ b/src/features/dsp408/constants.ts @@ -0,0 +1,89 @@ +import { DiscreteFrequency, InputChannel } from './types'; + +export const INPUT_ORDER: InputChannel[] = [ + InputChannel.InA, + InputChannel.InB, + InputChannel.InC, + InputChannel.InD, +]; + +export const DISCRETE_FREQUENCIES: Record = { + [DiscreteFrequency.F20]: 20, + [DiscreteFrequency.F25]: 25, + [DiscreteFrequency.F31_5]: 31.5, + [DiscreteFrequency.F40]: 40, + [DiscreteFrequency.F50]: 50, + [DiscreteFrequency.F63]: 63, + [DiscreteFrequency.F80]: 80, + [DiscreteFrequency.F100]: 100, + [DiscreteFrequency.F125]: 125, + [DiscreteFrequency.F160]: 160, + [DiscreteFrequency.F200]: 200, + [DiscreteFrequency.F250]: 250, + [DiscreteFrequency.F315]: 315, + [DiscreteFrequency.F400]: 400, + [DiscreteFrequency.F500]: 500, + [DiscreteFrequency.F630]: 630, + [DiscreteFrequency.F800]: 800, + [DiscreteFrequency.F1000]: 1000, + [DiscreteFrequency.F1250]: 1250, + [DiscreteFrequency.F1600]: 1600, + [DiscreteFrequency.F2000]: 2000, + [DiscreteFrequency.F2500]: 2500, + [DiscreteFrequency.F3150]: 3150, + [DiscreteFrequency.F4000]: 4000, + [DiscreteFrequency.F5000]: 5000, + [DiscreteFrequency.F6300]: 6300, + [DiscreteFrequency.F8000]: 8000, + [DiscreteFrequency.F10000]: 10000, + [DiscreteFrequency.F12500]: 12500, + [DiscreteFrequency.F16000]: 16000, + [DiscreteFrequency.F20000]: 20000, +}; + +// ------ GEQ constants ------ +export const GRAPH_GEQ_CURVE_Q = 8.65; + +export const GEQ_BANDS_LABEL = [ + { frequency: DiscreteFrequency.F20, label: '20' }, + { frequency: DiscreteFrequency.F25, label: '25' }, + { frequency: DiscreteFrequency.F31_5, label: '31.5' }, + { frequency: DiscreteFrequency.F40, label: '40' }, + { frequency: DiscreteFrequency.F50, label: '50' }, + { frequency: DiscreteFrequency.F63, label: '63' }, + { frequency: DiscreteFrequency.F80, label: '80' }, + { frequency: DiscreteFrequency.F100, label: '100' }, + { frequency: DiscreteFrequency.F125, label: '125' }, + { frequency: DiscreteFrequency.F160, label: '160' }, + { frequency: DiscreteFrequency.F200, label: '200' }, + { frequency: DiscreteFrequency.F250, label: '250' }, + { frequency: DiscreteFrequency.F315, label: '315' }, + { frequency: DiscreteFrequency.F400, label: '400' }, + { frequency: DiscreteFrequency.F500, label: '500' }, + { frequency: DiscreteFrequency.F630, label: '630' }, + { frequency: DiscreteFrequency.F800, label: '800' }, + { frequency: DiscreteFrequency.F1000, label: '1k' }, + { frequency: DiscreteFrequency.F1250, label: '1.25k' }, + { frequency: DiscreteFrequency.F1600, label: '1.6k' }, + { frequency: DiscreteFrequency.F2000, label: '2k' }, + { frequency: DiscreteFrequency.F2500, label: '2.5k' }, + { frequency: DiscreteFrequency.F3150, label: '3.15k' }, + { frequency: DiscreteFrequency.F4000, label: '4k' }, + { frequency: DiscreteFrequency.F5000, label: '5k' }, + { frequency: DiscreteFrequency.F6300, label: '6.3k' }, + { frequency: DiscreteFrequency.F8000, label: '8k' }, + { frequency: DiscreteFrequency.F10000, label: '10k' }, + { frequency: DiscreteFrequency.F12500, label: '12.5k' }, + { frequency: DiscreteFrequency.F16000, label: '16k' }, + { frequency: DiscreteFrequency.F20000, label: '20k' }, +] as const; + +export const GEQ_GAIN_MIN = -12; +export const GEQ_GAIN_MAX = 12; +export const GEQ_GAIN_STEP = 0.1; + +// ------ Graph constants ------ +export const GRAPH_X_MIN = 10; +export const GRAPH_X_MAX = 30000; +export const GRAPH_Y_MIN = -21; +export const GRAPH_Y_MAX = 21; diff --git a/src/features/dsp408/utils/geqCurve.ts b/src/features/dsp408/utils/geqCurve.ts new file mode 100644 index 0000000..7dbed1e --- /dev/null +++ b/src/features/dsp408/utils/geqCurve.ts @@ -0,0 +1,144 @@ +export const GEQ_BAND_HZ = [ + 20, 25, 31.5, 40, 50, 63, 80, 100, 125, 160, 200, 250, 315, 400, 500, 630, 800, 1000, 1250, 1600, + 2000, 2500, 3150, 4000, 5000, 6300, 8000, 10000, 12500, 16000, 20000, +] as const; + +/** Peaking-filter magnitude response (dB) of a single band at a given frequency. */ +export function geqPeakResponseDb( + freq: number, + bandFreq: number, + gainDb: number, + q: number +): number { + if (gainDb === 0) return 0; + const ratio = freq / bandFreq - bandFreq / freq; + return gainDb / (1 + (q * ratio) ** 2); +} + +/** Sum of all bands' contributions at a given frequency. */ +export function geqTotalCurveDb( + freq: number, + values: number[], + bandHz: readonly number[] = GEQ_BAND_HZ, + q: number +): number { + let sum = 0; + for (let i = 0; i < values.length; i++) { + sum += geqPeakResponseDb(freq, bandHz[i], values[i], q); + } + return sum; +} + +/** Log-spaced sample points from xMin to xMax, for a pixel-smooth curve on a log x-axis. */ +export function logSpace(xMin: number, xMax: number, steps: number): number[] { + const logMin = Math.log10(xMin); + const logMax = Math.log10(xMax); + const out: number[] = []; + for (let s = 0; s <= steps; s++) { + out.push(Math.pow(10, logMin + ((logMax - logMin) * s) / steps)); + } + return out; +} + +export type GeqCurveColors = { + strokeColor: string; + fillAbove: string; + fillAboveFade: string; + fillBelowFade: string; + fillBelow: string; +}; + +export const DEFAULT_GEQ_CURVE_COLORS: GeqCurveColors = { + strokeColor: '#e5e7eb', + fillAbove: 'rgba(74, 222, 128, 0.35)', + fillAboveFade: 'rgba(74, 222, 128, 0.08)', + fillBelowFade: 'rgba(248, 113, 113, 0.08)', + fillBelow: 'rgba(248, 113, 113, 0.35)', +}; + +export type GeqCurveOptions = { + values: number[]; + bandHz?: readonly number[]; + q: number; + xMin: number; + xMax: number; + yMin: number; + yMax: number; + steps?: number; + colors?: GeqCurveColors; + lineWidth?: number; + dimmed?: boolean; +}; + +/** + * Returns a `draw` callback compatible with LinearGraph's `draw` prop: + * ({ ctx, toPx }) => void + * + * Usage: + * + */ +export function buildGeqCurveDraw({ + values, + bandHz = GEQ_BAND_HZ, + q, + xMin, + xMax, + yMin, + yMax, + steps = 500, + colors = DEFAULT_GEQ_CURVE_COLORS, + lineWidth = 2, + dimmed = false, +}: GeqCurveOptions) { + return ({ + ctx, + toPx, + }: { + ctx: CanvasRenderingContext2D; + toPx: (x: number, y: number) => [number, number]; + }) => { + const freqs = logSpace(xMin, xMax, steps); + const points = freqs.map((freq) => { + const gain = geqTotalCurveDb(freq, values, bandHz, q); + return toPx(freq, gain); + }); + + ctx.save(); + if (dimmed) ctx.globalAlpha = 0.25; // NEW: fade everything drawn below + + // Fill area under the curve down to 0 dB + ctx.beginPath(); + points.forEach(([xPx, yPx], i) => { + if (i === 0) ctx.moveTo(xPx, yPx); + else ctx.lineTo(xPx, yPx); + }); + const [xEndPx, zeroYPx] = toPx(xMax, 0); + const [xStartPx] = toPx(xMin, 0); + ctx.lineTo(xEndPx, zeroYPx); + ctx.lineTo(xStartPx, zeroYPx); + ctx.closePath(); + + const [, topPx] = toPx(xMin, yMax); + const [, bottomPx] = toPx(xMin, yMin); + const gradient = ctx.createLinearGradient(0, topPx, 0, bottomPx); + gradient.addColorStop(0, colors.fillAbove); + gradient.addColorStop(0.5, colors.fillAboveFade); + gradient.addColorStop(0.5, colors.fillBelowFade); + gradient.addColorStop(1, colors.fillBelow); + ctx.fillStyle = gradient; + ctx.fill(); + + // Stroke the curve on top + ctx.beginPath(); + points.forEach(([xPx, yPx], i) => { + if (i === 0) ctx.moveTo(xPx, yPx); + else ctx.lineTo(xPx, yPx); + }); + ctx.strokeStyle = colors.strokeColor; + ctx.lineWidth = lineWidth; + if (dimmed) ctx.setLineDash([6, 4]); // dashed = "not actually applied" + ctx.stroke(); + + ctx.restore(); + }; +}