Files
2026-05-27 23:47:53 +01:00

158 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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: "auto",
name: "auto",
channels: 2,
sampleRate: SR,
blockSize: BLOCK,
slotCount: 1,
};
return {
chain,
position: { index: 0, total: 1 },
predecessors: [],
successors: [],
ambient: defaultAmbientState(),
buses: { get: () => undefined, list: () => [] },
};
}
describe("automation: master gain ramp affects rendered output", () => {
it("master gain set to 0 produces silence; set to 2 produces louder output", {
timeout: 5_000,
}, async () => {
const mod = makeStubModule();
const plugin = new SpessaSynthPlugin({
moduleLoader: async () => mod,
});
await plugin.init({
soundFontBytes: buildFakeSf2Bytes(64),
sampleRate: SR,
});
const config: ProcessorConfig = {
sampleRate: SR,
channels: 2,
blockSize: BLOCK,
};
await plugin.setup(config, newCtx());
plugin.sendMidi({
type: "noteOn",
channel: 0,
note: 60,
velocity: 100,
});
const ctx: BlockContext = {
blockOffsetSamples: 0,
sampleRate: SR,
channels: 2,
opts: {},
};
// Render with master gain 1 (default).
const outBaseline: Float32Array[] = [
new Float32Array(BLOCK),
new Float32Array(BLOCK),
];
plugin.process([], outBaseline, ctx);
let baselineRms = 0;
for (let i = 0; i < BLOCK; i++) baselineRms += outBaseline[0]![i]! ** 2;
baselineRms = Math.sqrt(baselineRms / BLOCK);
expect(baselineRms).toBeGreaterThan(0);
// Silence: master 0.
plugin.setParameter("master/gain", 0);
const outSilent: Float32Array[] = [
new Float32Array(BLOCK),
new Float32Array(BLOCK),
];
plugin.process([], outSilent, ctx);
let silentRms = 0;
for (let i = 0; i < BLOCK; i++) {
silentRms += outSilent[0]![i]! ** 2;
}
expect(silentRms).toBe(0);
// Loud: master 2.
plugin.setParameter("master/gain", 2);
const outLoud: Float32Array[] = [
new Float32Array(BLOCK),
new Float32Array(BLOCK),
];
plugin.process([], outLoud, ctx);
let loudRms = 0;
for (let i = 0; i < BLOCK; i++) loudRms += outLoud[0]![i]! ** 2;
loudRms = Math.sqrt(loudRms / BLOCK);
// Should be roughly 2× the baseline.
expect(loudRms).toBeGreaterThan(baselineRms);
});
it("channel volume parameter flushes to CC#7 each block", {
timeout: 5_000,
}, async () => {
const mod = makeStubModule();
const plugin = new SpessaSynthPlugin({
moduleLoader: async () => mod,
});
await plugin.init({
soundFontBytes: buildFakeSf2Bytes(64),
sampleRate: SR,
});
await plugin.setup(
{ sampleRate: SR, channels: 2, blockSize: BLOCK },
newCtx()
);
plugin.setParameter("channel/0/volume", 1);
const out: Float32Array[] = [
new Float32Array(BLOCK),
new Float32Array(BLOCK),
];
plugin.process([], out, {
blockOffsetSamples: 0,
sampleRate: SR,
channels: 2,
opts: {},
});
plugin.setParameter("channel/0/volume", 0);
plugin.process([], out, {
blockOffsetSamples: 0,
sampleRate: SR,
channels: 2,
opts: {},
});
const proc = (
mod as {
lastProcessor?: {
observer: {
cc: Array<{
channel: number;
controller: number;
value: number;
}>;
};
};
}
).lastProcessor!;
const ch0Vol = proc.observer.cc.filter(
(e) => e.channel === 0 && e.controller === 7
);
// First flush wrote 127 (volume=1), second wrote 0.
expect(ch0Vol[0]?.value).toBe(127);
expect(ch0Vol[ch0Vol.length - 1]?.value).toBe(0);
});
});