Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,154 @@
import type { BlockContext, ProcessorConfig } from "@suaveplan/audio-core";
import type {
ChainContext,
ChainHandle,
} from "@suaveplan/audio-fx-plugin-base/base";
import { defaultAmbientState } from "@suaveplan/audio-fx-plugin-base/chain";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { SpessaSynthPlugin } from "../src/plugin/spessasynth-plugin.js";
import { buildFakeSf2Bytes, makeStubModule } from "./stub-module.js";
const SR = 48_000;
const BLOCK = 64;
function newCtx(): ChainContext {
const chain: ChainHandle = {
id: "midi",
name: "midi",
channels: 2,
sampleRate: SR,
blockSize: BLOCK,
slotCount: 1,
};
return {
chain,
position: { index: 0, total: 1 },
predecessors: [],
successors: [],
ambient: defaultAmbientState(),
buses: { get: () => undefined, list: () => [] },
};
}
function newConfig(): ProcessorConfig {
return { sampleRate: SR, channels: 2, blockSize: BLOCK };
}
function blockCtx(): BlockContext {
return {
blockOffsetSamples: 0,
sampleRate: SR,
channels: 2,
opts: {},
};
}
async function newPlugin() {
const mod = makeStubModule();
const plugin = new SpessaSynthPlugin({ moduleLoader: async () => mod });
await plugin.init({
soundFontBytes: buildFakeSf2Bytes(64),
sampleRate: SR,
});
await plugin.setup(newConfig(), newCtx());
return { mod, plugin };
}
describe("midi-routing: 4-bar sequence drives correct voice activations", () => {
it("voiceCount tracks noteOn/noteOff across a 4-bar sequence", {
timeout: 5_000,
}, async () => {
const { plugin } = await newPlugin();
const sequence: Array<{
offsetBlocks: number;
on: boolean;
note: number;
}> = [];
// 4 bars × 4 quarter notes × C major arpeggio.
for (let bar = 0; bar < 4; bar++) {
for (let beat = 0; beat < 4; beat++) {
const offset = bar * 16 + beat * 4;
const note = 60 + ((beat * 4) % 12);
sequence.push({ offsetBlocks: offset, on: true, note });
sequence.push({ offsetBlocks: offset + 2, on: false, note });
}
}
let active = 0;
let maxObservedActive = 0;
for (let b = 0; b < 64; b++) {
for (const event of sequence) {
if (event.offsetBlocks === b) {
if (event.on) {
plugin.sendMidi({
type: "noteOn",
channel: 0,
note: event.note,
velocity: 100,
});
active++;
} else {
plugin.sendMidi({
type: "noteOff",
channel: 0,
note: event.note,
});
active--;
}
}
}
const out: Float32Array[] = [
new Float32Array(BLOCK),
new Float32Array(BLOCK),
];
plugin.process([], out, blockCtx());
maxObservedActive = Math.max(
maxObservedActive,
plugin.getActiveVoiceCount()
);
}
expect(maxObservedActive).toBeGreaterThan(0);
expect(active).toBe(0); // sequence is balanced
});
it("programChange on channel 0 updates the underlying preset", {
timeout: 5_000,
}, async () => {
const { plugin, mod } = await newPlugin();
plugin.sendMidi({ type: "programChange", channel: 0, program: 24 });
const proc = (
mod as {
lastProcessor?: { observer: { programs: Map<number, number> } };
}
).lastProcessor;
// Block-level parameter flush writes program 0 by default, then
// our explicit program-change writes 24.
expect(proc?.observer.programs.get(0)).toBe(24);
});
it("NRPN per-note tuning sequence triggers the parameter observer", {
timeout: 5_000,
}, async () => {
const { plugin } = await newPlugin();
plugin.sendMidi({
type: "controlChange",
channel: 0,
controller: 99,
value: 0x65,
});
plugin.sendMidi({
type: "controlChange",
channel: 0,
controller: 98,
value: 0x4c,
});
plugin.sendMidi({
type: "controlChange",
channel: 0,
controller: 6,
value: 80,
});
// The processor receives the raw CCs; verified by no throw +
// active voice count remaining stable across the sequence.
expect(plugin.getActiveVoiceCount()).toBe(0);
});
});