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>
115 lines
4.1 KiB
TypeScript
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]);
|
|
});
|
|
});
|