mirror of
https://github.com/got-feedBack/feedBack-desktop.git
synced 2026-07-19 19:31:31 +00:00
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:
parent
72a3f871e5
commit
23047291e3
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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, {});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user