From b2be91808cd9d61f297aba5509d4933be805c6c7 Mon Sep 17 00:00:00 2001 From: Jorge Fritis <120731233+Jafz2001@users.noreply.github.com> Date: Thu, 9 Jul 2026 10:14:14 -0400 Subject: [PATCH] fix(audio): accept standard base64 for IR/NAM slot state (was silently dropped) (#88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MemoryBlock::fromBase64Encoding only parses JUCE's proprietary "." format and returns false on standard RFC-4648 base64 — which is what the Python-side plugins (rig_builder) emit for per-slot state. That silent false meant LoadPresetWorker/SetSlotState never called setState() for those slots, so IR stages lost their per-stage `gain` (cab loudness makeup, amp-trim impulse compensation) on every chain load. Add decodeStateBlob(): JUCE format first (engine-native saves unchanged), then a standard-base64 fallback via juce::Base64 — gated to IR/NAM slots only, whose processors take exactly the JSON these states carry ({"irPath","gain"} / {"modelPath",...}). VST slots keep the JUCE-only decode: their plugin-emitted blobs are metadata wrappers, not real setStateInformation() chunks. Co-authored-by: Jafz2001 Co-authored-by: Claude Fable 5 --- src/audio/NodeAddon.cpp | 41 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/src/audio/NodeAddon.cpp b/src/audio/NodeAddon.cpp index 971554d..800dc1e 100644 --- a/src/audio/NodeAddon.cpp +++ b/src/audio/NodeAddon.cpp @@ -54,6 +54,32 @@ static void cancelAllPendingLoads(); static std::shared_ptr engine; static std::mutex engineMutex; +// Decode a state blob that may be in EITHER base64 flavour. JUCE's +// MemoryBlock::fromBase64Encoding only understands JUCE's own proprietary +// format (".") and returns false for standard RFC-4648 +// base64 — which is what the Python-side plugins (rig_builder et al.) emit +// for per-slot state. That silent false meant setState() was never called +// for those slots: IR stages lost their per-stage `gain` (the cab loudness +// makeup and amp trims never reached the engine). Try the JUCE format first +// (engine-native saves), then fall back to standard b64. +// +// `allowStandard` is only set for IR/NAM slots: their processors take a JSON +// state ({"irPath","gain"} / model path), which is exactly what the plugins +// emit. VST slots keep the JUCE-only decode — their plugin-emitted blobs are +// metadata wrappers, not real setStateInformation() chunks, and feeding those +// to a VST3 for the first time would be an unasked-for behaviour change. +static bool decodeStateBlob(const juce::String& s, juce::MemoryBlock& mb, + bool allowStandard) +{ + if (mb.fromBase64Encoding(s) && mb.getSize() > 0) + return true; + if (!allowStandard) + return false; + mb.reset(); + juce::MemoryOutputStream mo(mb, false); + return juce::Base64::convertFromBase64(mo, s) && mb.getSize() > 0; +} + static std::shared_ptr snapshotEngine() { std::lock_guard lock(engineMutex); @@ -3079,8 +3105,12 @@ static Napi::Value SetSlotState(const Napi::CallbackInfo& info) { int slotId = info[0].As().Int32Value(); auto base64 = info[1].As().Utf8Value(); + const auto* slot = liveEngine->getSignalChain().getSlot(slotId); + const bool allowStandard = slot != nullptr + && (slot->type == ProcessorSlot::Type::IR + || slot->type == ProcessorSlot::Type::NAM); juce::MemoryBlock mb; - if (mb.fromBase64Encoding(juce::String(base64)) && mb.getSize() > 0) + if (decodeStateBlob(juce::String(base64), mb, allowStandard)) liveEngine->getSignalChain().setSlotState(slotId, mb); } return info.Env().Undefined(); @@ -3305,11 +3335,16 @@ public: liveEngine->getSignalChain().setBranchSrc(slotId, (int)slotObj->getProperty("branchSrc")); } - // Restore processor state + // Restore processor state (JUCE-format base64; IR/NAM slots also + // accept standard base64 — see decodeStateBlob: their plugin- + // emitted JSON states were silently dropped before, so IR stages + // never got their per-stage gain). if (stateB64.isNotEmpty() && slotId >= 0) { + const bool allowStandard = type == (int)ProcessorSlot::Type::IR + || type == (int)ProcessorSlot::Type::NAM; juce::MemoryBlock state; - if (state.fromBase64Encoding(stateB64)) + if (decodeStateBlob(stateB64, state, allowStandard)) { auto* slot = const_cast(liveEngine->getSignalChain().getSlot(slotId)); if (slot) slot->setState(state);