mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-08-15 13:17:24 +00:00
fix(renderer): keep Rig Builder's tone out of the user's manual VST chain (#73)
* fix(renderer): keep Rig Builder's tone out of the user's manual VST chain Rig Builder's chain preloader is always on, so it loads its whole tone (amp / pedals / racks / master pre-post / RB Final Leveler) into the SHARED engine chain. The Audio menu's 'Save Current Chain' and auto-persist captured the LIVE engine via getChainState()/savePreset(), baking those stages into the user's manual chain — so a user who built their own VST chain saw it sprout a full Rig Builder rig they never added. Add aeIsRigBuilderStage() (path under /rig_builder/, 'RB Final Leveler', rs_gear __rb*, or slot master_pre/post) + aeStripRigBuilderFromNativePreset(), and apply them at save (items + native blob), the app-init restore loop, the preset-load path (with an empty-guard), and refreshChain (display filter) so the manual chain only ever holds the user's own processors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(renderer): skip Rig Builder stages in the tone-switch preload paths too Codex review: legacy polluted presets were only sanitized in replaceChainWithPresetBlob(), but the tone-switch preloads load directly from raw preset.items + nativePreset.chain (loadPresetItemsWithState in IIFE 1 and the deliberately-inline copy in IIFE 2). Skip Rig Builder stages by index in both loops — index-skips keep the items/nativeChain alignment for the remaining pairs — and expose the detector as window._aeIsRigBuilderStage for IIFE 2. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(renderer): load fully-polluted presets as empty instead of falling back Codex review round 2: the never-empty guard restored the ORIGINAL polluted blob whenever stripping emptied the chain — but a preset that empties completely was 100% Rig Builder's tone, exactly the case the sanitizer exists for. Load the stripped (empty) chain and warn; empty- chain presets are a supported shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jafz2001 <ignacio.fritis@mundotelecomunicaciones.cl> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: byrongamatos <xasiklas@gmail.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jafz2001
byrongamatos
parent
fad294c6fc
commit
f785fb9ab1
@@ -1,10 +1,11 @@
|
||||
// Guards against the alpha-tester chain-duplication bug: the audio_engine
|
||||
// panel must (1) never persist a Rig-Builder-owned live chain into
|
||||
// localStorage, (2) drop Rig Builder plumbing stages from legacy saves on
|
||||
// restore, and (3) never auto-load (default preset / saved-chain restore) on
|
||||
// top of an engine that already has a live chain — the native chain survives
|
||||
// renderer re-evaluations, so an unconditional restore appended an exact
|
||||
// duplicate of every stage (two amp stages in series = "gain blown out").
|
||||
// Guards against the alpha-tester chain-duplication/pollution bugs: the
|
||||
// audio_engine panel must (1) persist only the USER's stages — never Rig
|
||||
// Builder's always-on tone stages — into localStorage and named presets,
|
||||
// (2) drop Rig Builder stages from legacy saves on restore, and (3) never
|
||||
// auto-load (default preset / saved-chain restore) on top of an engine that
|
||||
// already has a live chain — the native chain survives renderer
|
||||
// re-evaluations, so an unconditional restore appended an exact duplicate of
|
||||
// every stage (two amp stages in series = "gain blown out").
|
||||
|
||||
const test = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
@@ -15,14 +16,24 @@ const vm = require('node:vm');
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SCREEN_JS = fs.readFileSync(path.join(ROOT, 'src', 'renderer', 'screen.js'), 'utf8');
|
||||
|
||||
// Brace-balanced extraction of `function NAME(...) { ... }`.
|
||||
// Brace-balanced extraction of `function NAME(...) { ... }`. Skips past the
|
||||
// parameter list first (paren-balanced) so a destructured default parameter
|
||||
// like `{ snapshot = true } = {}` isn't mistaken for the body's opening brace.
|
||||
function extractFunction(src, name) {
|
||||
const sig = `function ${name}(`;
|
||||
const start = src.indexOf(sig);
|
||||
assert.ok(start !== -1, `function '${name}' not found`);
|
||||
const openBrace = src.indexOf('{', start);
|
||||
let i = start + sig.length;
|
||||
let parens = 1;
|
||||
while (i < src.length && parens > 0) {
|
||||
if (src[i] === '(') parens++;
|
||||
else if (src[i] === ')') parens--;
|
||||
i++;
|
||||
}
|
||||
assert.ok(parens === 0, `unbalanced parens in '${name}' signature`);
|
||||
const openBrace = src.indexOf('{', i);
|
||||
let depth = 1;
|
||||
let i = openBrace + 1;
|
||||
i = openBrace + 1;
|
||||
while (i < src.length && depth > 0) {
|
||||
if (src[i] === '{') depth++;
|
||||
else if (src[i] === '}') depth--;
|
||||
@@ -32,14 +43,10 @@ function extractFunction(src, name) {
|
||||
return src.slice(start, i);
|
||||
}
|
||||
|
||||
// Extract the marker const + both helpers into one sandbox.
|
||||
function setupSandbox() {
|
||||
const markerStart = SCREEN_JS.indexOf('const RB_PLUMBING_MARKER');
|
||||
assert.ok(markerStart !== -1, 'RB_PLUMBING_MARKER not found');
|
||||
const markerDecl = SCREEN_JS.slice(markerStart, SCREEN_JS.indexOf(';', markerStart) + 1);
|
||||
const code = [
|
||||
markerDecl,
|
||||
extractFunction(SCREEN_JS, 'isRigBuilderChainStage'),
|
||||
extractFunction(SCREEN_JS, 'aeIsRigBuilderStage'),
|
||||
extractFunction(SCREEN_JS, 'aeStripRigBuilderFromNativePreset'),
|
||||
extractFunction(SCREEN_JS, 'saveChainStateFromChain'),
|
||||
].join('\n');
|
||||
const stored = new Map();
|
||||
@@ -54,20 +61,17 @@ function setupSandbox() {
|
||||
}
|
||||
|
||||
const RB_CHAIN = [
|
||||
{ type: 0, path: '/vst/SamplegSBTCL.vst3', name: 'SamplegSBTCL' },
|
||||
{ type: 0, path: '/plugins/rig_builder/vst/SamplegSBTCL.vst3', name: 'SamplegSBTCL' },
|
||||
{ type: 2, path: '/irs/_rb_unit_impulse.wav', name: '_rb_unit_impulse' },
|
||||
{ type: 0, path: '/vst/RB Final Leveler.vst3', name: 'RB Final Leveler' },
|
||||
];
|
||||
|
||||
test('saveChainStateFromChain skips a Rig-Builder-owned live chain', () => {
|
||||
test('saveChainStateFromChain persists only the user stages of a mixed chain', () => {
|
||||
const { sandbox, stored } = setupSandbox();
|
||||
stored.set('slopsmith-signal-chain', '[{"type":"NAM","path":"/nam/amp.nam","name":"amp"}]');
|
||||
sandbox.saveChainStateFromChain([...RB_CHAIN, { type: 1, path: '/nam/vox.nam', name: 'VOX' }]);
|
||||
assert.equal(
|
||||
stored.get('slopsmith-signal-chain'),
|
||||
'[{"type":"NAM","path":"/nam/amp.nam","name":"amp"}]',
|
||||
'the previously saved panel-built chain must be preserved',
|
||||
);
|
||||
assert.deepEqual(JSON.parse(stored.get('slopsmith-signal-chain')), [
|
||||
{ type: 'NAM', path: '/nam/vox.nam', name: 'VOX' },
|
||||
], 'RB stages stripped, the user NAM stacked on top survives');
|
||||
});
|
||||
|
||||
test('saveChainStateFromChain persists a panel-built chain normally', () => {
|
||||
@@ -83,12 +87,42 @@ test('saveChainStateFromChain persists a panel-built chain normally', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('isRigBuilderChainStage matches plumbing by path or name only', () => {
|
||||
test('aeIsRigBuilderStage recognizes every Rig Builder stage shape', () => {
|
||||
const { sandbox } = setupSandbox();
|
||||
assert.equal(sandbox.isRigBuilderChainStage({ path: '/irs/_rb_unit_impulse.wav' }), true);
|
||||
assert.equal(sandbox.isRigBuilderChainStage({ name: 'RB Final Leveler' }), true);
|
||||
assert.equal(sandbox.isRigBuilderChainStage({ path: '/vst/MyAmp.vst3', name: 'MyAmp' }), false);
|
||||
assert.equal(sandbox.isRigBuilderChainStage(null), false);
|
||||
const rb = sandbox.aeIsRigBuilderStage;
|
||||
// Bundled RB gear by plugin-dir path (amps/pedals/racks), both separators.
|
||||
assert.equal(rb({ path: '/plugins/rig_builder/vst/SamplegSBTCL.vst3' }), true);
|
||||
assert.equal(rb({ path: 'C:\\plugins\\rig_builder\\vst\\Amp_AT20.vst3' }), true);
|
||||
// Plumbing by name or path, wherever the file lives.
|
||||
assert.equal(rb({ path: '/irs/_rb_unit_impulse.wav' }), true);
|
||||
assert.equal(rb({ name: '_rb_unit_impulse' }), true);
|
||||
assert.equal(rb({ name: 'RB Final Leveler' }), true);
|
||||
assert.equal(rb({ path: '/vst/RB Final Leveler.vst3' }), true);
|
||||
// Backend chain-spec sentinels.
|
||||
assert.equal(rb({ rs_gear: '__rb_final_leveler__' }), true);
|
||||
assert.equal(rb({ slot: 'master_pre' }), true);
|
||||
assert.equal(rb({ slot: 'master_post' }), true);
|
||||
// User gear is untouched.
|
||||
assert.equal(rb({ path: '/vst/MyAmp.vst3', name: 'MyAmp' }), false);
|
||||
assert.equal(rb({ path: '/nam/vox.nam', name: 'VOX' }), false);
|
||||
assert.equal(rb(null), false);
|
||||
});
|
||||
|
||||
test('aeStripRigBuilderFromNativePreset filters the blob chain, keeps user state', () => {
|
||||
const { sandbox } = setupSandbox();
|
||||
const blob = JSON.stringify({
|
||||
chain: [
|
||||
{ type: 0, path: '/plugins/rig_builder/vst/SamplegSBTCL.vst3', state: 'rb' },
|
||||
{ type: 0, path: '/vst/MyReverb.vst3', state: 'user-params' },
|
||||
{ type: 0, path: '/vst/x.vst3', slot: 'master_post', state: 'rb' },
|
||||
],
|
||||
gains: { input: 1 },
|
||||
});
|
||||
const out = JSON.parse(sandbox.aeStripRigBuilderFromNativePreset(blob));
|
||||
assert.deepEqual(out.chain, [{ type: 0, path: '/vst/MyReverb.vst3', state: 'user-params' }]);
|
||||
assert.deepEqual(out.gains, { input: 1 }, 'non-chain fields pass through');
|
||||
// Unparseable blob passes through untouched.
|
||||
assert.equal(sandbox.aeStripRigBuilderFromNativePreset('not json{'), 'not json{');
|
||||
});
|
||||
|
||||
test('init auto-load is gated on an empty engine chain (re-evaluation guard)', () => {
|
||||
@@ -106,10 +140,13 @@ test('init auto-load is gated on an empty engine chain (re-evaluation guard)', (
|
||||
);
|
||||
});
|
||||
|
||||
test('restore drops Rig Builder plumbing stages from legacy saves', () => {
|
||||
// aeRestoreSavedChain is async and coupled to the api bridge; assert the
|
||||
// sanitize step is present and rewrites the cleaned save.
|
||||
const fn = extractFunction(SCREEN_JS, 'aeRestoreSavedChain');
|
||||
assert.equal(fn.includes('isRigBuilderChainStage'), true);
|
||||
assert.equal(fn.includes("localStorage.setItem('slopsmith-signal-chain', JSON.stringify(_cleaned))"), true);
|
||||
test('restore self-heals legacy saves and preset save/load strip RB stages', () => {
|
||||
const restore = extractFunction(SCREEN_JS, 'aeRestoreSavedChain');
|
||||
assert.equal(restore.includes('aeIsRigBuilderStage'), true);
|
||||
assert.equal(restore.includes("localStorage.setItem('slopsmith-signal-chain', JSON.stringify(_cleaned))"), true);
|
||||
// Save Current Chain strips both the native blob and the item list…
|
||||
assert.equal(SCREEN_JS.includes('aeStripRigBuilderFromNativePreset(nativePresetRaw)'), true);
|
||||
// …and the preset-load path sanitizes legacy polluted presets.
|
||||
const load = extractFunction(SCREEN_JS, 'replaceChainWithPresetBlob');
|
||||
assert.equal(load.includes('aeStripRigBuilderFromNativePreset(preset.nativePreset)'), true);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user