import { describe, expect, it } from 'vitest'; import { PRESETS, createPresetBuffer } from '../src/audio/presets'; import { MockAudioContext } from './mockAudioContext'; const ctx = new MockAudioContext() as unknown as BaseAudioContext; describe('sound presets', () => { it('offers a labelled, explained option for every id', () => { for (const preset of PRESETS) { expect(preset.label.length).toBeGreaterThan(0); expect(preset.hint.length).toBeGreaterThan(0); } expect(new Set(PRESETS.map((p) => p.id)).size).toBe(PRESETS.length); }); it('renders every preset as finite, audible, unclipped audio', () => { for (const preset of PRESETS) { const buffer = createPresetBuffer(ctx, preset.id); expect(buffer.length).toBeGreaterThan(1000); const data = buffer.getChannelData(0); let peak = 0; let energy = 0; for (let i = 0; i < data.length; i++) { expect(Number.isFinite(data[i])).toBe(true); peak = Math.max(peak, Math.abs(data[i])); energy += data[i] * data[i]; } expect(peak).toBeGreaterThan(0.05); expect(peak).toBeLessThanOrEqual(1); expect(energy).toBeGreaterThan(0); } }); it('falls back to a known preset for an unrecognised id', () => { expect(createPresetBuffer(ctx, 'nonsense' as never).length).toBeGreaterThan(0); }); it('loops without a discontinuity', () => { for (const preset of PRESETS) { const data = createPresetBuffer(ctx, preset.id).getChannelData(0); // Measure against the 99.9th percentile of the internal sample-to-sample // steps, never the maximum. A bug that injects one discontinuity would // raise the maximum and so raise the bar it is being measured against — // it would manufacture its own alibi. const steps = new Float64Array(data.length - 1); for (let i = 1; i < data.length; i++) steps[i - 1] = Math.abs(data[i] - data[i - 1]); steps.sort(); const typical = steps[Math.floor(steps.length * 0.999)]; const seam = Math.abs(data[data.length - 1] - data[0]); expect(seam).toBeLessThanOrEqual(typical * 2 + 0.02); } }); it('introduces no discontinuity of its own at the crossfade boundary', () => { // The wrap-around crossfade must not swap a seam at index 0 for a seam at // the end of the fade region. for (const preset of ['pink', 'engine'] as const) { const data = createPresetBuffer(ctx, preset).getChannelData(0); const steps = new Float64Array(data.length - 1); for (let i = 1; i < data.length; i++) steps[i - 1] = Math.abs(data[i] - data[i - 1]); const sorted = Float64Array.from(steps).sort(); const typical = sorted[Math.floor(sorted.length * 0.999)]; let worst = 0; for (const step of steps) worst = Math.max(worst, step); expect(worst).toBeLessThanOrEqual(typical * 3); } }); it('band-limits the harmonic waveforms so Doppler cannot alias them', () => { // A naive sawtooth carries energy right up to Nyquist; pitching it up folds // the top harmonics back down as inharmonic noise. Building the wave from a // bounded harmonic series leaves an octave of clean headroom instead. const rate = 48000; const fundamental = 165; const data = createPresetBuffer(ctx, 'sawtooth').getChannelData(0); const inBand = magnitudeAt(data, fundamental * 20, rate); const nearLimit = magnitudeAt(data, fundamental * 70, rate); const aboveLimit = magnitudeAt(data, fundamental * 90, rate); expect(inBand).toBeGreaterThan(0); // Harmonics run out below rate/4, leaving room for a 2x Doppler shift. expect(nearLimit).toBeGreaterThan(aboveLimit * 20); expect(aboveLimit).toBeLessThan(inBand * 0.02); }); }); /** Single-bin DFT magnitude, for probing one frequency without a full FFT. */ function magnitudeAt(data: Float32Array, frequency: number, sampleRate: number): number { const omega = (2 * Math.PI * frequency) / sampleRate; let real = 0; let imaginary = 0; for (let i = 0; i < data.length; i++) { real += data[i] * Math.cos(omega * i); imaginary += data[i] * Math.sin(omega * i); } return Math.hypot(real, imaginary) / data.length; }