/** * What the tab of a Compressor draws (#378): its curve and the handles on it, the ratio a drag of * the end of the curve asks for, and the level picture tinted where the compressor works. */ import { describe, expect, it } from 'vitest'; import { autoGainText, type CompressorNumbers, compressorGainDb, compressorNumbers, curveDbAtX, curveDbAtY, curveOutDb, curveX, curveY, HISTORY_SECONDS, LevelHistory, levelY, MAX_CURVE, MIN_CURVE, picturesLayout, ratioFor, ratioHandle, reductionOf, reductionShapes, thresholdHandle, } from '../../src/editor/soundfx/compressorView'; const numbers: CompressorNumbers = { threshold: -24, knee: 0, ratio: 4, attack: 0.003, release: 0.25, makeup: 0, autoGain: false, }; const RATIO = { min: 1, max: 20, step: 0.1 }; describe('the curve of a Compressor', () => { it('leaves a level below the threshold as it is and squeezes one above it by the ratio', () => { expect(curveOutDb(-40, -24, 0, 4)).toBeCloseTo(-40, 6); // 24 dB over a threshold of -24 come out 6 dB over it at a ratio of 4. expect(curveOutDb(0, -24, 0, 4)).toBeCloseTo(-18, 1); }); it('spans -60 to 0 dB on both axes, level out going up', () => { expect(curveX(-60, 300)).toBe(0); expect(curveX(0, 300)).toBe(300); expect(curveY(0, 300)).toBe(0); expect(curveDbAtX(150, 300)).toBe(-30); expect(curveDbAtY(100, 300)).toBe(-20); }); it('has the threshold where it starts to bend and the ratio at its end', () => { expect(thresholdHandle(numbers, 300)).toEqual({ x: 180, y: 120 }); const end = ratioHandle(numbers, 300); expect(end.x).toBe(300); expect(curveDbAtY(end.y, 300)).toBeCloseTo(-18, 1); }); it('finds the ratio whose end is where it is dragged to, held to the range', () => { expect(ratioFor(-18, numbers, RATIO)).toBe(4); expect(ratioFor(-12, numbers, RATIO)).toBe(2); expect(ratioFor(5, numbers, RATIO)).toBe(1); expect(ratioFor(-40, numbers, RATIO)).toBe(20); // With a knee the curve ends lower than the straight line would, and the ratio found still lands there. const kneed = { ...numbers, knee: 6 }; const ratio = ratioFor(-16, kneed, RATIO); expect(curveOutDb(0, -24, 6, ratio)).toBeCloseTo(-16, 0); }); it('counts the makeup, and the auto-gain on top of it where it is on, as the gain after it', () => { expect(compressorGainDb({ ...numbers, makeup: 6 })).toBe(6); // -18 dB in comes out at -22.5: auto-gain gives the 4.5 dB back. expect(compressorGainDb({ ...numbers, makeup: 6, autoGain: true })).toBeCloseTo(10.5, 3); }); }); describe('the numbers of a Compressor node (#380)', () => { it('takes what the node has set, its defaults where it has none or no number, and reads an old auto-gain', () => { expect(compressorNumbers({ threshold: -30, ratio: 'x', autoGain: 0.5 })).toEqual({ threshold: -30, ratio: 4, knee: 6, attack: 0.003, release: 0.25, makeup: 0, autoGain: true, }); expect(compressorNumbers({}).autoGain).toBe(false); }); it('says what auto-gain gives, or that it is off', () => { expect(autoGainText({ ...numbers, autoGain: false })).toBe('off'); expect(autoGainText({ ...numbers, autoGain: true })).toMatch(/^\+\d+\.\d dB$/); }); }); describe('the level picture of a Compressor', () => { it('spans +6 to -60 dB top to bottom, silence at the bottom', () => { expect(levelY(6, 660)).toBe(0); expect(levelY(-60, 660)).toBe(660); expect(levelY(-Infinity, 660)).toBe(660); expect(levelY(0, 660)).toBe(60); }); it('keeps the readings of the last seconds and knows when all of them are silent', () => { const history = new LevelHistory(); history.push(0, { in: -6, out: -9 }, 0); history.push(1, null, 0); expect(history.heard).toBe(true); history.push(1 + HISTORY_SECONDS, null, 0); expect(history.all.map((p) => p.t)).toEqual([1, 1 + HISTORY_SECONDS]); expect(history.heard).toBe(false); }); it('takes away what came in less what went out, the gain after it left out', () => { expect(reductionOf({ in: -6, out: -12 }, 0)).toBe(6); expect(reductionOf({ in: -6, out: -6 }, 6)).toBe(6); expect(reductionOf({ in: -30, out: -29 }, 0)).toBe(0); expect(reductionOf({ in: -Infinity, out: -Infinity }, 0)).toBe(0); }); it('tints from the level coming in down by what is taken away, one shape per stretch', () => { const points = [ { t: 0, in: -6, out: -12, gain: 0 }, { t: 1, in: -6, out: -12, gain: 0 }, { t: 2, in: -30, out: -30, gain: 0 }, { t: 3, in: 0, out: -3, gain: 0 }, { t: 4, in: 0, out: -3, gain: 0 }, ]; const shapes = reductionShapes(points, 4, 400, 660); expect(shapes).toHaveLength(2); // Along the level coming in, then back along it less what is taken away. expect(shapes[0]).toEqual([0, 120, 100, 120, 100, 180, 0, 180]); expect(shapes[1]).toEqual([300, 60, 400, 60, 400, 90, 300, 90]); }); it('reads every reading by the gain after the compressing as it was then', () => { // Raised by 20 dB until a second ago, by 0 since: the same 6 dB taken away both times. const history = new LevelHistory(); history.push(0, { in: -24, out: -10 }, 20); history.push(1, { in: -24, out: -10 }, 20); history.push(2, { in: -24, out: -30 }, 0); history.push(3, { in: -24, out: -30 }, 0); const shapes = reductionShapes(history.all, 3, 300, 660); expect(shapes).toHaveLength(1); expect(shapes[0]).toHaveLength(16); expect(shapes[0]!.filter((_, i) => i % 2 === 1)).toEqual([300, 300, 300, 300, 360, 360, 360, 360]); }); }); describe('the pictures in the space of the tab', () => { it('lets the levels fill the height and keeps the curve square, centred up and down', () => { const at = picturesLayout(1600, 1000, 8); expect(at.curve).toEqual({ x: 1600 - MAX_CURVE, y: (1000 - MAX_CURVE) / 2, side: MAX_CURVE }); expect(at.levels).toEqual({ w: 1600 - MAX_CURVE - 8, h: 1000 }); }); it('grows the curve with a lower tab, and holds it at its least in a narrow one', () => { expect(picturesLayout(1600, 250, 8).curve).toEqual({ x: 1350, y: 0, side: 250 }); expect(picturesLayout(500, 400, 8).curve.side).toBe(200); expect(picturesLayout(300, 400, 8).curve.side).toBe(MIN_CURVE); }); });