/**
* Парити-тест спек-синтеза: renderSpec(SPEC_SFX[key]) должен совпадать с
* формулой бывшего файлового генератора (tools/audio/gen.mjs) — миграция
* на playSpec не изменила звучание. Агент без слуха, поэтому сверяем
* численно: bell_hit/whoosh бит-в-бит (формулы совпали дословно), остальные
* — с допуском 1e-5 (перегруппировка умножений даёт расхождение ~1e-15,
* допуск ловит любую реальную ошибку формулы: частоту, вес, огибающую).
*/
import { describe, expect, it } from 'vitest';
import { renderSpec } from '@rpg/engine';
import { bandNoise, decay, normalize, synth } from '@rpg/engine/tools/synth.mjs';
import {
ashDie,
ashHiss,
bellHit,
bellLow,
chestOpen,
chime,
deerStartle,
hurt,
motePickup,
note,
pickup,
spit,
uiClick,
whoosh
} from '../../../tools/audio/gen.mjs';
import { SPEC_SFX } from '../sfxSpecs';
/** Максимум модуля разности двух сэмпл-массивов. */
function maxDiff(a: Float32Array, b: Float32Array): number {
expect(a.length).toBe(b.length);
let max = 0;
for (let i = 0; i < a.length; i++) max = Math.max(max, Math.abs(a[i]! - b[i]!));
return max;
}
describe('паритет SPEC_SFX с формулами gen.mjs', () => {
it('sfx/bell_hit — бит-в-бит равен bellHit()', () => {
const spec = renderSpec(SPEC_SFX['sfx/bell_hit']!);
const wav = bellHit();
expect(spec.length).toBe(wav.length);
for (let i = 0; i < wav.length; i++) expect(spec[i]).toBe(wav[i]);
});
it('sfx/whoosh — бит-в-бит равен whoosh()', () => {
const spec = renderSpec(SPEC_SFX['sfx/whoosh']!);
const wav = whoosh();
expect(spec.length).toBe(wav.length);
for (let i = 0; i < wav.length; i++) expect(spec[i]).toBe(wav[i]);
});
const EPS = 1e-5;
const CASES: [string, () => Float32Array][] = [
['sfx/bell_low', bellLow],
['sfx/ash_hiss', ashHiss],
['sfx/ash_die', ashDie],
['sfx/spit', spit],
['sfx/hurt', hurt],
['sfx/mote_pickup', motePickup],
['sfx/pickup', pickup],
['sfx/chest_open', chestOpen],
['sfx/note', note],
['sfx/deer_startle', deerStartle],
['sfx/ui_click', uiClick],
['sfx/chime', chime]
];
for (const [key, gen] of CASES) {
it(`${key} — совпадает с формулой gen.mjs (eps ${EPS})`, () => {
expect(maxDiff(renderSpec(SPEC_SFX[key]!), gen())).toBeLessThan(EPS);
});
}
it('sfx/step_water — совпадает с формулой всплеска (eps)', () => {
// Формула бывшего sfx/step_water.wav (seed 255 в gen.mjs); у спека
// сид 255+... — нет, базовый спек шагов взял сиды StepVariants
// (253-256), поэтому здесь сверяем саму формулу с сидом 255.
const gen = (): Float32Array => {
const dur = 0.18;
const noise = bandNoise(155, 600, 3200, dur);
const s = synth(dur, (t) => (noise() * 0.6 + Math.sin(2 * Math.PI * 300 * t) * 0.2) * decay(t, dur, 2));
return normalize(s, 0.3);
};
const spec: typeof SPEC_SFX[string] = { ...SPEC_SFX['sfx/step_water']!, seed: 155 };
expect(maxDiff(renderSpec(spec), gen())).toBeLessThan(EPS);
});
});