Initial Commit
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user