Expose sandboxed VST parameter values via kListParameters

A sandboxed (out-of-process) VST3 exposes no host-side JUCE parameter proxies,
so SignalChain::getParameters returned an empty list for it. That left the
renderer with no way to read a plugin's live values — the plugin's own editor
window could not be mirrored back into the in-app UI, and "Capture state" was a
no-op for sandboxed gear.

- vst-host/main.cpp: implement the (already-declared) op::kListParameters
  handler, replying with each parameter's index/name/value(0..1)/label/text
  from the child's live AudioProcessor.
- SandboxedProcessor::listSandboxedParameters(): blocking control-pipe
  round-trip returning the reply's params array.
- SignalChain::getParameters: resolve the slot under the audio lock, then do
  the sandboxed IPC AFTER releasing it (a blocking round-trip must not stall
  processBlock) — same discipline getStateInformation() already uses.
  In-process plugins still read their JUCE proxies directly under the lock.

Verified end-to-end against a bundled VST3: getParameters now returns live
values and setParameter -> getParameters round-trips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jafz2001 2026-07-15 10:37:35 -04:00
parent 72a3f871e5
commit 23047291e3
4 changed files with 105 additions and 15 deletions

View File

@ -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);

View File

@ -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

View File

@ -712,23 +712,59 @@ juce::Array<const ProcessorSlot*> SignalChain::getAllSlots() const
juce::Array<SignalChain::ParamInfo> SignalChain::getParameters(int slotId) const
{
juce::Array<ParamInfo> 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<slopsmith::sandbox::SandboxedProcessor*>(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;
}

View File

@ -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<juce::var> 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, {});