/** The curves of a Saturation (#323) and the gain that keeps it as loud as it came in. */ import { describe, expect, it } from 'vitest'; import { driveGain, SATURATION_RANGE, SATURATION_TYPES, saturate, saturationAutoGain, saturationCurve, toneCutoff, } from '../../src/core/soundfx/saturation'; describe('the curves of a Saturation', () => { it('leave silence silent and never go past full scale, however hard they are driven', () => { for (const { value } of SATURATION_TYPES) { expect(saturate(value, 0)).toBeCloseTo(0, 12); for (const x of [-300, -5, -1, -0.3, 0.3, 1, 5, 300]) expect(Math.abs(saturate(value, x))).toBeLessThanOrEqual(1); } }); it('bend each in a shape of their own', () => { expect(saturate('hard', 0.5)).toBe(0.5); expect(saturate('hard', 3)).toBe(1); expect(saturate('cubic', 1)).toBe(1); expect(saturate('cubic', 0.5)).toBeCloseTo(0.6875, 9); // Foldback mirrors at ±1: 1.5 comes back to 0.5, 3 to -1. expect([saturate('foldback', 0.5), saturate('foldback', 1.5), saturate('foldback', 3)]).toEqual([ 0.5, 0.5, -1, ]); expect(saturate('sine', 1)).toBeCloseTo(1, 12); expect(saturate('sine', 2)).toBeCloseTo(0, 12); // Tube and Fuzz bend one half harder than the other; Rectify turns the lower half up. expect(saturate('tube', 2)).not.toBeCloseTo(-saturate('tube', -2), 2); expect(saturate('fuzz', -10)).toBeCloseTo(-0.5, 9); expect(saturate('rectify', -0.5)).toBeCloseTo(saturate('rectify', 0.5), 12); }); it('are made once, spanning the range a drive of 48 dB reaches', () => { const curve = saturationCurve('hard'); expect(saturationCurve('hard')).toBe(curve); expect(curve.length % 2).toBe(1); expect([curve[0], curve[(curve.length - 1) / 2], curve.at(-1)]).toEqual([-1, 0, 1]); expect(10 ** (48 / 20)).toBeLessThan(SATURATION_RANGE); expect(driveGain(0)).toBe(1 / SATURATION_RANGE); }); }); describe('the auto-gain of a Saturation', () => { it('leaves a curve that does nothing at -12 dBFS alone, and makes up for what a hard one takes', () => { expect(saturationAutoGain('hard', 0)).toBeCloseTo(0, 9); // Driven 24 dB into a hard clip, a sine is nearly a square at full scale: 3 dB above the // sine's 0 dBFS RMS, which is 12 dB over -12 dBFS. 15 dB come off. expect(saturationAutoGain('hard', 24)).toBeCloseTo(-15, 0); expect(saturationAutoGain('soft', 0)).toBeGreaterThan(0); expect(saturationAutoGain('soft', 0)).toBeLessThan(0.3); }); it('opens the tone from 1 kHz to 20 kHz', () => { expect([toneCutoff(0), toneCutoff(1), toneCutoff(5)]).toEqual([1000, 20000, 20000]); expect(toneCutoff(0.5)).toBeCloseTo(1000 * Math.sqrt(20), 6); }); });