mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-09-10 23:44:11 +00:00
AddonContext (src/audio/addon/): engine/vstHost lifetime + snapshot rule, the JUCE message thread with the macOS no-pump fork quarantined into ONE file, the shutdown latch (exposed as isShuttingDown), doShutdown with a UI teardown hook (NodeAddon points it at the editor-window nuke, #56), and the pending-async-load registry. NodeAddon keeps using-declarations so the binding bodies are unchanged. Also fixes SetBackingSpeed's bare `engine` dereference — the one binding that dodged the file's own snapshot rule. NapiHelpers (typed extractors argInt/argSlotId/argFiniteFloat/argBool/ argMidiChannel/argMidiByte) + rewrites of the unguarded bindings — the deep-read §2 fix, done once: SetParameter/SetBypass/RemoveProcessor/ MoveProcessor/SetMultiBypass/SendMidiToSlot/SetGain plus the St-1 routing quartet (SetPan/SetPostGain/SetBranch/SetBranchSrc). NaN slot ids no longer coerce to slot 0; MIDI channel/program are range-checked before JUCE. New gate: tests/napi-arg-fuzz.test.js — table-driven garbage (NaN/Inf/ negative/string/missing/object) against the real addon; chain state must be byte-identical after the storm and a valid call must still apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
76 lines
3.2 KiB
JavaScript
76 lines
3.2 KiB
JavaScript
// Phase 6 gate (docs/audio-engine-tlc.md §5, deep-read §2): table-driven
|
|
// argument fuzz against the real addon. Every chain-mutating binding must
|
|
// treat NaN/Infinity/negative/string/missing/object arguments as a clean
|
|
// no-op — historically Int32Value() coerced NaN → 0 and mutated SLOT 0.
|
|
// Quarantined behind the addon being built (CI native lane), auto-skips
|
|
// otherwise. Uses no audio device (engine constructed, never started).
|
|
'use strict';
|
|
|
|
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const os = require('node:os');
|
|
const path = require('node:path');
|
|
|
|
const ADDON = path.join(__dirname, '..', 'build', 'Release', 'slopsmith_audio.node');
|
|
const HAVE_ADDON = fs.existsSync(ADDON);
|
|
|
|
function writeImpulseWav(file) {
|
|
const buf = Buffer.alloc(44 + 128);
|
|
buf.write('RIFF', 0); buf.writeUInt32LE(36 + 128, 4); buf.write('WAVE', 8);
|
|
buf.write('fmt ', 12); buf.writeUInt32LE(16, 16); buf.writeUInt16LE(1, 20);
|
|
buf.writeUInt16LE(1, 22); buf.writeUInt32LE(48000, 24); buf.writeUInt32LE(96000, 28);
|
|
buf.writeUInt16LE(2, 32); buf.writeUInt16LE(16, 34);
|
|
buf.write('data', 36); buf.writeUInt32LE(128, 40);
|
|
buf.writeInt16LE(32767, 44);
|
|
fs.writeFileSync(file, buf);
|
|
}
|
|
|
|
const GARBAGE = [NaN, Infinity, -Infinity, -1, 1.5, 4097, 'x', null, undefined, {}, []];
|
|
|
|
test('chain-mutating bindings no-op on garbage args and never touch slot 0', { skip: !HAVE_ADDON && 'addon not built' }, async () => {
|
|
const audio = require(ADDON);
|
|
audio.init();
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'arg-fuzz-'));
|
|
const ir = path.join(tmp, 'i.wav');
|
|
writeImpulseWav(ir);
|
|
try {
|
|
const res = await audio.loadPreset(JSON.stringify({
|
|
chain: [{ type: 2, name: 'fuzz-anchor', path: ir, bypassed: false }],
|
|
}));
|
|
assert.ok(res?.success, 'anchor preset must load');
|
|
const before = JSON.stringify(audio.getChainState());
|
|
|
|
for (const g of GARBAGE) {
|
|
audio.setBypass(g, true);
|
|
audio.setBypass(0 /* valid id shape */, g);
|
|
audio.removeProcessor(g);
|
|
audio.moveProcessor(g, 0);
|
|
audio.moveProcessor(0, g);
|
|
audio.setParameter(g, 0, 0.5);
|
|
audio.setParameter(1, g, 0.5);
|
|
audio.setParameter(1, 0, g);
|
|
audio.setPan?.(g, 0);
|
|
audio.sendMidiToSlot(g, 0, 1, 0);
|
|
audio.sendMidiToSlot(1, g, 1, 0);
|
|
audio.sendMidiToSlot(1, 0, g, 0);
|
|
audio.sendMidiToSlot(1, 0, 1, g);
|
|
audio.setMultiBypass([{ slotId: g, bypassed: true }, g, null]);
|
|
audio.setGain('output', g);
|
|
audio.setGain(g, 1);
|
|
}
|
|
|
|
const after = JSON.stringify(audio.getChainState());
|
|
assert.equal(after, before, 'garbage args must not mutate any slot');
|
|
// Sanity: a VALID call still works after the fuzz storm.
|
|
audio.setBypass(1, true);
|
|
const st = audio.getChainState();
|
|
assert.equal(st[0]?.bypassed, true, 'valid call after fuzz must apply');
|
|
audio.setBypass(1, false);
|
|
} finally {
|
|
await audio.clearChain?.();
|
|
audio.shutdown?.();
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
});
|