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>
57 lines
2.4 KiB
TypeScript
57 lines
2.4 KiB
TypeScript
/**
|
||
* The rules of a Knob (#378): where on its turn a value sits, and what a drag or a wheel makes of
|
||
* it. A turn is a position from 0 to 1; `taper` bends it, so a range whose useful part is small –
|
||
* an attack of a few milliseconds out of a second – gets most of the turn.
|
||
*/
|
||
import { clamp, snapToStep } from './math';
|
||
|
||
export interface KnobRange {
|
||
min: number;
|
||
max: number;
|
||
/** The grid a value snaps to. */
|
||
step: number;
|
||
/** 1 turns evenly; above it the low end gets more of the turn. */
|
||
taper?: number;
|
||
}
|
||
|
||
/** Pixels of a vertical drag that take the knob from one end to the other. */
|
||
export const KNOB_TRAVEL = 200;
|
||
/** How much of the turn Shift leaves to a drag. */
|
||
export const KNOB_FINE = 0.1;
|
||
/** How far one notch of the wheel turns the knob. */
|
||
export const KNOB_NOTCH = 0.02;
|
||
/** The turn as an angle: from down-left round over the top to down-right, in radians. */
|
||
export const KNOB_START = Math.PI * 0.75;
|
||
export const KNOB_SWEEP = Math.PI * 1.5;
|
||
|
||
export function knobPosition(value: number, range: KnobRange): number {
|
||
const share = clamp((value - range.min) / (range.max - range.min), 0, 1);
|
||
return share ** (1 / (range.taper ?? 1));
|
||
}
|
||
|
||
export function knobValue(position: number, range: KnobRange): number {
|
||
const share = clamp(position, 0, 1) ** (range.taper ?? 1);
|
||
return clamp(snapToStep(range.min + share * (range.max - range.min), range.step), range.min, range.max);
|
||
}
|
||
|
||
/**
|
||
* The value a vertical drag reaches from `start`: up turns it up. A drag too short to reach the
|
||
* next step still moves it by one, so a fine drag over a coarse range is never stuck.
|
||
*/
|
||
export function draggedValue(start: number, dy: number, fine: boolean, range: KnobRange): number {
|
||
const travel = (-dy / KNOB_TRAVEL) * (fine ? KNOB_FINE : 1);
|
||
const value = knobValue(knobPosition(start, range) + travel, range);
|
||
if (value !== start || travel === 0) return value;
|
||
return clamp(snapToStep(start + Math.sign(travel) * range.step, range.step), range.min, range.max);
|
||
}
|
||
|
||
/** The value one notch of the wheel reaches: up turns it up, by at least one step. */
|
||
export function wheeledValue(value: number, deltaY: number, range: KnobRange): number {
|
||
return draggedValue(value, Math.sign(deltaY) * KNOB_NOTCH * KNOB_TRAVEL, false, range);
|
||
}
|
||
|
||
/** Where the filled arc starts: at 0 when the range has it inside, else at the low end. */
|
||
export function knobOrigin(range: KnobRange): number {
|
||
return range.min < 0 && range.max > 0 ? knobPosition(0, range) : 0;
|
||
}
|