feat: add geq graph + bypass warning
This commit is contained in:
@@ -8,3 +8,4 @@ This template should help get you started developing with Tauri, React and Types
|
|||||||
|
|
||||||
# TODO
|
# TODO
|
||||||
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
- Move meters polling logic outside dsp state because it causes re-render of everything tied to dsp constantly
|
||||||
|
- Check render to improve perfoormances
|
||||||
@@ -5,6 +5,8 @@ import Button from '../../../../components/Button/Button';
|
|||||||
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
||||||
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
|
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
|
||||||
import './GainSection.css';
|
import './GainSection.css';
|
||||||
|
import { DiscreteFrequency } from '../../types';
|
||||||
|
import { DISCRETE_FREQUENCIES } from '../../constants';
|
||||||
|
|
||||||
function GainPanel({
|
function GainPanel({
|
||||||
dsp,
|
dsp,
|
||||||
@@ -48,6 +50,7 @@ function GainPanel({
|
|||||||
step={(v) => (v >= -20 ? 0.1 : 0.5)}
|
step={(v) => (v >= -20 ? 0.1 : 0.5)}
|
||||||
unit=" dB"
|
unit=" dB"
|
||||||
value={gain}
|
value={gain}
|
||||||
|
formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))}
|
||||||
onChange={(value) => setGain(channel, value)}
|
onChange={(value) => setGain(channel, value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -116,6 +119,8 @@ function GainSection({
|
|||||||
{ channel: { Output: OutputChannel.Out8 }, title: 'Out 8' },
|
{ channel: { Output: OutputChannel.Out8 }, title: 'Out 8' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const currentChannel = dsp.state.current_config.input_states[InputChannel.InA];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="gain-linear-graph-section">
|
<div className="gain-linear-graph-section">
|
||||||
@@ -131,13 +136,46 @@ function GainSection({
|
|||||||
xLabel="Hz"
|
xLabel="Hz"
|
||||||
yLabel="dB"
|
yLabel="dB"
|
||||||
draw={({ ctx, toPx }) => {
|
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.strokeStyle = '#4ade80';
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
const [x0, y0] = toPx(20, 0);
|
ctx.lineJoin = 'round';
|
||||||
const [x1, y1] = toPx(20000, 0);
|
ctx.lineCap = 'round';
|
||||||
|
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(x0, y0);
|
ctx.moveTo(points[0].x, points[0].y);
|
||||||
ctx.lineTo(x1, y1);
|
|
||||||
|
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();
|
ctx.stroke();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -141,3 +141,29 @@
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,54 +4,22 @@ import { DSP408 as DSP } from '../../../../types';
|
|||||||
import Button from '../../../../components/Button/Button';
|
import Button from '../../../../components/Button/Button';
|
||||||
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
import ChannelGroup from '../../../../components/ChannelGroup/ChannelGroup';
|
||||||
import { LinearGraph } from '../../../../components/LinearGraph/LinearGraph';
|
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';
|
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<Record<DiscreteFrequency, number>>;
|
type LiveBands = Partial<Record<DiscreteFrequency, number>>;
|
||||||
|
|
||||||
type SetGeqBandFn = (
|
type SetGeqBandFn = (
|
||||||
@@ -78,7 +46,7 @@ function GeqBandPanel({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="geq-band-panel">
|
<div className="geq-band-panel">
|
||||||
{GEQ_BANDS.map(({ frequency, label }) => (
|
{GEQ_BANDS_LABEL.map(({ frequency, label }) => (
|
||||||
<div className="geq-band-cell" key={frequency}>
|
<div className="geq-band-cell" key={frequency}>
|
||||||
<VerticalSlider
|
<VerticalSlider
|
||||||
min={GEQ_GAIN_MIN}
|
min={GEQ_GAIN_MIN}
|
||||||
@@ -112,13 +80,7 @@ function GeqGraph({
|
|||||||
}) {
|
}) {
|
||||||
const geq = dsp.state.current_config.input_states[channel].geq;
|
const geq = dsp.state.current_config.input_states[channel].geq;
|
||||||
|
|
||||||
const labelByX = useMemo(() => {
|
const values = GEQ_BANDS_LABEL.map(
|
||||||
const map = new Map<number, string>();
|
|
||||||
GEQ_BANDS.forEach((band, i) => map.set(i + 1, band.label));
|
|
||||||
return map;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const values = GEQ_BANDS.map(
|
|
||||||
({ frequency }) => liveBands?.[frequency] ?? geq.gains[frequency] ?? 0
|
({ frequency }) => liveBands?.[frequency] ?? geq.gains[frequency] ?? 0
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -126,31 +88,31 @@ function GeqGraph({
|
|||||||
<div className="geq-graph-panel">
|
<div className="geq-graph-panel">
|
||||||
<div className="geq-graph-title">{title}</div>
|
<div className="geq-graph-title">{title}</div>
|
||||||
<LinearGraph
|
<LinearGraph
|
||||||
xMin={0.5}
|
xMin={GRAPH_X_MIN}
|
||||||
xMax={GEQ_BANDS.length + 0.5}
|
xMax={GRAPH_X_MAX}
|
||||||
xStep={1}
|
xScale="log"
|
||||||
yMin={GEQ_GAIN_MIN}
|
yMin={GRAPH_Y_MIN}
|
||||||
yMax={GEQ_GAIN_MAX}
|
yMax={GRAPH_Y_MAX}
|
||||||
|
yStep={3}
|
||||||
xLabel="Band Hz"
|
xLabel="Band Hz"
|
||||||
yLabel="dB"
|
yLabel="dB"
|
||||||
showGrid={true}
|
showGrid={true}
|
||||||
formatX={(v) => labelByX.get(Math.round(v)) ?? ''}
|
draw={buildGeqCurveDraw({
|
||||||
formatY={(v) => v.toFixed(0)}
|
values,
|
||||||
draw={({ ctx, toPx, plot }) => {
|
bandHz: GEQ_BAND_HZ,
|
||||||
const barWidth = (plot.width / GEQ_BANDS.length) * 0.6;
|
q: GRAPH_GEQ_CURVE_Q * 0.5,
|
||||||
|
xMin: GRAPH_X_MIN,
|
||||||
values.forEach((gain, i) => {
|
xMax: GRAPH_X_MAX,
|
||||||
const x = i + 1;
|
yMin: GRAPH_Y_MIN,
|
||||||
const [xPx, zeroY] = toPx(x, 0);
|
yMax: GRAPH_Y_MAX,
|
||||||
const [, gainY] = toPx(x, gain);
|
dimmed: geq.bypass,
|
||||||
|
})}
|
||||||
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);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
{geq.bypass && (
|
||||||
|
<div className="geq-bypass-overlay">
|
||||||
|
<span>BYPASSED</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -274,7 +236,7 @@ function GeqSection({
|
|||||||
if (resetting) return;
|
if (resetting) return;
|
||||||
setResetting(true);
|
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.
|
// Skip bands already at 0 to save a round trip.
|
||||||
const current = dsp.state.current_config.input_states[channel].geq.gains[frequency] ?? 0;
|
const current = dsp.state.current_config.input_states[channel].geq.gains[frequency] ?? 0;
|
||||||
if (current === 0) continue;
|
if (current === 0) continue;
|
||||||
|
|||||||
@@ -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, number> = {
|
||||||
|
[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;
|
||||||
@@ -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:
|
||||||
|
* <LinearGraph ... draw={buildGeqCurveDraw({ values, q: 8.65, xMin, xMax, yMin, yMax })} />
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user