The small curve below the EQ node's rows (#375) only showed the bands. Like the Compressor since #380, the node now shows the editor of its tab between its inputs and its output, and is worked right there. A double-click on the node still opens the same editor in a tab. Two wishes for the nodes with a panel came on top and share the same files, so they are in this commit as well: the numbers below the Compressor's knobs read as plain labels (#382), and the wheel over a node zooms the graph instead of turning a value (#383). #381, the EQ in its node: - EqPanel holds the curve, the band's numbers and the help line, taken out of EqEditor. The tab puts its one panel under a header, each EQ node has a panel of its own at 720 x 360 (EqFace). - A press on the curve neither picks nor moves the node. A double-click on the curve adds a band, one on the title or the edge opens the tab. - In the node a press on the Type or Slope button stays with the button, so the node is not picked or moved. - The spectrum runs in the node while it is in the picture. The tab lets go of the tap once, like the Compressor's, and Editor.tick lets every unseen tab and graph go before any seen one listens. - The count of bands moved from the tab's header into the band row, so the node shows it as well. - The node picture of #375 is gone with its only user: NodePicture, EQ_PICTURE and NodeView's drawing of it. #382, the numbers below the Compressor's knobs in its node: - A Knob with typed: false shows its number as a centred Label without a box, in the panel's ink. The ratio reads 4:1 (ratioText). - The label is as high as its text and sits up in the dial's open bottom, right under the ends of the scale. - Label takes a colour of its own. #383, the wheel in a graph: - GraphCanvas takes the wheel on the way down and stops it there. It zooms over a node too, and no knob or curve in a node sees it. - The tabs keep their wheel: a knob's step, a band's Q and the zoom of the EQ's dB. Found on the way: - A Menu opened below a button in the zoomed graph would have been placed by the button's own height. openBelow now takes the bottom edge as drawn. - The band row did not lay out its labels after a text change when its own size stayed. In the tab a later resize hid it, in the node "Band 1" and the count were missing. The row now lays out itself. - Handle and point positions for the checks were unscaled. They now go through toGlobal and hold at the graph's zoom. Tests: - Unit parametric-eq: 16 tests. The #375 tests now check eqCurvePoints and eqRangeOf directly. compressor-view: ratioText. - e2e 34 (atx): ALL PASSED, 125 checks (3 new: the EQ panel and its ports, editing in the node, the spectrum in the node with the tab open). The Compressor node's check reads its numbers as labels and 8:1. Both node checks now expect the wheel to zoom the graph and leave the ratio, the Q and the dB zoom alone. - The unseen-tab check now leaves for the Scene tab, since the node in the graph listens to the same tap. - Counter-check: the tab letting go of the tap every unseen frame stalls the node's spectrum with the tab open (1 frame against 22, probe on stein2). - Gate (atx): CHECK EXIT 0, 283 files, 4357 tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
173 lines
6.4 KiB
TypeScript
173 lines
6.4 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,
|
|
ratioText,
|
|
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$/);
|
|
});
|
|
|
|
it('prints the ratio as a device does, without a trailing zero (#382)', () => {
|
|
expect(ratioText(4)).toBe('4:1');
|
|
expect(ratioText(2.5)).toBe('2.5: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 }, 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);
|
|
});
|
|
});
|