Initial Commit
This commit is contained in:
86
packages/media/audio/midi/tests/conformance.test.ts
Normal file
86
packages/media/audio/midi/tests/conformance.test.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import {
|
||||
GM_LEVEL1_FAMILIES,
|
||||
GM_LEVEL1_PATCHES,
|
||||
GM_LEVEL1_PERCUSSION,
|
||||
GM_LEVEL2_VARIATIONS,
|
||||
GS_VARIATIONS,
|
||||
gm2Key,
|
||||
XG_VARIATIONS,
|
||||
} from "../src/gm/index.js";
|
||||
import { MIDI_MESSAGE_TYPES } from "../src/message/message.types.js";
|
||||
import { midi1ToUmp, umpToMidi1 } from "../src/ump/ump.js";
|
||||
|
||||
describe("General MIDI Level 1 table integrity", () => {
|
||||
it("has all 128 programs", { timeout: 5000 }, () => {
|
||||
expect(Object.keys(GM_LEVEL1_PATCHES).length).toBe(128);
|
||||
for (let i = 0; i < 128; i += 1) {
|
||||
expect(typeof GM_LEVEL1_PATCHES[i]).toBe("string");
|
||||
}
|
||||
});
|
||||
|
||||
it("GM patch 0 is Acoustic Grand Piano", { timeout: 5000 }, () => {
|
||||
expect(GM_LEVEL1_PATCHES[0]).toBe("Acoustic Grand Piano");
|
||||
});
|
||||
|
||||
it("has percussion notes 35..81 (47 entries)", { timeout: 5000 }, () => {
|
||||
expect(Object.keys(GM_LEVEL1_PERCUSSION).length).toBe(81 - 35 + 1);
|
||||
expect(GM_LEVEL1_PERCUSSION[36]).toBe("Bass Drum 1");
|
||||
expect(GM_LEVEL1_PERCUSSION[42]).toBe("Closed Hi Hat");
|
||||
expect(GM_LEVEL1_PERCUSSION[81]).toBe("Open Triangle");
|
||||
});
|
||||
|
||||
it("families list has 16 entries", { timeout: 5000 }, () => {
|
||||
expect(GM_LEVEL1_FAMILIES.length).toBe(16);
|
||||
});
|
||||
|
||||
it("GM2 / GS / XG dialects expose at least one variation each", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(Object.keys(GM_LEVEL2_VARIATIONS).length).toBeGreaterThan(0);
|
||||
expect(Object.keys(GS_VARIATIONS).length).toBeGreaterThan(0);
|
||||
expect(Object.keys(XG_VARIATIONS).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("gm2Key composes the same way both tables encode", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(gm2Key(4, 1)).toBe(4 * 256 + 1);
|
||||
expect(GM_LEVEL2_VARIATIONS[gm2Key(4, 1)]).toBe(
|
||||
"Detuned Electric Piano 1"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("UMP framing invariants", () => {
|
||||
it("group bits map to bits 24..27 of word 0", { timeout: 5000 }, () => {
|
||||
const packet = midi1ToUmp(
|
||||
{ type: "noteOn", channel: 0, note: 60, velocity: 100 },
|
||||
3
|
||||
);
|
||||
expect(((packet.words[0] as number) >>> 24) & 0xf).toBe(3);
|
||||
});
|
||||
|
||||
it("channel-voice round-trip preserves all 16 channels", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
for (let ch = 0; ch < 16; ch += 1) {
|
||||
const msg = {
|
||||
type: "noteOn" as const,
|
||||
channel: ch,
|
||||
note: 60,
|
||||
velocity: 64,
|
||||
};
|
||||
expect(umpToMidi1(midi1ToUmp(msg, 0))).toEqual(msg);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("Message-type registry", () => {
|
||||
it("MIDI_MESSAGE_TYPES contains all 18 variants exactly once", {
|
||||
timeout: 5000,
|
||||
}, () => {
|
||||
expect(MIDI_MESSAGE_TYPES.length).toBe(18);
|
||||
expect(new Set(MIDI_MESSAGE_TYPES).size).toBe(18);
|
||||
});
|
||||
});
|
||||
130
packages/media/audio/midi/tests/property.test.ts
Normal file
130
packages/media/audio/midi/tests/property.test.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import { describe, expect, it } from "@suaveplan/testing/runner";
|
||||
import fc from "fast-check";
|
||||
import { decodeMessage } from "../src/message/decode.js";
|
||||
import { encodeMessage } from "../src/message/encode.js";
|
||||
import type { MidiMessage } from "../src/message/message.types.js";
|
||||
|
||||
const channel = fc.integer({ min: 0, max: 15 });
|
||||
const data7 = fc.integer({ min: 0, max: 127 });
|
||||
const data14 = fc.integer({ min: 0, max: 16383 });
|
||||
|
||||
const channelVoiceArbs: fc.Arbitrary<MidiMessage>[] = [
|
||||
fc.record({
|
||||
type: fc.constant("noteOff" as const),
|
||||
channel,
|
||||
note: data7,
|
||||
velocity: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("noteOn" as const),
|
||||
channel,
|
||||
note: data7,
|
||||
velocity: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("polyPressure" as const),
|
||||
channel,
|
||||
note: data7,
|
||||
pressure: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("controlChange" as const),
|
||||
channel,
|
||||
controller: data7,
|
||||
value: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("programChange" as const),
|
||||
channel,
|
||||
program: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("channelPressure" as const),
|
||||
channel,
|
||||
pressure: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("pitchBend" as const),
|
||||
channel,
|
||||
value: data14,
|
||||
}),
|
||||
];
|
||||
|
||||
const systemArbs: fc.Arbitrary<MidiMessage>[] = [
|
||||
fc.record({
|
||||
type: fc.constant("timeCode" as const),
|
||||
channel: fc.constant(null),
|
||||
value: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("songPosition" as const),
|
||||
channel: fc.constant(null),
|
||||
position: data14,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("songSelect" as const),
|
||||
channel: fc.constant(null),
|
||||
song: data7,
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("tuneRequest" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("clock" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("start" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("continue" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("stop" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("activeSensing" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
fc.record({
|
||||
type: fc.constant("reset" as const),
|
||||
channel: fc.constant(null),
|
||||
}),
|
||||
];
|
||||
|
||||
const sysexArb: fc.Arbitrary<MidiMessage> = fc
|
||||
.uint8Array({ minLength: 0, maxLength: 32, min: 0, max: 0x7f })
|
||||
.map((payload) => ({
|
||||
type: "systemExclusive" as const,
|
||||
channel: null,
|
||||
payload,
|
||||
}));
|
||||
|
||||
const anyMessageArb = fc.oneof(...channelVoiceArbs, ...systemArbs, sysexArb);
|
||||
|
||||
describe("property: encode/decode round-trip", () => {
|
||||
it("every variant round-trips", { timeout: 30000 }, () => {
|
||||
fc.assert(
|
||||
fc.property(anyMessageArb, (msg) => {
|
||||
const bytes = encodeMessage(msg);
|
||||
const { msg: decoded, next } = decodeMessage(bytes, 0);
|
||||
expect(next).toBe(bytes.length);
|
||||
if (
|
||||
msg.type === "systemExclusive" &&
|
||||
decoded.type === "systemExclusive"
|
||||
) {
|
||||
expect(Array.from(decoded.payload)).toEqual(
|
||||
Array.from(msg.payload)
|
||||
);
|
||||
} else {
|
||||
expect(decoded).toEqual(msg);
|
||||
}
|
||||
}),
|
||||
{ numRuns: 1000 }
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user