The panel of #379 opened in a window of its own and replaced the tab. The user wanted the device in the graph instead, and the tab of #378 back on a double-click. The switch Hardware look goes again. - A node can show a face instead of its rows (`NodeFace`). Its inputs sit on the left of it and its outputs on the right, in the middle of the height. Each side is as wide as its longest port name. - A Compressor node shows the panel at full size, 820x280 graph units. Its settings and numbers are not listed as rows, the panel has them. - The knobs and the lever work in the node. A drag, a notch of the wheel and a double-click are one undo step each. A press on them neither picks nor moves the node, and the wheel does not zoom the graph. The number below a knob is only shown, typing stays in the tab and the inspector. - The meter and the LEDs run while the node is in the picture of a sound FX or instrument graph that is seen. Out of the picture, folded or hidden, the node lets go of its taps. - The window of one size, `fixedSize`, `refit`, `keptSize` and the rule that kept a Compressor out of the tab row are gone again. - A knob's drag as one undo step lives in `nodeParamDrag.ts` now, for the tab and the node. The numbers of a node and what auto-gain says moved to `compressorView.ts`. Found and fixed on the way: - The tab and the node listen to the same taps. The tab let go of them on every frame it was not seen, so the node's meter stood still while the tab was open. Now the tab lets go once, and in a frame everything unseen lets go before anything seen listens. Tests: - Unit: compressor-view (2 new), documents (the test of `keptSize` is gone with it) - e2e 34 (stein2, melektaus-atx was off): ALL PASSED, 122 checks. The four checks of #379 are now four of the node: ports beside the panel, knobs and lever in the node, the needle when played (21.7 dB with the tab open), and the double-click opening the tab. - Gate (stein2): CHECK EXIT 0, 283 files, 4356 tests - Counter-checks: an old auto-gain number read as off turns exactly its unit test red. A tab that lets go on every frame leaves the node's needle at 0 dB and no LED lit (probe on stein2). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
167 lines
6.2 KiB
TypeScript
167 lines
6.2 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 {
|
|
autoGainText,
|
|
type CompressorNumbers,
|
|
compressorGainDb,
|
|
compressorNumbers,
|
|
curveDbAtX,
|
|
curveDbAtY,
|
|
curveOutDb,
|
|
curveX,
|
|
curveY,
|
|
HISTORY_SECONDS,
|
|
LevelHistory,
|
|
levelY,
|
|
MAX_CURVE,
|
|
MIN_CURVE,
|
|
picturesLayout,
|
|
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: false,
|
|
};
|
|
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, and the auto-gain on top of it where it is on, as the gain after it', () => {
|
|
expect(compressorGainDb({ ...numbers, makeup: 6 })).toBe(6);
|
|
// -18 dB in comes out at -22.5: auto-gain gives the 4.5 dB back.
|
|
expect(compressorGainDb({ ...numbers, makeup: 6, autoGain: true })).toBeCloseTo(10.5, 3);
|
|
});
|
|
});
|
|
|
|
describe('the numbers of a Compressor node (#380)', () => {
|
|
it('takes what the node has set, its defaults where it has none or no number, and reads an old auto-gain', () => {
|
|
expect(compressorNumbers({ threshold: -30, ratio: 'x', autoGain: 0.5 })).toEqual({
|
|
threshold: -30,
|
|
ratio: 4,
|
|
knee: 6,
|
|
attack: 0.003,
|
|
release: 0.25,
|
|
makeup: 0,
|
|
autoGain: true,
|
|
});
|
|
expect(compressorNumbers({}).autoGain).toBe(false);
|
|
});
|
|
|
|
it('says what auto-gain gives, or that it is off', () => {
|
|
expect(autoGainText({ ...numbers, autoGain: false })).toBe('off');
|
|
expect(autoGainText({ ...numbers, autoGain: true })).toMatch(/^\+\d+\.\d dB$/);
|
|
});
|
|
});
|
|
|
|
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 }, 0);
|
|
history.push(1, null, 0);
|
|
expect(history.heard).toBe(true);
|
|
history.push(1 + HISTORY_SECONDS, null, 0);
|
|
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, gain: 0 },
|
|
{ t: 1, in: -6, out: -12, gain: 0 },
|
|
{ t: 2, in: -30, out: -30, gain: 0 },
|
|
{ t: 3, in: 0, out: -3, gain: 0 },
|
|
{ t: 4, in: 0, out: -3, gain: 0 },
|
|
];
|
|
const shapes = reductionShapes(points, 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]);
|
|
});
|
|
|
|
it('reads every reading by the gain after the compressing as it was then', () => {
|
|
// Raised by 20 dB until a second ago, by 0 since: the same 6 dB taken away both times.
|
|
const history = new LevelHistory();
|
|
history.push(0, { in: -24, out: -10 }, 20);
|
|
history.push(1, { in: -24, out: -10 }, 20);
|
|
history.push(2, { in: -24, out: -30 }, 0);
|
|
history.push(3, { in: -24, out: -30 }, 0);
|
|
const shapes = reductionShapes(history.all, 3, 300, 660);
|
|
expect(shapes).toHaveLength(1);
|
|
expect(shapes[0]).toHaveLength(16);
|
|
expect(shapes[0]!.filter((_, i) => i % 2 === 1)).toEqual([300, 300, 300, 300, 360, 360, 360, 360]);
|
|
});
|
|
});
|
|
|
|
describe('the pictures in the space of the tab', () => {
|
|
it('lets the levels fill the height and keeps the curve square, centred up and down', () => {
|
|
const at = picturesLayout(1600, 1000, 8);
|
|
expect(at.curve).toEqual({ x: 1600 - MAX_CURVE, y: (1000 - MAX_CURVE) / 2, side: MAX_CURVE });
|
|
expect(at.levels).toEqual({ w: 1600 - MAX_CURVE - 8, h: 1000 });
|
|
});
|
|
|
|
it('grows the curve with a lower tab, and holds it at its least in a narrow one', () => {
|
|
expect(picturesLayout(1600, 250, 8).curve).toEqual({ x: 1350, y: 0, side: 250 });
|
|
expect(picturesLayout(500, 400, 8).curve.side).toBe(200);
|
|
expect(picturesLayout(300, 400, 8).curve.side).toBe(MIN_CURVE);
|
|
});
|
|
});
|