import { describe, expect, it } from 'vitest';
import { renderMusic, type MusicSpec } from '../Music';
const RATE = 22050;
const spb = (bpm: number): number => 60 / bpm;
const same = (a: Float32Array, b: Float32Array): boolean =>
a.length === b.length && a.every((v, i) => v === b[i]);
const peak = (samples: Float32Array): number => Math.max(...[...samples].map(Math.abs));
/** Одна нота на долю — простейшая партитура. */
const one = (over: Partial<MusicSpec> = {}): MusicSpec => ({
bpm: 120,
beats: 4,
tracks: [{ instrument: 'pluck', notes: [{ beat: 0, dur: 1, midi: 69 }] }],
...over
});
describe('renderMusic', () => {
it('детерминизм: одинаковые спеки → одинаковые сэмплы', () => {
expect(same(renderMusic(one()), renderMusic(one()))).toBe(true);
});
it('длина лупа — сырая минус кроссфейд (фейд = min(0.5с, 25% длины))', () => {
const t1 = 4 * spb(120) * RATE; // 2 с
expect(renderMusic(one({ bpm: 120, beats: 4 })).length).toBe(t1 - 0.5 * RATE);
const t2 = 8 * spb(60) * RATE; // 8 с — фейд упёрся в 0.5 с
expect(renderMusic(one({ bpm: 60, beats: 8 })).length).toBe(t2 - 0.5 * RATE);
});
it('высота ноты доезжает: другой midi — другие сэмплы', () => {
const a = renderMusic(one());
const b = renderMusic(one({ tracks: [{ instrument: 'pluck', notes: [{ beat: 0, dur: 1, midi: 71 }] }] }));
expect(same(a, b)).toBe(false);
});
it('vel и gain меняют микс (пик после нормализации — 0.8 вне кроссфейд-зоны)', () => {
const beat1 = one({ tracks: [{ instrument: 'pluck', notes: [{ beat: 1, dur: 1, midi: 69 }] }] });
expect(peak(renderMusic(beat1))).toBeCloseTo(0.8, 2); // нормализация
// gain одной партии при опорной второй стереть нормализацией нельзя:
const mix = (g: number): MusicSpec => ({
bpm: 120,
beats: 4,
tracks: [
{ instrument: 'pad', notes: [{ beat: 0, dur: 4, midi: 57 }] },
{ instrument: 'pluck', notes: [{ beat: 1, dur: 1, midi: 69 }], gain: g }
]
});
expect(same(renderMusic(mix(1)), renderMusic(mix(0.3)))).toBe(false);
});
it('ноты за краем лупа обрезаются, пустой трек — тишина нормализована в 0', () => {
const empty = renderMusic(one({ tracks: [{ instrument: 'pad', notes: [] }] }));
expect(peak(empty)).toBe(0);
const clipped = renderMusic(one({ beats: 1, tracks: [{ instrument: 'pluck', notes: [{ beat: 0.5, dur: 2, midi: 69 }] }] }));
expect(clipped.length).toBeLessThanOrEqual(1 * spb(120) * RATE);
expect(peak(clipped)).toBeGreaterThan(0);
});
});