The Chip had eleven numbers in rows and nothing that showed what they do to a note. Its node is now a device of one size like the Compressor's, and two screens draw the wave and the pitch from the settings, worked right in the instrument graph. - ChipPanel is 840x280 in the warm grey of a handheld console. A button per wave, the "follow note" lever and a chrome knob per number, grouped Oscillator, Slide, Vibrato, Arpeggio and Repeat. - The wave screen draws two periods as loud as the Chip plays them, a square at its duty. toneValue and periodicSteps come out of chip.ts, so the screen and the worklet share one formula. - The pitch screen draws what slide, delta slide, vibrato, arpeggio and repeat make of the pitch over a note. It ends where the note falls below "min frequency" and starts from A4 while the Chip follows the note. - Idle settings are faded, wired ones greyed and locked, as the rows were. Every change is one undo step. - Knobs take a bipolar range: 0 straight up, the taper bends each half. Slide, delta slide, duty sweep and arpeggio use it. - The Compressor's auto-gain lever is now a Lever with a name of its own, which the Chip uses for "follow note". - Presets give a Chip five columns, as a Filter has four since #385. Found on the way: - With the Chip this wide the Laser preset is about 3060 units. In a 1280 px window Fit stops at the least zoom of 0.25 and cuts the Notes node off. The zoom limit is left as it is. Tests: - Unit: chip-view (9 new), knob (1 new), chip, chip-presets and hardware-view, 53 green - e2e 34 on melektaus-atx: ALL PASSED, 131 checks (1 new). The first run was red on the check itself: a Laser's "min frequency" can lie above A4, and the screen then rightly says the note ends at once. - Gate on melektaus-atx: CHECK EXIT 0, 285 files, 4377 tests - Counter-check: a square drawn with its duty mirrored turns the wave test red Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
71 lines
3.0 KiB
TypeScript
71 lines
3.0 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('has 0 straight up on a bipolar range, and gives the small numbers either way most of the turn (#386)', () => {
|
|
const slide = { min: -2000, max: 2000, step: 1, taper: 3, bipolar: true };
|
|
expect(knobPosition(0, slide)).toBe(0.5);
|
|
expect(knobPosition(2000, slide)).toBe(1);
|
|
expect(knobPosition(-2000, slide)).toBe(0);
|
|
// 250 st/s, an eighth of the reach, is halfway up the right half; even it would be at 0.5625.
|
|
expect(knobPosition(250, slide)).toBeCloseTo(0.75, 9);
|
|
expect(knobPosition(-250, slide)).toBeCloseTo(0.25, 9);
|
|
expect(knobValue(0.75, slide)).toBe(250);
|
|
expect(knobValue(0.5, slide)).toBe(0);
|
|
for (const value of [-1200, -7, 0, 3, 640])
|
|
expect(knobValue(knobPosition(value, slide), slide)).toBe(value);
|
|
expect(knobOrigin(slide)).toBe(0.5);
|
|
expect(draggedValue(0, -1, true, slide)).toBe(1);
|
|
});
|
|
|
|
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);
|
|
});
|
|
});
|