feat(rig-builder): override active tone with preset (in-song player control) - #62
feat(rig-builder): override active tone with preset (in-song player control)#62Duali98 wants to merge 1 commit into
Conversation
…ontrol)
Adds a name-independent way to permanently swap a song's specific tone for
another preset's gear, from right inside the player while the song is
playing.
A Clean/OD/Distortion tone-name-classification approach was tried first
(matching a song's own tone_key against those 3 words) but dropped after
testing against a real library: RS tone-naming conventions are inconsistent
enough ("Clean"/"CLEAN"/"orion_dist"/"bass_base" all coexist) that name
matching missed or misfired on real charts. This is the more universal
replacement: it doesn't classify anything by name at all.
- New POST /override_active_tone: given {filename, tone_key, source, name},
clones the chosen preset's ENTIRE preset_pieces (gear, VST state, gain_db/
pan/phase_inv — everything) onto that specific song tone's own saved
preset, via the existing _persist_preset_chain/_resolve_tone_preset_id
primitives (no new DB schema, no lossy enrich/de-enrich round-trip — reads
the source's raw preset_pieces columns directly). Same destructive
semantics as the existing per-piece "🔁 Swap" button (routes.py's
/gear/replace_with) — this is the whole-tone equivalent, and just as
permanent (no separate override layer, no built-in undo).
- New in-song player control: a select next to "Rig Tones On" in the
Plugin Controls panel, labeled with whichever tone_key is CURRENTLY
ACTIVE. Picking "Default tone" or a saved tone shows a confirmation
(this is destructive) and, on confirm, POSTs the swap and rebuilds the
song's mega-chain so the new gear is heard immediately. Works on any
tone_key regardless of what it's named — the control doesn't need to
know or care.
Verified live: enabling this against a real song's active tone correctly
replaced its gear while every other tone in the same song stayed untouched.
📝 WalkthroughWalkthroughAdds an endpoint and in-song control to replace the active tone with a saved or default preset, preserves existing gate settings, corrects the chain input fallback, and refreshes live previews after gear swaps. ChangesActive tone preset override
Estimated code review effort: 3 (Moderate) | ~25 minutes Sequence Diagram(s)sequenceDiagram
participant Player
participant OverrideControl
participant override_active_tone
participant MegaChain
Player->>OverrideControl: Select source tone preset
OverrideControl->>override_active_tone: POST active tone override
override_active_tone-->>OverrideControl: Return new preset_id
OverrideControl->>MegaChain: Rebuild song chain
MegaChain-->>Player: Use replaced tone preset
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@routes.py`:
- Around line 9984-10008: Remove gain_db, pan, and phase_inv from the
preset_pieces SELECT, row unpacking, and pieces dictionaries in the copy path.
Keep the selected fields aligned with the columns persisted by
_persist_preset_chain, preserving all other copied gear properties.
- Around line 10009-10019: Update override_active_tone to mirror the permanent
tone override across both sibling container formats, matching save_preset. Use
_canonical_song_key to derive the shared song identity, construct each
psarc/sloppak filename, call _persist_preset_chain for both, and collect their
preset IDs. Return the collected IDs while preserving the existing error
handling and piece count response.
In `@screen.js`:
- Around line 3738-3801: Track the override request state for the select created
by rbInjectOverrideActiveToneControl, and update
rbUpdateOverrideActiveToneControl so refreshes preserve sel.disabled while a
request is in flight. Set the pending state before the override_active_tone
fetch and clear it in finally, ensuring unrelated control refreshes cannot
re-enable or permit another submission until the first request completes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
| conn = _get_conn() | ||
| rows = conn.execute( | ||
| "SELECT slot, rs_gear_type, kind, file, params_json, tone3000_id, " | ||
| "assigned_mode, bypassed, vst_path, vst_format, vst_state, " | ||
| "gain_db, pan, phase_inv " | ||
| "FROM preset_pieces WHERE preset_id = ? ORDER BY slot_order", | ||
| (src_pid,), | ||
| ).fetchall() | ||
| if not rows: | ||
| return JSONResponse({"error": "source tone has no gear to copy"}, 400) | ||
| pieces = [] | ||
| for (slot, rs_gear_type, kind, file, params_json, tone3000_id, | ||
| assigned_mode, bypassed, vst_path, vst_format, vst_state, | ||
| gain_db, pan, phase_inv) in rows: | ||
| try: | ||
| params = json.loads(params_json) if params_json else {} | ||
| except (ValueError, TypeError): | ||
| params = {} | ||
| pieces.append({ | ||
| "slot": slot, "rs_gear_type": rs_gear_type, "kind": kind, | ||
| "file": file, "params": params, "tone3000_id": tone3000_id, | ||
| "assigned_mode": assigned_mode, "bypassed": bool(bypassed), | ||
| "vst_path": vst_path, "vst_format": vst_format, "vst_state": vst_state, | ||
| "gain_db": gain_db, "pan": pan, "phase_inv": phase_inv, | ||
| }) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n 'gain_db|phase_inv|\bpan\b|CREATE TABLE preset_pieces|ALTER TABLE preset_pieces ADD' routes.pyRepository: got-feedBack/feedBack-plugin-rig-builder
Length of output: 1190
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== preset_pieces schema/migrations around ALTER TABLE preset_pieces ADD =="
sed -n '850,910p' routes.py | cat -n
echo
echo "== _persist_preset_chain implementation =="
sed -n '4720,4795p' routes.py | cat -n
echo
echo "== override_active_tone implementation =="
sed -n '9960,10025p' routes.py | cat -n
echo
echo "== all preset_pieces column occurrences =="
python3 - <<'PY'
from pathlib import Path
p=Path('routes.py')
text=p.read_text()
lines=text.splitlines()
for i,l in enumerate(lines,1):
if 'preset_pieces' in l:
print(f"{i}: {l}")
PYRepository: got-feedBack/feedBack-plugin-rig-builder
Length of output: 23743
Drop/deprecate the unused gain_db/pan/phase_inv fields on this copy path.
These columns are not in preset_pieces and _persist_preset_chain only inserts slot_order, slot, rs_gear_type, kind, file, params_json, tone3000_id, assigned_mode, bypassed, vst_path, vst_format, vst_state; including them here makes this endpoint fail on existing databases.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@routes.py` around lines 9984 - 10008, Remove gain_db, pan, and phase_inv from
the preset_pieces SELECT, row unpacking, and pieces dictionaries in the copy
path. Keep the selected fields aligned with the columns persisted by
_persist_preset_chain, preserving all other copied gear properties.
| fname_key = _db_song_key(filename) | ||
| target_name = f"{fname_key}::{tone_key}" | ||
| try: | ||
| preset_id = _persist_preset_chain( | ||
| filename=fname_key, tone_key=tone_key, name=target_name, | ||
| pieces=pieces, assigned_mode="manual", | ||
| ) | ||
| except Exception as e: | ||
| log.exception("override_active_tone failed") | ||
| return JSONResponse({"error": f"{type(e).__name__}: {e}"}, 500) | ||
| return {"ok": True, "preset_id": preset_id, "piece_count": len(pieces)} |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "assigned_mode.*'auto'|'auto'.*assigned_mode" routes.py -C3Repository: got-feedBack/feedBack-plugin-rig-builder
Length of output: 1753
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate relevant functions by name and then inspect focused line ranges.
rg -n "def (save_preset|override_active_tone|_persist_preset_chain|_canonical_song_key|_tone_mapping_filename_filter)" routes.py -C 2
# Show focused ranges for the two functions (adjust after grep if needed).
start_save=$(rg -n "def save_preset" routes.py | head -1 | cut -d: -f1)
start_override=$(rg -n "def override_active_tone" routes.py | head -1 | cut -d: -f1)
start_persist=$(rg -n "def _persist_preset_chain" routes.py | head -1 | cut -d: -f1)
echo "---- save_preset around $start_save ----"
sed -n "$((start_save-10)),$((start_save+140))p" routes.py
echo "---- override_active_tone around $start_override ----"
sed -n "$((start_override-20)),$((start_override+80))p" routes.py
echo "---- _persist_preset_chain around $start_persist ----"
sed -n "$((start_persist-20)),$((start_persist+180))p" routes.pyRepository: got-feedBack/feedBack-plugin-rig-builder
Length of output: 24098
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Search for deterministic remap/batch paths that handle assigned_mode or replace pieces.
rg -n "remap|batch|assigned_mode|replace_with|gear/replace_with|replace_with" routes.py -C 4
# Show focused ranges around candidates found, limited to lines.
candidates=$(rg -n "def .*remap|def .*batch|def .*replace_with|assigned_mode|replace_with" routes.py | cut -d: -f1 | sort -n | uniq)
python3 - <<'PY'
lines = sorted({int(ln.split(':')[0].split(',')[0].rstrip()) for ln in __import__('subprocess').check_output(['rg','-n','assigned_mode|replace_with|remap|batch'], 'routes.py').decode().splitlines()})
print('\n'.join(str(l) for l in lines[:40]))
PYRepository: got-feedBack/feedBack-plugin-rig-builder
Length of output: 50396
Mirror tone overrides to the sibling container.
save_preset mirrors the target preset to the same song in the other container format (psarc↔sloppak), but override_active_tone only updates the requested filename. Since this is also a destructive permanent write, mirroring with _canonical_song_key and collecting each sibling preset ID will prevent playback divergence when the sibling file is loaded.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@routes.py` around lines 10009 - 10019, Update override_active_tone to mirror
the permanent tone override across both sibling container formats, matching
save_preset. Use _canonical_song_key to derive the shared song identity,
construct each psarc/sloppak filename, call _persist_preset_chain for both, and
collect their preset IDs. Return the collected IDs while preserving the existing
error handling and piece count response.
| function rbInjectOverrideActiveToneControl(controls, closeBtn) { | ||
| if (document.getElementById('rb-override-active-tone')) { rbUpdateOverrideActiveToneControl(); return; } | ||
| const sel = document.createElement('select'); | ||
| sel.id = 'rb-override-active-tone'; | ||
| sel.className = 'px-2 py-1.5 bg-dark-600 hover:bg-dark-500 rounded-lg text-xs text-gray-300 transition'; | ||
| sel.addEventListener('change', async () => { | ||
| const value = sel.value; | ||
| sel.value = ''; // one-shot picker — always snaps back to the placeholder | ||
| if (!value) return; | ||
| const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function' | ||
| ? window.RbMegaChain.state() : null; | ||
| const activeToneKey = state && state.activeToneKey; | ||
| const cur = window.slopsmith && window.slopsmith.currentSong; | ||
| const filename = cur && cur.filename; | ||
| if (!activeToneKey || !filename) { | ||
| alert('No tone is actively playing right now.'); | ||
| return; | ||
| } | ||
| const isDefault = value === '__default__'; | ||
| const targetLabel = isDefault ? 'your Default tone' : `saved tone "${value}"`; | ||
| if (!confirm(`Permanently replace the CURRENTLY PLAYING tone ("${activeToneKey}") with ${targetLabel}?\n\n` | ||
| + `This overwrites this song's own saved gear for "${activeToneKey}" — there's no built-in undo ` | ||
| + `(re-map/re-batch the song to restore the original).`)) { | ||
| return; | ||
| } | ||
| sel.disabled = true; | ||
| try { | ||
| const r = await fetch(`${window.RB_API}/override_active_tone`, { | ||
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | ||
| body: JSON.stringify({ | ||
| filename, tone_key: activeToneKey, | ||
| source: isDefault ? 'default' : 'saved', | ||
| name: isDefault ? '' : value, | ||
| }), | ||
| }); | ||
| if (!r.ok) { | ||
| const err = await r.json().catch(() => ({})); | ||
| alert(`Override failed: ${err.error || r.status}`); | ||
| return; | ||
| } | ||
| if (typeof RbMegaChain !== 'undefined') RbMegaChain.buildForSong(filename).catch(() => {}); | ||
| } catch (e) { | ||
| alert(`Override failed: ${e.message || e}`); | ||
| } finally { | ||
| sel.disabled = false; | ||
| } | ||
| }); | ||
| if (closeBtn && closeBtn.parentElement === controls) controls.insertBefore(sel, closeBtn); | ||
| else controls.appendChild(sel); | ||
| rbUpdateOverrideActiveToneControl(); | ||
| } | ||
| function rbUpdateOverrideActiveToneControl() { | ||
| const sel = document.getElementById('rb-override-active-tone'); | ||
| if (!sel) return; | ||
| const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function' | ||
| ? window.RbMegaChain.state() : null; | ||
| const activeToneKey = state && state.activeToneKey; | ||
| sel.disabled = !activeToneKey; | ||
| const label = activeToneKey ? `Override "${activeToneKey}" with preset…` : 'Override active tone with preset…'; | ||
| sel.innerHTML = `<option value="">${rbEsc(label)}</option>` | ||
| + `<option value="__default__">→ Default tone</option>` | ||
| + (rbState.savedTones || []).map(t => `<option value="${rbEsc(t.name)}">→ ${rbEsc(t.name)}</option>`).join(''); | ||
| sel.value = ''; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
rbUpdateOverrideActiveToneControl can re-enable the select mid-request.
The change handler sets sel.disabled = true before fetch and clears it in finally (lines 3763, 3782). But rbUpdateOverrideActiveToneControl unconditionally does sel.disabled = !activeToneKey and rebuilds innerHTML (lines 3795-3800), with no awareness of an in-flight request. This function is called from other paths independent of the fetch lifecycle — rbInjectPlayerToneButton's update branch (line 3699, likely on a frequent player-state refresh) and rbStudioLoadSavedTones (line 8732, triggered by unrelated tone-save/delete actions elsewhere in the UI). If either fires while an override POST is in flight, it silently flips the select back to enabled, letting the user fire a second overlapping destructive "permanently replace this tone's gear" request before the first completes.
🔒 Guard the disabled state while a request is pending
sel.disabled = true;
+ sel.dataset.overriding = '1';
try {
const r = await fetch(`${window.RB_API}/override_active_tone`, {
...
});
...
} catch (e) {
alert(`Override failed: ${e.message || e}`);
} finally {
sel.disabled = false;
+ delete sel.dataset.overriding;
}
});
...
}
function rbUpdateOverrideActiveToneControl() {
const sel = document.getElementById('rb-override-active-tone');
if (!sel) return;
+ if (sel.dataset.overriding) return; // don't clobber disabled state mid-request
const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function'
? window.RbMegaChain.state() : null;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function rbInjectOverrideActiveToneControl(controls, closeBtn) { | |
| if (document.getElementById('rb-override-active-tone')) { rbUpdateOverrideActiveToneControl(); return; } | |
| const sel = document.createElement('select'); | |
| sel.id = 'rb-override-active-tone'; | |
| sel.className = 'px-2 py-1.5 bg-dark-600 hover:bg-dark-500 rounded-lg text-xs text-gray-300 transition'; | |
| sel.addEventListener('change', async () => { | |
| const value = sel.value; | |
| sel.value = ''; // one-shot picker — always snaps back to the placeholder | |
| if (!value) return; | |
| const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function' | |
| ? window.RbMegaChain.state() : null; | |
| const activeToneKey = state && state.activeToneKey; | |
| const cur = window.slopsmith && window.slopsmith.currentSong; | |
| const filename = cur && cur.filename; | |
| if (!activeToneKey || !filename) { | |
| alert('No tone is actively playing right now.'); | |
| return; | |
| } | |
| const isDefault = value === '__default__'; | |
| const targetLabel = isDefault ? 'your Default tone' : `saved tone "${value}"`; | |
| if (!confirm(`Permanently replace the CURRENTLY PLAYING tone ("${activeToneKey}") with ${targetLabel}?\n\n` | |
| + `This overwrites this song's own saved gear for "${activeToneKey}" — there's no built-in undo ` | |
| + `(re-map/re-batch the song to restore the original).`)) { | |
| return; | |
| } | |
| sel.disabled = true; | |
| try { | |
| const r = await fetch(`${window.RB_API}/override_active_tone`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| filename, tone_key: activeToneKey, | |
| source: isDefault ? 'default' : 'saved', | |
| name: isDefault ? '' : value, | |
| }), | |
| }); | |
| if (!r.ok) { | |
| const err = await r.json().catch(() => ({})); | |
| alert(`Override failed: ${err.error || r.status}`); | |
| return; | |
| } | |
| if (typeof RbMegaChain !== 'undefined') RbMegaChain.buildForSong(filename).catch(() => {}); | |
| } catch (e) { | |
| alert(`Override failed: ${e.message || e}`); | |
| } finally { | |
| sel.disabled = false; | |
| } | |
| }); | |
| if (closeBtn && closeBtn.parentElement === controls) controls.insertBefore(sel, closeBtn); | |
| else controls.appendChild(sel); | |
| rbUpdateOverrideActiveToneControl(); | |
| } | |
| function rbUpdateOverrideActiveToneControl() { | |
| const sel = document.getElementById('rb-override-active-tone'); | |
| if (!sel) return; | |
| const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function' | |
| ? window.RbMegaChain.state() : null; | |
| const activeToneKey = state && state.activeToneKey; | |
| sel.disabled = !activeToneKey; | |
| const label = activeToneKey ? `Override "${activeToneKey}" with preset…` : 'Override active tone with preset…'; | |
| sel.innerHTML = `<option value="">${rbEsc(label)}</option>` | |
| + `<option value="__default__">→ Default tone</option>` | |
| + (rbState.savedTones || []).map(t => `<option value="${rbEsc(t.name)}">→ ${rbEsc(t.name)}</option>`).join(''); | |
| sel.value = ''; | |
| } | |
| function rbInjectOverrideActiveToneControl(controls, closeBtn) { | |
| if (document.getElementById('rb-override-active-tone')) { rbUpdateOverrideActiveToneControl(); return; } | |
| const sel = document.createElement('select'); | |
| sel.id = 'rb-override-active-tone'; | |
| sel.className = 'px-2 py-1.5 bg-dark-600 hover:bg-dark-500 rounded-lg text-xs text-gray-300 transition'; | |
| sel.addEventListener('change', async () => { | |
| const value = sel.value; | |
| sel.value = ''; // one-shot picker — always snaps back to the placeholder | |
| if (!value) return; | |
| const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function' | |
| ? window.RbMegaChain.state() : null; | |
| const activeToneKey = state && state.activeToneKey; | |
| const cur = window.slopsmith && window.slopsmith.currentSong; | |
| const filename = cur && cur.filename; | |
| if (!activeToneKey || !filename) { | |
| alert('No tone is actively playing right now.'); | |
| return; | |
| } | |
| const isDefault = value === '__default__'; | |
| const targetLabel = isDefault ? 'your Default tone' : `saved tone "${value}"`; | |
| if (!confirm(`Permanently replace the CURRENTLY PLAYING tone ("${activeToneKey}") with ${targetLabel}?\n\n` | |
| `This overwrites this song's own saved gear for "${activeToneKey}" — there's no built-in undo ` | |
| `(re-map/re-batch the song to restore the original).`)) { | |
| return; | |
| } | |
| sel.disabled = true; | |
| sel.dataset.overriding = '1'; | |
| try { | |
| const r = await fetch(`${window.RB_API}/override_active_tone`, { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| filename, tone_key: activeToneKey, | |
| source: isDefault ? 'default' : 'saved', | |
| name: isDefault ? '' : value, | |
| }), | |
| }); | |
| if (!r.ok) { | |
| const err = await r.json().catch(() => ({})); | |
| alert(`Override failed: ${err.error || r.status}`); | |
| return; | |
| } | |
| if (typeof RbMegaChain !== 'undefined') RbMegaChain.buildForSong(filename).catch(() => {}); | |
| } catch (e) { | |
| alert(`Override failed: ${e.message || e}`); | |
| } finally { | |
| sel.disabled = false; | |
| delete sel.dataset.overriding; | |
| } | |
| }); | |
| if (closeBtn && closeBtn.parentElement === controls) controls.insertBefore(sel, closeBtn); | |
| else controls.appendChild(sel); | |
| rbUpdateOverrideActiveToneControl(); | |
| } | |
| function rbUpdateOverrideActiveToneControl() { | |
| const sel = document.getElementById('rb-override-active-tone'); | |
| if (!sel) return; | |
| if (sel.dataset.overriding) return; // don't clobber disabled state mid-request | |
| const state = window.RbMegaChain && typeof window.RbMegaChain.state === 'function' | |
| ? window.RbMegaChain.state() : null; | |
| const activeToneKey = state && state.activeToneKey; | |
| sel.disabled = !activeToneKey; | |
| const label = activeToneKey ? `Override "${activeToneKey}" with preset…` : 'Override active tone with preset…'; | |
| sel.innerHTML = `<option value="">${rbEsc(label)}</option>` | |
| `<option value="__default__">→ Default tone</option>` | |
| (rbState.savedTones || []).map(t => `<option value="${rbEsc(t.name)}">→ ${rbEsc(t.name)}</option>`).join(''); | |
| sel.value = ''; | |
| } |
🧰 Tools
🪛 ast-grep (0.44.1)
[warning] 3796-3798: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: sel.innerHTML = <option value="">${rbEsc(label)}</option>
+ <option value="__default__">→ Default tone</option>
+ (rbState.savedTones || []).map(t => <option value="${rbEsc(t.name)}">→ ${rbEsc(t.name)}</option>).join('')
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').
(inner-outer-html)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@screen.js` around lines 3738 - 3801, Track the override request state for the
select created by rbInjectOverrideActiveToneControl, and update
rbUpdateOverrideActiveToneControl so refreshes preserve sel.disabled while a
request is in flight. Set the pending state before the override_active_tone
fetch and clear it in finally, ensuring unrelated control refreshes cannot
re-enable or permit another submission until the first request completes.
Adds a name-independent way to permanently swap a song's specific tone for another preset's gear, right from the player while the song is playing.
Background
A Clean/OD/Distortion tone-name-classification approach was tried first (matching a song's own
tone_keyagainst those 3 words) but dropped after testing against a real library: RS tone-naming conventions are inconsistent enough ("Clean"/"CLEAN"/"orion_dist"/"bass_base" all coexist in the same collection) that name matching missed or misfired on real charts. This feature is the more universal replacement - it doesn't classify anything by name at all, it just targets whatever tone is actually playing right now.Changes
POST /override_active_tone: given{filename, tone_key, source, name}, clones the chosen preset's ENTIREpreset_pieces(gear, VST state,gain_db/pan/phase_inv- everything) onto that specific song tone's own saved preset, via the existing_persist_preset_chain/_resolve_tone_preset_idprimitives - no new DB schema, no lossy enrich/de-enrich round-trip (reads the source's rawpreset_piecescolumns directly). Same destructive semantics as the existing per-piece "🔁 Swap" button (/gear/replace_with) - this is the whole-tone equivalent, and just as permanent (no separate override layer, no built-in undo).<select>next to "Rig Tones On" in the Plugin Controls panel, labeled with whichevertone_keyis CURRENTLY ACTIVE. Picking "Default tone" or a saved tone shows a confirmation (this is destructive) and, on confirm, POSTs the swap and rebuilds the song's mega-chain so the new gear is heard immediately. Works on anytone_keyregardless of what it's named - the control doesn't need to know or care.Verified live
Enabling this against a real song's actively-playing tone correctly replaced its gear while every other tone in the same song stayed completely untouched.
Test plan
Summary by CodeRabbit
New Features
Bug Fixes