Files
feuerwerk/tests/editor/hardware-view.test.ts
T
adminandClaude Opus 5.5 389d9555fa
game-maker / gate (push) Successful in 4m13s
game-maker / suite (push) Failing after 15m23s
Show a Pan node as a one-knob device in cyan (#392)
After the Gain, the Pitch and the Mix, the Pan is the next node that
is just one number. It now gets the same device in the graph instead
of a row for its pan.

- PAN_DEVICE: the Gain's 160x160 plate in cyan with one bipolar knob
  for Pan. The middle stands straight up, and the turn goes evenly to
  L and R, which are engraved at its ends like A and B on the Mix.
- PAN_TAPER in hardwareView is 1, so left and right are heard alike.
  Device and unit test read the same constant.
- PAN_ROW is exported from the core, so the knob has the node's range.
- A linked Pan greys the knob, as for the Gain.
- The test hook __gm.pan.panel returns the same shape as the Gain's.
- No instrument preset uses a Pan, so no preset columns change.

Tests:
- Unit hardware-view: 17 tests, 2 new (the turn, and L and R inside
  the plate and clear of the screws). Counter-check: PAN_TAPER 2
  turns the turn test red.
- e2e 34 (melektaus-atx): 135 checks, 1 new.
- Gate (melektaus-atx): 287 files, 4399 tests.
- Pictures looked at: Gain, Pitch, Mix and Pan in a row, Pan at -0.35.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 18:26:17 +02:00

251 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* The Compressor's hardware panel (#379): every part on the plate and none over another, the meter's
* scale and needle, the LEDs, and the grain of the plate the same every time. The Gain's plate
* (#384) and how its one knob turns, the Pitch's knob and the Mix's lever and ends (#390), the
* Pan's knob and ends on the Gain's plate (#392).
*/
import { describe, expect, it } from 'vitest';
import {
type Box,
brushStrokes,
CAPTION_AIR,
GAIN_LAYOUT,
KNOB_CAPTION,
GAIN_PANEL,
GAIN_TAPER,
gainReduction,
knobBox,
knobEnds,
LED_COUNT,
ledColour,
ledDb,
ledsLit,
ledStep,
METER_LABELS,
METER_RANGE_DB,
meterPosition,
MIX_LAYOUT,
MIX_PANEL,
needleAngle,
needleStep,
PAN_TAPER,
PANEL,
PANEL_LAYOUT,
PITCH_TAPER,
} from '../../src/editor/soundfx/hardwareView';
import { GAIN_ROW, PAN_ROW, PITCH_ROW } from '../../src/core/soundfx/nodes';
import { knobPosition } from '../../src/ui/knob';
const overlap = (a: Box, b: Box) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
describe('the plate', () => {
const L = PANEL_LAYOUT;
const parts: [string, Box][] = [
['title', L.title],
['lcd', L.lcd],
['autoGain', L.autoGain],
['meter', L.meter],
['leds', L.leds],
...Object.entries(L.knobs).map(([key, spot]): [string, Box] => [key, knobBox(spot)]),
];
it('holds every part inside it, clear of the screws', () => {
for (const [, box] of parts) {
expect(box.x).toBeGreaterThanOrEqual(0);
expect(box.y).toBeGreaterThanOrEqual(0);
expect(box.x + box.w).toBeLessThanOrEqual(PANEL.w);
expect(box.y + box.h).toBeLessThanOrEqual(PANEL.h);
for (const screw of L.screws)
expect(overlap(box, { x: screw.x - 7, y: screw.y - 7, w: 14, h: 14 })).toBe(false);
}
});
it('puts no part over another', () => {
const over = parts.flatMap(([a, boxA], i) =>
parts.slice(i + 1).flatMap(([b, boxB]) => (overlap(boxA, boxB) ? [`${a}/${b}`] : [])),
);
expect(over).toEqual([]);
});
it('has the big knobs at its ends and the small ones beside the meter', () => {
const { threshold, makeup, ratio, attack } = L.knobs;
expect(threshold.size).toBeGreaterThan(ratio.size);
expect(threshold.x).toBeLessThan(ratio.x);
expect(ratio.x).toBeLessThan(L.meter.x);
expect(attack.x).toBeGreaterThan(L.meter.x + L.meter.w);
expect(makeup.x).toBeGreaterThan(attack.x);
});
it('is brushed the same every time, a stroke per row', () => {
const strokes = brushStrokes(PANEL.h);
expect(strokes).toHaveLength(PANEL.h);
expect(brushStrokes(PANEL.h)).toEqual(strokes);
const alphas = strokes.map((s) => s.alpha);
expect(Math.max(...alphas) - Math.min(...alphas)).toBeGreaterThan(0.07);
expect(strokes.some((s) => s.light) && strokes.some((s) => !s.light)).toBe(true);
});
});
describe('the meter', () => {
it('reads 0 dB at the right end, the range at the left, and the first dB about as wide as the last nine', () => {
expect(meterPosition(0)).toBe(1);
expect(meterPosition(METER_RANGE_DB)).toBe(0);
expect(meterPosition(40)).toBe(0);
expect(meterPosition(-3)).toBe(1);
expect(1 - meterPosition(1)).toBeCloseTo(meterPosition(15) - meterPosition(24), 1);
const spots = METER_LABELS.map(meterPosition);
for (let i = 1; i < spots.length; i++) expect(spots[i]).toBeLessThan(spots[i - 1]!);
});
it('swings the needle symmetrically about straight up', () => {
expect(needleAngle(0.5)).toBeCloseTo(-Math.PI / 2);
expect(needleAngle(0) + needleAngle(1)).toBeCloseTo(-Math.PI);
expect(needleAngle(1)).toBeGreaterThan(needleAngle(0));
});
it('moves the needle as a VU: most of the way in 300 ms, and there in the end', () => {
let shown = 0;
for (let t = 0; t < 0.3; t += 1 / 60) shown = needleStep(shown, 10, 1 / 60);
expect(shown).toBeGreaterThan(9);
expect(shown).toBeLessThan(10);
expect(needleStep(0, 10, 1 / 60)).toBeLessThan(3);
for (let i = 0; i < 60; i++) shown = needleStep(shown, 10, 1 / 60);
expect(shown).toBe(10);
// A long pause between two frames moves it no further than a quarter of a second does.
expect(needleStep(5, 0, 10)).toBe(needleStep(5, 0, 0.25));
});
it('takes the gain after the compressing out of what it reads, and nothing from silence', () => {
expect(gainReduction({ in: -10, out: -6 }, 10)).toBeCloseTo(6);
expect(gainReduction({ in: -10, out: -10 }, 0)).toBe(0);
expect(gainReduction({ in: -10, out: -2 }, 0)).toBe(0);
expect(gainReduction({ in: -Infinity, out: -Infinity }, 0)).toBe(0);
expect(gainReduction(null, 0)).toBe(0);
});
});
describe('the LEDs', () => {
it('light one every 3 dB up to 0, red at the top and green at the bottom', () => {
expect(ledDb(LED_COUNT - 1)).toBe(0);
expect(ledDb(LED_COUNT - 2)).toBe(-3);
expect(ledColour(LED_COUNT - 1)).toBe('red');
expect(ledColour(LED_COUNT - 2)).toBe('red');
expect(ledColour(LED_COUNT - 4)).toBe('amber');
expect(ledColour(0)).toBe('green');
expect(ledsLit(-Infinity)).toBe(0);
expect(ledsLit(0)).toBe(LED_COUNT);
expect(ledsLit(-4)).toBe(LED_COUNT - 2);
});
it('go up at once and down by 20 dB a second', () => {
expect(ledStep(-30, -6, 1 / 60)).toBe(-6);
expect(ledStep(-6, -30, 0.1)).toBeCloseTo(-8);
expect(ledStep(-6, -Infinity, 10)).toBe(-11);
});
});
describe('the Gain (#384)', () => {
const L = GAIN_LAYOUT;
const knob = knobBox(L.knob);
it('holds its knob in the middle of the plate, clear of the screws', () => {
expect(knob.x).toBeGreaterThanOrEqual(0);
expect(knob.x + knob.w).toBeLessThanOrEqual(GAIN_PANEL.w);
// Its number hangs inside the dial (#382): from the caption down to the dial's edge is all it shows.
const top = L.knob.y - L.knob.size / 2 - KNOB_CAPTION;
const bottom = L.knob.y + L.knob.size / 2;
expect(top).toBeGreaterThan(0);
expect(top + CAPTION_AIR).toBe(GAIN_PANEL.h - bottom);
expect(knob.x + knob.w / 2).toBe(GAIN_PANEL.w / 2);
const shown = { ...knob, y: top, h: bottom - top };
for (const screw of L.screws)
expect(overlap(shown, { x: screw.x - 7, y: screw.y - 7, w: 14, h: 14 })).toBe(false);
});
it('turns 0 dB straight up and the quiet tail by in a fifth of the turn', () => {
const range = { min: GAIN_ROW.min ?? 0, max: GAIN_ROW.max ?? 0, step: 0.5, taper: GAIN_TAPER };
expect(knobPosition(0, range)).toBeCloseTo(0.5, 2);
expect(knobPosition(-24, range)).toBeLessThan(0.22);
expect(knobPosition(-6, range)).toBeGreaterThan(0.4);
});
});
describe('the Pitch and the Mix (#390)', () => {
it('turns the Pitch from 0 straight up, an octave either way alike and well inside the turn', () => {
const range = {
min: PITCH_ROW.min ?? 0,
max: PITCH_ROW.max ?? 0,
step: 0.1,
taper: PITCH_TAPER,
bipolar: true,
};
expect(knobPosition(0, range)).toBeCloseTo(0.5, 5);
const up = knobPosition(12, range);
expect(up - 0.5).toBeCloseTo(0.5 - knobPosition(-12, range), 5);
expect(up).toBeGreaterThan(0.7);
expect(up).toBeLessThan(0.75);
expect(knobPosition(64, range)).toBe(1);
});
it('holds the Mix’s lever above the knob, centred and clear of it and of the screws', () => {
const lever = MIX_LAYOUT.curve;
const spot = MIX_LAYOUT.knob;
const knob = knobBox(spot);
expect(lever.x).toBeGreaterThanOrEqual(0);
expect(lever.x + lever.w).toBeLessThanOrEqual(MIX_PANEL.w);
expect(lever.x + lever.w / 2).toBe(MIX_PANEL.w / 2);
expect(spot.x).toBe(MIX_PANEL.w / 2);
// A lever that is down reaches 8 below its box (`Lever.redraw`).
expect(lever.y + lever.h + 8).toBeLessThanOrEqual(knob.y);
for (const screw of MIX_LAYOUT.screws)
expect(overlap(lever, { x: screw.x - 7, y: screw.y - 7, w: 14, h: 14 })).toBe(false);
// Below the dial as much air as on the Gain.
expect(MIX_PANEL.h - (spot.y + spot.size / 2)).toBe(GAIN_PANEL.h - (GAIN_LAYOUT.knob.y + spot.size / 2));
expect(MIX_PANEL.w).toBeLessThan(MIX_PANEL.h);
});
it('names the ends of the Mix’s turn just outside the dial, left and right below its middle', () => {
const spot = MIX_LAYOUT.knob;
const { a, b } = knobEnds(spot);
for (const end of [a, b]) {
expect(Math.hypot(end.x - spot.x, end.y - spot.y)).toBeGreaterThan(spot.size / 2);
expect(end.y).toBeGreaterThan(spot.y);
expect(end.y).toBeLessThan(MIX_PANEL.h);
}
expect(a.x).toBeLessThan(spot.x);
expect(b.x).toBeGreaterThan(spot.x);
expect(a.y).toBe(b.y);
});
});
describe('the Pan (#392)', () => {
it('turns from the middle straight up, as far left as right, the whole turn for -1 to 1', () => {
const range = {
min: PAN_ROW.min ?? 0,
max: PAN_ROW.max ?? 0,
step: 0.01,
taper: PAN_TAPER,
bipolar: true,
};
expect(knobPosition(0, range)).toBeCloseTo(0.5, 5);
expect(knobPosition(0.5, range)).toBeCloseTo(0.75, 5);
expect(knobPosition(-0.5, range)).toBeCloseTo(0.25, 5);
expect(knobPosition(-1, range)).toBe(0);
expect(knobPosition(1, range)).toBe(1);
});
it('names L and R at the ends of the turn inside the Gain’s plate, clear of the screws', () => {
const spot = GAIN_LAYOUT.knob;
const { a, b } = knobEnds(spot);
for (const end of [a, b]) {
expect(end.x).toBeGreaterThan(0);
expect(end.x).toBeLessThan(GAIN_PANEL.w);
expect(end.y).toBeLessThan(GAIN_PANEL.h);
for (const screw of GAIN_LAYOUT.screws)
expect(Math.hypot(end.x - screw.x, end.y - screw.y)).toBeGreaterThan(14);
}
expect(a.x).toBeLessThan(spot.x);
expect(b.x).toBeGreaterThan(spot.x);
});
});