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); }); });