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>
55 lines
2.1 KiB
TypeScript
55 lines
2.1 KiB
TypeScript
/**
|
|
* The rules of a Knob (#378): where a value sits on the turn, and what a drag and the wheel make of it.
|
|
*/
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
draggedValue,
|
|
KNOB_TRAVEL,
|
|
knobOrigin,
|
|
knobPosition,
|
|
knobValue,
|
|
wheeledValue,
|
|
} from '../../src/ui/knob';
|
|
|
|
const threshold = { min: -100, max: 0, step: 0.5 };
|
|
const attack = { min: 0, max: 1, step: 0.001, taper: 3 };
|
|
const makeup = { min: -24, max: 24, step: 0.5 };
|
|
|
|
describe('a knob', () => {
|
|
it('turns evenly without a taper, and gives the low end more of the turn with one', () => {
|
|
expect(knobPosition(-50, threshold)).toBeCloseTo(0.5, 9);
|
|
expect(knobValue(0.5, threshold)).toBe(-50);
|
|
// An attack of 3 ms is an eighth of the way round, not three thousandths.
|
|
expect(knobPosition(0.003, attack)).toBeCloseTo(0.144, 3);
|
|
expect(knobValue(knobPosition(0.003, attack), attack)).toBe(0.003);
|
|
expect(knobValue(-1, attack)).toBe(0);
|
|
expect(knobValue(2, attack)).toBe(1);
|
|
});
|
|
|
|
it('turns up as it is dragged up, a tenth as far with Shift', () => {
|
|
expect(draggedValue(-50, -KNOB_TRAVEL / 2, false, threshold)).toBe(0);
|
|
expect(draggedValue(-50, KNOB_TRAVEL / 4, false, threshold)).toBe(-75);
|
|
expect(draggedValue(-50, -KNOB_TRAVEL / 2, true, threshold)).toBe(-45);
|
|
expect(draggedValue(-50, KNOB_TRAVEL * 10, false, threshold)).toBe(-100);
|
|
});
|
|
|
|
it('moves at least one step, however short the drag', () => {
|
|
expect(draggedValue(-50, -1, true, threshold)).toBe(-49.5);
|
|
expect(draggedValue(-50, 1, true, threshold)).toBe(-50.5);
|
|
expect(draggedValue(0, -1, true, threshold)).toBe(0);
|
|
expect(draggedValue(-50, 0, false, threshold)).toBe(-50);
|
|
});
|
|
|
|
it('notches up on the wheel turned up, and down on it turned down', () => {
|
|
expect(wheeledValue(-50, -100, threshold)).toBe(-48);
|
|
expect(wheeledValue(-50, 100, threshold)).toBe(-52);
|
|
expect(wheeledValue(0.003, -100, attack)).toBeGreaterThan(0.003);
|
|
});
|
|
|
|
it('fills its arc from 0 when the range has it inside, else from the low end', () => {
|
|
expect(knobOrigin(makeup)).toBeCloseTo(0.5, 9);
|
|
expect(knobOrigin(threshold)).toBe(0);
|
|
expect(knobOrigin(attack)).toBe(0);
|
|
});
|
|
});
|