feat(audio): own-rig opt-in — gate saved tone-chain restore on use_amp_sims (#46) (#50)

* feat(audio): own-rig opt-in — gate saved tone-chain restore on use_amp_sims

Second half of #46 (desktop side), paired with the core onboarding PR.
The amp-sim/tone chain auto-restores from localStorage on every launch, so
once a user has loaded a tone they get a processed monitor forever — an
idle high-gain amp is a constant distorted buzz, and the dry-only monitor
mute can't kill it (the full monitor kill from #47 can, but only on demand).

This makes monitoring "own-rig first": at app init, read the core
`use_amp_sims` preference (set during onboarding / the new toggle) and only
auto-restore the saved signal chain when the user opted IN. Default OFF — a
missing key or any read failure is treated as opt-out, so a flaky/late
backend can never resurrect the buzz. With no chain loaded, the existing
default-on dry mute keeps the monitor silent.

- screen.js: aeUseAmpSims() reads /api/settings; init gates loadDefaultPreset
  + saved-chain restore behind it. Extracted the restore loop into
  aeRestoreSavedChain() (shared by init and the live opt-in toggle).
- screen.html/js: new "Use in-app amp sims" checkbox in Audio settings,
  persisted to /api/settings (shared with onboarding). Reflects the saved
  value on load; turning it ON loads the saved chain immediately (no restart).

Stacked on #47 (monitor kill). node --check clean. NOT built/run here — the
renderer change needs a desktop build + a tester check: with a saved tone and
amp sims OFF, launch is silent (no buzz); toggling ON loads the tone live;
the onboarding choice carries through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(audio): make the amp-sim toggle apply live (Codex review on #50)

Addresses Codex findings on the "Use in-app amp sims" checkbox:

- P2: turning it OFF now clears the live engine chain so monitoring
  actually goes silent this session (with no processors, the default-on
  dry mute silences the bus) — previously OFF persisted the pref but left
  the amp running, so the checkbox lied and the buzz persisted until
  restart. The saved chain in localStorage is left intact (we don't call
  saveChainState) so re-enabling restores the same tone.
- P2: ON no longer stacks a duplicate chain — when there's no default
  preset (so loadDefaultPreset returns without clearing) we clearChain()
  before aeRestoreSavedChain() instead of appending onto the current chain.
- P3: the /api/settings POST now warns on a non-ok HTTP status.

node --check clean. Still needs a desktop build + tester pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(audio): render empty chain directly on amp-sim OFF (avoid getChainState-after-clearChain JUCE crash)

Codex re-review P2: the OFF path called refreshChain() right after
clearChain(), which getChainStates the native engine — a sequence the
codebase documents can crash some JUCE bridges (clearChainForNewSong).
Render the empty-chain placeholder directly instead, mirroring that safe
pattern. localStorage is still preserved so re-enabling restores the tone.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Byron Gamatos 2026-06-29 00:13:31 +02:00 committed by GitHub
parent 9facf78c98
commit 0eabbceb73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 115 additions and 25 deletions

View File

@ -51,6 +51,14 @@
<span class="text-xs text-slate-500">(play through your own amp/rig — silences all in-app monitoring, including amp sims)</span>
</label>
</div>
<div class="md:col-span-2">
<label class="flex items-center gap-2 cursor-pointer">
<input type="checkbox" id="ae-amp-sims"
class="w-4 h-4 rounded bg-slate-700 border-slate-600 accent-emerald-500">
<span class="text-sm text-slate-300">Use in-app amp sims</span>
<span class="text-xs text-slate-500">(auto-load your saved tone chain for monitoring — leave off to play through your own rig)</span>
</label>
</div>
<div>
<label class="block text-xs text-slate-400 mb-1">Input Device</label>
<select id="ae-input-device" class="w-full bg-slate-700 border border-slate-600 rounded px-3 py-2 text-sm">

View File

@ -56,6 +56,7 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
const outputGainLabel = $('ae-output-gain-label');
const monitorMuteCheckbox = $('ae-monitor-mute');
const monitorKillCheckbox = $('ae-monitor-kill');
const ampSimsCheckbox = $('ae-amp-sims');
const chainContainer = $('ae-chain');
const addVstBtn = $('ae-add-vst');
const addNamBtn = $('ae-add-nam');
@ -893,45 +894,82 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
}
}
// Amp-sim opt-in gate (feedBack-desktop#46). Only auto-load a tone chain
// for monitoring when the user opted into in-app amp sims. Default OFF
// ("own-rig first") so players monitoring through their own external amp/
// rig get clean, silent monitoring and never the idle distorted buzz. The
// preference lives in core /api/settings (set during onboarding and via
// the "Use in-app amp sims" toggle below); a missing key or failed read is
// treated as OFF so a flaky backend can't resurrect the buzz.
const _ampSimsEnabled = await aeUseAmpSims();
ampSimsCheckbox.checked = _ampSimsEnabled;
// Try the default preset first; only restore the saved chain if no default preset is
// configured or the preset load fails (corrupted blob, missing VST, etc.). This avoids
// redundant native load/unload when the preset immediately replaces the chain, while
// ensuring a valid chain is always available as a fallback.
let _defaultLoaded = false;
try {
_defaultLoaded = await loadDefaultPreset('app-init');
} catch (e) {
console.error('[audio-engine] Default preset load threw at init; falling back to saved chain:', e);
}
if (!_defaultLoaded) {
let savedChain;
if (_ampSimsEnabled) {
try {
savedChain = JSON.parse(localStorage.getItem('slopsmith-signal-chain') || '[]');
if (!Array.isArray(savedChain)) savedChain = [];
_defaultLoaded = await loadDefaultPreset('app-init');
} catch (e) {
console.warn('[audio-engine] Corrupted slopsmith-signal-chain; starting empty:', e);
savedChain = [];
console.error('[audio-engine] Default preset load threw at init; falling back to saved chain:', e);
}
for (const item of savedChain) {
try {
if (item.type === 'VST' && item.path) {
await api.loadVST(item.path);
} else if (item.type === 'NAM' && item.path) {
await api.loadNAMModel(item.path);
} else if (item.type === 'IR' && item.path) {
await api.loadIR(item.path);
}
} catch (e) {
console.error('[audio-engine] Failed to restore chain item:', item, e);
}
}
if (savedChain.length > 0) await refreshChain();
} else {
console.info('[audio-engine] Amp sims opt-out — skipping saved tone-chain restore (own-rig monitoring).');
}
if (_ampSimsEnabled && !_defaultLoaded) {
await aeRestoreSavedChain();
}
aeApplyNoiseGateToEngine();
aeApplyTonePolishToEngine();
}
// Read the core "use in-app amp sims" preference (feedBack-desktop#46).
// Default OFF — a missing key or any read failure is treated as opt-OUT so a
// flaky/late backend never resurrects the idle amp-sim buzz for own-rig users.
async function aeUseAmpSims() {
try {
const r = await fetch('/api/settings');
if (!r.ok) return false;
const s = await r.json();
return !!(s && s.use_amp_sims === true);
} catch (e) {
console.warn('[audio-engine] use_amp_sims read failed; assuming opt-out:', e);
return false;
}
}
// Restore the persisted signal chain (VST/NAM/IR) into the engine. Shared by
// app init (when amp sims are opted in) and the live "Use in-app amp sims"
// opt-in toggle. Each item failure is contained so one bad plugin can't abort
// the rest of the chain.
async function aeRestoreSavedChain() {
let savedChain;
try {
savedChain = JSON.parse(localStorage.getItem('slopsmith-signal-chain') || '[]');
if (!Array.isArray(savedChain)) savedChain = [];
} catch (e) {
console.warn('[audio-engine] Corrupted slopsmith-signal-chain; starting empty:', e);
savedChain = [];
}
for (const item of savedChain) {
try {
if (item.type === 'VST' && item.path) {
await api.loadVST(item.path);
} else if (item.type === 'NAM' && item.path) {
await api.loadNAMModel(item.path);
} else if (item.type === 'IR' && item.path) {
await api.loadIR(item.path);
}
} catch (e) {
console.error('[audio-engine] Failed to restore chain item:', item, e);
}
}
if (savedChain.length > 0) await refreshChain();
}
function saveChainStateFromChain(chain) {
const typeMap = { 0: 'VST', 1: 'NAM', 2: 'IR' };
const items = chain.filter(s => s.type === 0 || s.type === 1 || s.type === 2).map(s => ({
@ -1329,6 +1367,50 @@ window.__feedBackDesktopAudioHooks = window.__feedBackDesktopAudioHooks || {};
await saveAppliedDeviceSettings({ monitorKill: monitorKillCheckbox.checked });
});
// Use in-app amp sims (feedBack-desktop#46) — persists the opt-in to core
// /api/settings (shared with the onboarding choice). Default OFF / own-rig.
// Applies LIVE so the checkbox is truthful this session, not just next
// launch:
// ON — clear then (re)load the saved tone chain so the user hears their
// tone now without duplicating processors onto an existing chain.
// OFF — clear the live engine chain so monitoring actually goes silent
// (with no processors, the default-on dry mute silences the bus).
// The SAVED chain in localStorage is left intact so re-enabling
// restores the same tone — so we deliberately do NOT saveChainState().
ampSimsCheckbox.addEventListener('change', async () => {
const on = ampSimsCheckbox.checked;
try {
const r = await fetch('/api/settings', {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ use_amp_sims: on }),
});
if (!r.ok) console.warn('[audio-engine] use_amp_sims persist HTTP', r.status);
} catch (e) { console.warn('[audio-engine] use_amp_sims persist failed:', e); }
try {
if (on) {
// loadDefaultPreset → replaceChainWithPresetBlob clears first; if
// there's no default preset it returns false WITHOUT clearing, so
// clear explicitly before the saved-chain restore to avoid stacking
// a duplicate chain on top of whatever is already loaded.
let _loaded = false;
try { _loaded = await loadDefaultPreset('amp-sims-optin'); }
catch (e) { console.error('[audio-engine] default preset load failed on opt-in:', e); }
if (!_loaded) {
await api.clearChain();
await aeRestoreSavedChain();
}
} else {
// Silence the live monitor now. Keep localStorage so ON restores it.
await api.clearChain();
// Render the empty chain directly — do NOT call refreshChain()/
// getChainState() right after clearChain(); some JUCE bridges crash
// on that sequence (see clearChainForNewSong).
const _c = chainContainer || $('ae-chain');
if (_c) _c.innerHTML = '<div class="text-sm text-slate-500 italic">No processors loaded — add a VST, NAM model, or cabinet IR</div>';
}
} catch (e) { console.error('[audio-engine] amp-sim toggle apply failed:', e); }
});
// Gain sliders (UI dB → linear amplitude for engine)
inputGainSlider.addEventListener('input', () => {
const db = parseFloat(inputGainSlider.value);