/** * 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 { type CompressorNumbers, compressorGainDb, curveDbAtX, curveDbAtY, curveOutDb, curveX, curveY, HISTORY_SECONDS, LevelHistory, levelY, 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: 0, }; 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, the auto-gain or a blend of both as the gain after it', () => { expect(compressorGainDb({ ...numbers, makeup: 6 })).toBe(6); expect(compressorGainDb({ ...numbers, makeup: 6, autoGain: 1 })).toBeCloseTo(18, 1); expect(compressorGainDb({ ...numbers, makeup: 6, autoGain: 0.5 })).toBeCloseTo(12, 1); }); }); 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 }); history.push(1, null); expect(history.heard).toBe(true); history.push(1 + HISTORY_SECONDS, null); 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 }, { t: 1, in: -6, out: -12 }, { t: 2, in: -30, out: -30 }, { t: 3, in: 0, out: -3 }, { t: 4, in: 0, out: -3 }, ]; const shapes = reductionShapes(points, 0, 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]); }); });