Files
feuerwerk/tests/editor/compressor-view.test.ts
T
adminandClaude Opus 5.5 1df38bd430 Edit a Compressor in a tab of its own, with knobs and levels (#378)
A Compressor had only its number rows in the node. Its numbers are
easier to set by feel on a dial, and what it does is easier to judge
when the level going in and the level going out are seen side by side.

- Knob is a new widget in the toolkit: a slider bent into a circle,
  its name above and a number field below. A drag up or down turns
  it, Shift a tenth as far, a notch of the wheel a fiftieth of the
  turn, a double-click sets the default, the number takes a typed
  value. A taper gives the low end more of the turn.
- The Compressor's tab opens on a double-click, one per node, like
  the Parametric EQ's. It has a knob for each of the seven numbers,
  greyed while its input is linked.
- Its curve shows level in to level out from -60 to 0 dB. The point
  where it bends sets the threshold, the end of the curve sets the
  ratio that ends there.
- Its levels show the last 4 s on both sides of the node. Where it
  takes more than 0.5 dB away, the stretch below the level coming in
  is tinted. The makeup is left out of that.
- The Compressor is tapped like the EQ. SpectrumTaps listens to more
  than one node at once and reads the loudest sample of the last
  25 ms on each side.
- The node tabs share one map in the editor (nodeTabs), so a third
  kind needs no new plumbing.

Findings:

- CI task 614 for 23922ee was red on the memory check of the stream:
  the tab's renderer grew by 94 MB with nothing decoded. Play mode
  made garbage of its own and was read before a collection. Both
  readings in play now come after one, and the check compares the
  growth streamed with the growth decoded (3.7 MB against 59.8 MB).
- The ratio handle sat on the right edge of the picture, where a
  widget's hit area ends. The curve is inset by the grab radius.

Tests:

- Gate (melektaus-atx): 282 files, 4338 tests (17 new)
- e2e 34 (melektaus-atx): ALL PASSED, 116 checks (6 new)
- Counter-checks: an inverted ratio search, stop() letting go of
  every node and a drag without its least step each fail exactly
  their test.
- Looked at the tab in a screenshot: a blip at -4.4 dB came out at
  -16.7 dB, the gap tinted, the threshold line at -24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 22:40:12 +02:00

115 lines
4.1 KiB
TypeScript

/**
* What the tab of a Compressor draws (#378): its curve and the handles on it, the ratio a drag of
* the end of the curve asks for, and the level picture tinted where the compressor works.
*/
import { describe, expect, it } from 'vitest';
import {
type CompressorNumbers,
compressorGainDb,
curveDbAtX,
curveDbAtY,
curveOutDb,
curveX,
curveY,
HISTORY_SECONDS,
LevelHistory,
levelY,
ratioFor,
ratioHandle,
reductionOf,
reductionShapes,
thresholdHandle,
} from '../../src/editor/soundfx/compressorView';
const numbers: CompressorNumbers = {
threshold: -24,
knee: 0,
ratio: 4,
attack: 0.003,
release: 0.25,
makeup: 0,
autoGain: 0,
};
const RATIO = { min: 1, max: 20, step: 0.1 };
describe('the curve of a Compressor', () => {
it('leaves a level below the threshold as it is and squeezes one above it by the ratio', () => {
expect(curveOutDb(-40, -24, 0, 4)).toBeCloseTo(-40, 6);
// 24 dB over a threshold of -24 come out 6 dB over it at a ratio of 4.
expect(curveOutDb(0, -24, 0, 4)).toBeCloseTo(-18, 1);
});
it('spans -60 to 0 dB on both axes, level out going up', () => {
expect(curveX(-60, 300)).toBe(0);
expect(curveX(0, 300)).toBe(300);
expect(curveY(0, 300)).toBe(0);
expect(curveDbAtX(150, 300)).toBe(-30);
expect(curveDbAtY(100, 300)).toBe(-20);
});
it('has the threshold where it starts to bend and the ratio at its end', () => {
expect(thresholdHandle(numbers, 300)).toEqual({ x: 180, y: 120 });
const end = ratioHandle(numbers, 300);
expect(end.x).toBe(300);
expect(curveDbAtY(end.y, 300)).toBeCloseTo(-18, 1);
});
it('finds the ratio whose end is where it is dragged to, held to the range', () => {
expect(ratioFor(-18, numbers, RATIO)).toBe(4);
expect(ratioFor(-12, numbers, RATIO)).toBe(2);
expect(ratioFor(5, numbers, RATIO)).toBe(1);
expect(ratioFor(-40, numbers, RATIO)).toBe(20);
// With a knee the curve ends lower than the straight line would, and the ratio found still lands there.
const kneed = { ...numbers, knee: 6 };
const ratio = ratioFor(-16, kneed, RATIO);
expect(curveOutDb(0, -24, 6, ratio)).toBeCloseTo(-16, 0);
});
it('counts the makeup, the auto-gain or a blend of both as the gain after it', () => {
expect(compressorGainDb({ ...numbers, makeup: 6 })).toBe(6);
expect(compressorGainDb({ ...numbers, makeup: 6, autoGain: 1 })).toBeCloseTo(18, 1);
expect(compressorGainDb({ ...numbers, makeup: 6, autoGain: 0.5 })).toBeCloseTo(12, 1);
});
});
describe('the level picture of a Compressor', () => {
it('spans +6 to -60 dB top to bottom, silence at the bottom', () => {
expect(levelY(6, 660)).toBe(0);
expect(levelY(-60, 660)).toBe(660);
expect(levelY(-Infinity, 660)).toBe(660);
expect(levelY(0, 660)).toBe(60);
});
it('keeps the readings of the last seconds and knows when all of them are silent', () => {
const history = new LevelHistory();
history.push(0, { in: -6, out: -9 });
history.push(1, null);
expect(history.heard).toBe(true);
history.push(1 + HISTORY_SECONDS, null);
expect(history.all.map((p) => p.t)).toEqual([1, 1 + HISTORY_SECONDS]);
expect(history.heard).toBe(false);
});
it('takes away what came in less what went out, the gain after it left out', () => {
expect(reductionOf({ in: -6, out: -12 }, 0)).toBe(6);
expect(reductionOf({ in: -6, out: -6 }, 6)).toBe(6);
expect(reductionOf({ in: -30, out: -29 }, 0)).toBe(0);
expect(reductionOf({ in: -Infinity, out: -Infinity }, 0)).toBe(0);
});
it('tints from the level coming in down by what is taken away, one shape per stretch', () => {
const points = [
{ t: 0, in: -6, out: -12 },
{ t: 1, in: -6, out: -12 },
{ t: 2, in: -30, out: -30 },
{ t: 3, in: 0, out: -3 },
{ t: 4, in: 0, out: -3 },
];
const shapes = reductionShapes(points, 0, 4, 400, 660);
expect(shapes).toHaveLength(2);
// Along the level coming in, then back along it less what is taken away.
expect(shapes[0]).toEqual([0, 120, 100, 120, 100, 180, 0, 180]);
expect(shapes[1]).toEqual([300, 60, 400, 60, 400, 90, 300, 90]);
});
});