/** * 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); }); });