Open a Compressor as a device in a window of one size (#379)
The tab from #378 shows a Compressor as an editor. The user wanted it to look like a device as well, after a brushed aluminium plug-in, with a meter of the gain reduction instead of the pictures. The tab stays and is switched to per node. - Hardware look is a switch under Settings, on by default for new and existing Compressors. Only the editor reads it. - The panel is 820x280 px and opens straight in a window of that size. The window has no sizing corner and no Dock. A pane says so through `fixedSize`, and `keptSize` keeps the window's middle when it changes to that size. - A Compressor in this look never sits in the tab row. Opening one floats it, and switching the look on moves its tab into the window. - The panel has chrome knobs (threshold and makeup large), a meter with a needle that moves like a VU's, an LCD with the name and the reading, a lever with a lamp for auto-gain, and LEDs for the level going out. Everything is drawn in code. - The Knob takes a size and a chrome look, and leaves out its caption when it has no name, so the panel engraves its own. - `autoGainOn` is now `switchOn`, because the new switch reads the same way. - Where parts sit, the meter's scale, the needle's speed, the LEDs and the plate's grain are in `hardwareView.ts`, apart from the drawing. Decided myself: - The meter's scale runs by the square root, so the first dB is about as wide as the last nine. It is red from 10 dB on. - The LEDs light one every 3 dB from -39 to 0 dB and fall by 20 dB a second. - Switching the look changes the graph's fingerprint, like any other setting of a node. The sound FX is built again once. Findings, fixed on the way: - Pixi's `arc` goes on from the last point of the path. The meter's scale had a line from the corner of the face until each arc got a `moveTo` to its own start. - A text put into a Graphics makes Pixi warn that only Containers take children. The LCD is a Container now. - The first browser check read the needle at one moment and missed the short sound. The panel keeps the most its needle and LEDs showed. Tests: - Unit: hardware-view (10 new), documents (1 new) - e2e 34 (melektaus-atx): ALL PASSED, 122 checks (4 new). Played, the needle reached 20.9 dB and 5 LEDs lit. - Gate (melektaus-atx): CHECK EXIT 0, 283 files, 4355 tests (11 new) - Counter-checks: a window that keeps its corner instead of its middle, and a linear meter scale, each turn exactly their test red. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2d4909d248
commit
9917253751
14 files changed
+1153
-52
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { closeDocument, neighbourOf, openDocument, replaceDocument } from '../../src/editor/documents';
|
||||
import {
|
||||
keptSize,
|
||||
leastBox,
|
||||
MIN_WINDOW_HEIGHT,
|
||||
MIN_WINDOW_WIDTH,
|
||||
@@ -104,6 +105,13 @@ describe('a tab in a window (#371)', () => {
|
||||
expect(onScreen({ ...box, w: 400, h: 300 }, 1280, 800, least)).toEqual({ ...box, w: 701, h: 450 });
|
||||
expect(onScreen({ ...box, w: 400, h: 300 }, 600, 400, least)).toEqual({ x: 0, y: 0, w: 600, h: 400 });
|
||||
});
|
||||
|
||||
it('takes the size of a pane of one size around the middle it had, and keeps a box of that size (#379)', () => {
|
||||
const box = { x: 100, y: 100, w: 800, h: 600 };
|
||||
expect(keptSize(box, { w: 400, h: 200 })).toEqual({ x: 300, y: 300, w: 400, h: 200 });
|
||||
const kept = { x: 7, y: 9, w: 400, h: 200 };
|
||||
expect(keptSize(kept, { w: 400, h: 200 })).toBe(kept);
|
||||
});
|
||||
});
|
||||
|
||||
describe('tab row without an active tab', () => {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
type Box,
|
||||
brushStrokes,
|
||||
gainReduction,
|
||||
knobBox,
|
||||
LED_COUNT,
|
||||
ledColour,
|
||||
ledDb,
|
||||
ledsLit,
|
||||
ledStep,
|
||||
METER_LABELS,
|
||||
METER_RANGE_DB,
|
||||
meterPosition,
|
||||
needleAngle,
|
||||
needleStep,
|
||||
PANEL,
|
||||
PANEL_LAYOUT,
|
||||
} from '../../src/editor/soundfx/hardwareView';
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user