78 lines
3.6 KiB
TypeScript
78 lines
3.6 KiB
TypeScript
import { test, expect } from "bun:test";
|
|
import { renderMidi } from "../../src/align/render.ts";
|
|
import { peaks } from "../../src/align/peaks.ts";
|
|
import { makeWarp } from "../../src/align/warp.ts";
|
|
import type { RefTrack, RefNote } from "../../src/align/smf.ts";
|
|
|
|
const SF2 = new Uint8Array(await Bun.file("node_modules/@coderline/alphatab/dist/soundfont/sonivox.sf2").arrayBuffer());
|
|
const note = (onset: number, offset: number, pitch: number): RefNote => ({ onset, offset, pitch, velocity: 100, channel: 0 });
|
|
const track = (notes: RefNote[], isDrums = false): RefTrack => ({ index: 0, name: "t", program: 25, isDrums, notes });
|
|
const SR = 44100;
|
|
|
|
function firstAbove(x: Float32Array, thr: number): number {
|
|
for (let i = 0; i < x.length; i++) if (Math.abs(x[i]!) > thr) return i;
|
|
return -1;
|
|
}
|
|
|
|
test("warp offset moves the note onset", () => {
|
|
const a = renderMidi([track([note(1, 2, 60)])], makeWarp([{ midiSec: 0, audioSec: 0.5, kind: "manual" }]), SF2, { sampleRate: SR, mono: true, durationSec: 4 });
|
|
expect(a.channels.length).toBe(1);
|
|
expect(a.channels[0]!.length).toBe(4 * SR);
|
|
const t = firstAbove(a.channels[0]!, 0.02) / SR;
|
|
expect(t).toBeGreaterThan(1.48);
|
|
expect(t).toBeLessThan(1.52);
|
|
});
|
|
|
|
test("dense chord is peak-normalised to -1 dBFS", () => {
|
|
const notes = Array.from({ length: 20 }, (_, i) => note(0, 1, 40 + i * 2));
|
|
const a = renderMidi([track(notes)], makeWarp([]), SF2, { sampleRate: SR });
|
|
let p = 0;
|
|
for (const c of a.channels) for (const v of c) p = Math.max(p, Math.abs(v));
|
|
expect(p).toBeLessThanOrEqual(Math.pow(10, -1 / 20) + 1e-3);
|
|
expect(p).toBeGreaterThan(0.85);
|
|
});
|
|
|
|
test("drums track is audible", () => {
|
|
const a = renderMidi([track([note(0.2, 0.3, 38)], true)], makeWarp([]), SF2, { sampleRate: SR, durationSec: 1 });
|
|
expect(a.channels[0]!.some((v) => Math.abs(v) > 0.1)).toBe(true);
|
|
});
|
|
|
|
test("peaks of a known sine", () => {
|
|
const x = new Float32Array(1000).map((_, i) => Math.sin((2 * Math.PI * i) / 100));
|
|
const p = peaks(x, 10);
|
|
expect(p.max.length).toBe(10);
|
|
for (let b = 0; b < 10; b++) {
|
|
expect(p.max[b]!).toBeCloseTo(1, 1);
|
|
expect(p.min[b]!).toBeCloseTo(-1, 1);
|
|
}
|
|
const q = peaks(new Float32Array([0.5, 0.25, -0.75, 0]), 2);
|
|
expect([...q.max]).toEqual([0.5, 0]);
|
|
expect([...q.min]).toEqual([0.25, -0.75]);
|
|
});
|
|
|
|
test("sampleRate 22050 is finite, at 22050 Hz, onset preserved", () => {
|
|
const a = renderMidi([track([note(1, 2, 60)])], makeWarp([{ midiSec: 0, audioSec: 0.5, kind: "manual" }]), SF2, { sampleRate: 22050, mono: true, durationSec: 4 });
|
|
expect(a.sampleRate).toBe(22050);
|
|
expect(a.channels[0]!.length).toBe(4 * 22050);
|
|
expect(a.channels[0]!.every(Number.isFinite)).toBe(true);
|
|
const t = firstAbove(a.channels[0]!, 0.02) / 22050;
|
|
expect(t).toBeGreaterThan(1.48);
|
|
expect(t).toBeLessThan(1.52);
|
|
});
|
|
|
|
test("notes before audio zero: clamped when overlapping, dropped when entirely before", () => {
|
|
const w = makeWarp([{ midiSec: 1, audioSec: 0, kind: "manual" }]); // audio = midi - 1
|
|
const dropped = renderMidi([track([note(0, 0.5, 60)]), track([note(2, 2.5, 64)])], w, SF2, { durationSec: 3 });
|
|
const t = firstAbove(dropped.channels[0]!, 0.02) / 44100;
|
|
expect(t).toBeGreaterThan(0.98);
|
|
const clamped = renderMidi([track([note(0.5, 2, 60)])], w, SF2, { durationSec: 3 });
|
|
expect(firstAbove(clamped.channels[0]!, 0.02) / 44100).toBeLessThan(0.05);
|
|
});
|
|
|
|
test("stereo with durationSec has two channels of exact length", () => {
|
|
const a = renderMidi([track([note(0, 0.5, 60)])], makeWarp([]), SF2, { durationSec: 2 });
|
|
expect(a.channels.length).toBe(2);
|
|
expect(a.channels[0]!.length).toBe(88200);
|
|
expect(a.channels[1]!.length).toBe(88200);
|
|
});
|