Files
feuerwerk/src/editor/soundfx/eqView.ts
T
adminandClaude Opus 5.5 3377524958
game-maker / gate (push) Successful in 3m48s
game-maker / suite (push) Successful in 14m59s
Zoom the Parametric EQ's dB with the wheel, saved on its node (#376)
Close moves of a few dB were hard to set and to see on a curve that
always spans -24 to +24 dB. The wheel on free space now zooms the dB
the curve shows, and the node's small curve shows the same range.

- The range is one of ±3, ±6, ±12, ±24 and ±48 dB, always the same
  either side of 0. Up zooms in.
- It is saved on the node as `range`, so it is back after loading.
  Like the game scale (#226) it is no undo step. EditorState gets
  `setNodeView` for such a setting of a node.
- The lines across the curve follow the range: every 1, 2, 3, 6 or
  12 dB. A handle whose gain lies beyond the range stays on the edge,
  in sight and in reach.
- The wheel over a handle still turns the band's Q. It no longer does
  so anywhere while a band is picked: after opening, a band nearly
  always is, and the zoom would be out of reach.
- `EqArea` carries the range, so the tab and the node map dB alike.
  The spectrum keeps its own scale.

Tests:
- Unit parametric-eq: 16 green, 5 new (range in compile and save,
  node picture, wheel steps, grid lines, handle on the edge)
- Counter-check: the node's picture without the saved range turns
  the new picture test red
- e2e 34 (melektaus-atx): ALL PASSED, 99 checks, 1 new
- Gate (melektaus-atx): CHECK EXIT 0, 275 files, 4252 tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 16:36:33 +02:00

115 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* What the Parametric EQ's tab (#367) lays onto its picture beside the curve, whose axes are in
* `parametricEq.ts`: the handles of the bands, and the spectrum of what passes the node (#374).
*/
import {
type EqArea,
type EqBand,
dbToY,
EQ_Q,
EQ_RANGES,
frequencyToX,
hasGain,
xToFrequency,
} from '../../core/soundfx/parametricEq';
/**
* Where a band's handle sits: at its frequency, and at its gain or – without one – on the 0 dB line.
* A gain beyond what the curve is zoomed to keeps its handle on the edge, in sight and in reach.
*/
export function handleOf(band: EqBand, area: EqArea): { x: number; y: number } {
const y = dbToY(hasGain(band.type) ? band.gain : 0, area);
return { x: frequencyToX(band.frequency, area), y: Math.max(0, Math.min(area.h, y)) };
}
/** The band whose handle is under a point, the nearest one if several are; -1 for none. */
export function bandAt(bands: readonly EqBand[], x: number, y: number, area: EqArea, radius: number): number {
let found = -1;
let best = radius * radius;
bands.forEach((band, i) => {
const at = handleOf(band, area);
const d = (at.x - x) ** 2 + (at.y - y) ** 2;
if (d <= best) {
best = d;
found = i;
}
});
return found;
}
/** The Q after a turn of the wheel: a notch of it is a sixth of an octave narrower or wider. */
export function wheeledQ(q: number, deltaY: number): number {
const turned = q * 2 ** (-Math.sign(deltaY) / 6);
return Math.max(EQ_Q.min, Math.min(EQ_Q.max, Math.round(turned * 1000) / 1000));
}
/** The zoom after a turn of the wheel (#376): up shows the next smaller range, down the next larger. */
export function wheeledRange(range: number, deltaY: number): number {
const at = EQ_RANGES.indexOf(range) + Math.sign(deltaY);
return EQ_RANGES[Math.max(0, Math.min(EQ_RANGES.length - 1, at))]!;
}
/** The lines the picture is ruled with, and what they say. */
export const EQ_GRID_FREQUENCIES: readonly { hz: number; label: string }[] = [
{ hz: 20, label: '20' },
{ hz: 50, label: '50' },
{ hz: 100, label: '100' },
{ hz: 200, label: '200' },
{ hz: 500, label: '500' },
{ hz: 1000, label: '1k' },
{ hz: 2000, label: '2k' },
{ hz: 5000, label: '5k' },
{ hz: 10000, label: '10k' },
{ hz: 20000, label: '20k' },
];
/** The dB lines for a range: in thirds of it up to ±6 dB, in quarters above, so whole numbers. */
export function eqGridDb(range: number): number[] {
const parts = range <= 6 ? 3 : 4;
return Array.from({ length: parts * 2 - 1 }, (_, i) => ((i + 1 - parts) * range) / parts);
}
/** The spectrum's own scale (#374): full scale at the top of the picture, this many dB at its foot. */
export const SPECTRUM_FLOOR_DB = -96;
const spectrumY = (db: number, area: EqArea): number =>
(Math.min(0, Math.max(SPECTRUM_FLOOR_DB, db)) / SPECTRUM_FLOOR_DB) * area.h;
/**
* A spectrum, one level in dB every `binHz`, laid onto the picture at `xs`: each point takes the
* loudest bin from halfway to the point before to halfway to the next, or – where the bins lie
* further apart than the points, down in the bass – the level between the two nearest.
*/
export function spectrumYs(
dbs: ArrayLike<number>,
binHz: number,
xs: readonly number[],
area: EqArea,
): number[] {
const last = dbs.length - 1;
const level = (bin: number) => {
const db = dbs[Math.max(0, Math.min(last, bin))]!;
return Number.isFinite(db) ? db : SPECTRUM_FLOOR_DB;
};
return xs.map((x, i) => {
const from = Math.ceil(xToFrequency(i > 0 ? (xs[i - 1]! + x) / 2 : x, area) / binHz);
const to = Math.floor(xToFrequency(i < xs.length - 1 ? (x + xs[i + 1]!) / 2 : x, area) / binHz);
let db = -Infinity;
for (let bin = from; bin <= Math.min(to, last); bin++) db = Math.max(db, level(bin));
if (db === -Infinity) {
const at = xToFrequency(x, area) / binHz;
const below = Math.floor(at);
db = level(below) + (level(below + 1) - level(below)) * (at - below);
}
return spectrumY(db, area);
});
}
/** The frequency of the loudest bin, and how loud it is; null for a spectrum that is all silence. */
export function spectrumPeak(dbs: ArrayLike<number>, binHz: number): { hz: number; db: number } | null {
let best = -1;
for (let bin = 1; bin < dbs.length; bin++)
if (Number.isFinite(dbs[bin]) && (best < 0 || dbs[bin]! > dbs[best]!)) best = bin;
return best < 0 ? null : { hz: best * binHz, db: dbs[best]! };
}