import { describe, expect, it } from 'vitest';
import { writeFileSync, readFileSync, mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { encodeWav, decodeWav } from '../wav.mjs';
import { spectrogram, envelope, spectroSheet } from '../spectro.mjs';
/** Синус 440 Гц амплитуды 0.5 (2 с — за пределами окна анализа). */
const tone = (rate = 22050) => {
const n = rate * 2;
const s = new Float32Array(n);
for (let i = 0; i < n; i++) s[i] = 0.5 * Math.sin((2 * Math.PI * 440 * i) / rate);
return s;
};
describe('decodeWav', () => {
it('читает обратно то, что написал encodeWav', () => {
const src = tone();
const { samples, rate } = decodeWav(encodeWav(src, 22050));
expect(rate).toBe(22050);
expect(samples.length).toBe(src.length);
// 16-бит квантование: расхождение с оригиналом микроскопическое.
let err = 0;
for (let i = 0; i < samples.length; i++) err = Math.max(err, Math.abs(samples[i] - src[i]));
expect(err).toBeLessThan(1e-4);
});
it('падает с внятной ошибкой на не-WAV', () => {
expect(() => decodeWav(Buffer.from('not a wav file at all'))).toThrow(/не WAV/);
});
});
describe('spectrogram/envelope', () => {
it('пик спектрограммы тона попадает на его частоту', () => {
const rate = 22050;
const spec = spectrogram(tone(rate), { win: 1024, hop: 512 });
expect(spec.cols).toBeGreaterThan(0);
expect(Number.isFinite(spec.max)).toBe(true);
// 440 Гц → бин 440/22050*1024 ≈ 20 (из 512).
const bin = Math.round((440 / rate) * 1024);
let best = -Infinity;
let bestRow = 0;
for (let r = 0; r < spec.rows; r++) {
if (spec.db[r] > best) {
best = spec.db[r];
bestRow = r;
}
}
expect(Math.abs(bestRow - bin)).toBeLessThanOrEqual(2);
});
it('огибающая нормализована в 0..1', () => {
const env = envelope(tone(), 22050);
for (const v of env) {
expect(v).toBeGreaterThanOrEqual(0);
expect(v).toBeLessThanOrEqual(1);
}
expect(env.some((v) => v > 0.9)).toBe(true); // максимум достижим
});
});
describe('spectroSheet', () => {
it('пишет PNG-лист по нескольким клипам', () => {
const dir = mkdtempSync(join(tmpdir(), 'spectro-'));
try {
const out = join(dir, 'sheet.png');
const s = tone();
spectroSheet(
[
{ name: 'a', samples: s, rate: 22050, loop: true },
{ name: 'b', samples: s, rate: 22050, loop: false }
],
out
);
const buf = readFileSync(out);
expect(buf.subarray(1, 4).toString()).toBe('PNG');
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
});