diff --git a/src/audio/Sandbox/SandboxedProcessor.cpp b/src/audio/Sandbox/SandboxedProcessor.cpp index fabed7d..14c4e6f 100644 --- a/src/audio/Sandbox/SandboxedProcessor.cpp +++ b/src/audio/Sandbox/SandboxedProcessor.cpp @@ -422,6 +422,21 @@ void SandboxedProcessor::setSandboxedParameter(int index, float value) control->postNoReply(op::kSetParameter, juce::var(args.get())); } +juce::var SandboxedProcessor::listSandboxedParameters() +{ + if (!control || !isAlive()) return {}; + juce::String err; + auto reply = control->request(op::kListParameters, {}, + kDefaultReplyTimeoutMs, &err); + if (err.isNotEmpty()) + { + VST_TRACE("[sandbox] listSandboxedParameters request failed: %s", + err.toRawUTF8()); + return {}; + } + return reply.getProperty("params", juce::var()); +} + bool SandboxedProcessor::isAlive() const noexcept { return alive.load(std::memory_order_acquire); diff --git a/src/audio/Sandbox/SandboxedProcessor.h b/src/audio/Sandbox/SandboxedProcessor.h index 4c03393..241ff5d 100644 --- a/src/audio/Sandbox/SandboxedProcessor.h +++ b/src/audio/Sandbox/SandboxedProcessor.h @@ -68,6 +68,14 @@ public: // Forward a parameter change to the sandboxed plugin over the control pipe. void setSandboxedParameter(int index, float value); + // Snapshot the sandboxed plugin's CURRENT parameter values (a blocking + // control-pipe round-trip). Returns the reply's "params" array var — one + // object per param: { index, name, value (normalized 0..1), label, text } — + // or an empty var on failure. A sandboxed plugin exposes no host-side JUCE + // parameter proxies, so this is how the host reads values the user changed in + // the plugin's own editor window. Call OFF the audio lock (blocking IPC). + juce::var listSandboxedParameters(); + // Callback fired when the subprocess unexpectedly exits or its control // pipe breaks. Always invoked from a background thread; mutex-guarded // so concurrent assignment from the owner thread + read from the I/O diff --git a/src/audio/SignalChain.cpp b/src/audio/SignalChain.cpp index 8f85c8a..67c443b 100644 --- a/src/audio/SignalChain.cpp +++ b/src/audio/SignalChain.cpp @@ -712,23 +712,59 @@ juce::Array SignalChain::getAllSlots() const juce::Array SignalChain::getParameters(int slotId) const { juce::Array result; - const juce::ScopedLock sl(lock); - int idx = findSlotIndex(slotId); - if (idx < 0) return result; - auto* proc = slots[idx]->processor.get(); - if (!proc) return result; - - auto& params = proc->getParameters(); - for (int i = 0; i < params.size(); ++i) + // Resolve the processor under the lock, then act on it. For a SANDBOXED + // plugin the value read is a blocking control-pipe round-trip, which must + // NOT run while holding the audio lock (it would stall processBlock for the + // reply timeout), so we drop the lock before the IPC. Slot add/remove and + // this getter both run on the host's main thread, so the pointer stays valid + // after the lock is released — the same discipline getStateInformation() + // relies on for its off-lock kGetState round-trip. + slopsmith::sandbox::SandboxedProcessor* sandboxed = nullptr; { - ParamInfo info; - info.index = i; - info.name = params[i]->getName(128); - info.value = params[i]->getValue(); - info.label = params[i]->getLabel(); - info.text = params[i]->getCurrentValueAsText(); - result.add(info); + const juce::ScopedLock sl(lock); + int idx = findSlotIndex(slotId); + if (idx < 0) return result; + + auto* proc = slots[idx]->processor.get(); + if (!proc) return result; + + sandboxed = dynamic_cast(proc); + if (sandboxed == nullptr) + { + // In-process plugin: JUCE parameter proxies are live — read them + // directly under the lock (cheap, no IPC). + auto& params = proc->getParameters(); + for (int i = 0; i < params.size(); ++i) + { + ParamInfo info; + info.index = i; + info.name = params[i]->getName(128); + info.value = params[i]->getValue(); + info.label = params[i]->getLabel(); + info.text = params[i]->getCurrentValueAsText(); + result.add(info); + } + return result; + } + } + + // Sandboxed plugin: no host-side JUCE parameter proxies exist, so ask the + // subprocess for its live values over the control pipe (off the audio lock). + juce::var params = sandboxed->listSandboxedParameters(); + if (auto* arr = params.getArray()) + { + for (const auto& pv : *arr) + { + ParamInfo info; + info.index = (int) pv.getProperty("index", -1); + info.name = pv.getProperty("name", juce::String()).toString(); + info.value = (float) (double) pv.getProperty("value", 0.0); + info.label = pv.getProperty("label", juce::String()).toString(); + info.text = pv.getProperty("text", juce::String()).toString(); + if (info.index >= 0) + result.add(info); + } } return result; } diff --git a/src/vst-host/main.cpp b/src/vst-host/main.cpp index 44af005..ecfd024 100644 --- a/src/vst-host/main.cpp +++ b/src/vst-host/main.cpp @@ -1266,6 +1266,37 @@ void dispatchRequest(HostState& st, int requestId, const juce::String& op, params[idx]->setValue((float)juce::jlimit(0.0, 1.0, vald)); reply(true, {}); } + else if (op == op::kListParameters) + { + if (!st.plugin) + { + reply(false, {}, "no plugin loaded"); + return; + } + // Snapshot the plugin's CURRENT parameter values so the host can mirror + // edits the user made in the plugin's OWN editor window back into the + // in-app UI (a sandboxed plugin exposes no host-side JUCE parameter + // proxies, so this pipe round-trip is the only way the host reads them). + // Read-only; mirrors the in-process SignalChain::getParameters fields. + // getValue() is normalized 0..1 — same range setParameter takes. + auto params = st.plugin->getParameters(); + juce::Array out; + out.ensureStorageAllocated(params.size()); + for (int i = 0; i < params.size(); ++i) + { + auto* p = params[i]; + juce::DynamicObject::Ptr po(new juce::DynamicObject()); + po->setProperty("index", i); + po->setProperty("name", p->getName(128)); + po->setProperty("value", (double) p->getValue()); + po->setProperty("label", p->getLabel()); + po->setProperty("text", p->getCurrentValueAsText()); + out.add(juce::var(po.get())); + } + juce::DynamicObject::Ptr res(new juce::DynamicObject()); + res->setProperty("params", juce::var(out)); + reply(true, juce::var(res.get())); + } else if (op == op::kShutdown) { reply(true, {});