C: realtime VST3 setParam live — Vst3Fx::setParam (title/shortTitle->ParamID, setParamNormalized + inputParameterChanges queue), RealtimeFxChain dispatch vst3, test_60 delay 1s->0.25s giua stream khong restart

This commit is contained in:
2026-08-23 07:36:43 +07:00
parent 17e22bcf32
commit 35076f680b
2 changed files with 131 additions and 1 deletions
+82 -1
View File
@@ -21,6 +21,7 @@
#include "public.sdk/source/vst/hosting/module.h" #include "public.sdk/source/vst/hosting/module.h"
#include "public.sdk/source/vst/hosting/hostclasses.h" #include "public.sdk/source/vst/hosting/hostclasses.h"
#include "public.sdk/source/vst/hosting/processdata.h" #include "public.sdk/source/vst/hosting/processdata.h"
#include "public.sdk/source/vst/hosting/parameterchanges.h"
#include "public.sdk/source/vst/vstpresetfile.h" #include "public.sdk/source/vst/vstpresetfile.h"
#include "public.sdk/source/common/memorystream.h" #include "public.sdk/source/common/memorystream.h"
#include "pluginterfaces/vst/ivstaudioprocessor.h" #include "pluginterfaces/vst/ivstaudioprocessor.h"
@@ -54,6 +55,7 @@
#include <condition_variable> #include <condition_variable>
#include <functional> #include <functional>
#include <iterator> #include <iterator>
#include <map>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
@@ -388,6 +390,9 @@ struct Vst3FxState {
bool controllerIsComponent = false; bool controllerIsComponent = false;
Steinberg::IPlugFrame* plugFrame = nullptr; // owned; freed in closeEditor Steinberg::IPlugFrame* plugFrame = nullptr; // owned; freed in closeEditor
IPtr<IPlugView> view; // editor view while GUI open IPtr<IPlugView> view; // editor view while GUI open
Steinberg::Vst::ParameterChanges paramChanges; // live setParam queue -> inputParameterChanges
std::mutex paramMutex; // setParam (loop/worker) vs processAudio
std::map<std::string, Steinberg::Vst::ParamID> paramByName; // lower(title/shortTitle) -> id
}; };
// Class selection for FX: any kVstAudioEffectClass whose subcategory does NOT // Class selection for FX: any kVstAudioEffectClass whose subcategory does NOT
@@ -534,6 +539,25 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0) if (component->getBusInfo(kAudio, kOutput, 0, bi) == kResultOk && bi.channelCount > 0)
s->outputChannels = bi.channelCount; s->outputChannels = bi.channelCount;
// Live setParam: map lowercased title/shortTitle -> ParamID so the
// realtime chain can address VST3 params by name (no UI-side tags).
// Titles are UTF-16 (String128); keep the ASCII subset lowercased.
for (int32 pi = 0; pi < controller->getParameterCount(); ++pi) {
Steinberg::Vst::ParameterInfo info = {};
if (controller->getParameterInfo(pi, info) != kResultOk) continue;
auto addName = [&](const Steinberg::Vst::TChar* nm) {
if (!nm) return;
std::string k;
for (const Steinberg::Vst::TChar* p = nm; p && *p && k.size() < 64; ++p) {
if (*p < 0x80) k.push_back((char)std::tolower((unsigned char)*p));
}
if (k.empty()) return;
s->paramByName[k] = info.id;
};
addName(info.title);
addName(info.shortTitle);
}
s->module = std::move(module); s->module = std::move(module);
s->component = std::move(component); s->component = std::move(component);
s->controller = std::move(controller); s->controller = std::move(controller);
@@ -599,7 +623,10 @@ public:
s->processData.processMode = kRealtime; s->processData.processMode = kRealtime;
s->processData.numSamples = (int32)n; s->processData.numSamples = (int32)n;
s->processData.inputEvents = nullptr; s->processData.inputEvents = nullptr;
s->processData.inputParameterChanges = nullptr; {
std::lock_guard<std::mutex> lk(s->paramMutex);
s->processData.inputParameterChanges = &s->paramChanges;
}
s->processData.processContext = &s->processContext; s->processData.processContext = &s->processContext;
s->processContext.projectTimeSamples += n; s->processContext.projectTimeSamples += n;
s->processContext.projectTimeMusic = s->processContext.projectTimeMusic =
@@ -640,6 +667,12 @@ public:
std::memset(outL, 0, n * sizeof(float)); std::memset(outL, 0, n * sizeof(float));
std::memset(outR, 0, n * sizeof(float)); std::memset(outR, 0, n * sizeof(float));
} }
{
// Each block consumes the queue once; clear so the next setParam
// writes fresh points (adelay reads the LAST point).
std::lock_guard<std::mutex> lk(s->paramMutex);
s->paramChanges.clearQueue();
}
#endif #endif
} }
// Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list) // Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list)
@@ -745,6 +778,52 @@ public:
append32((uint32_t)ctrl.size()); append32((uint32_t)ctrl.size());
blob.insert(blob.end(), ctrl.begin(), ctrl.end()); blob.insert(blob.end(), ctrl.begin(), ctrl.end());
base64EncodeTo(blob.data(), blob.size(), out); base64EncodeTo(blob.data(), blob.size(), out);
#endif
}
// Live param change (realtime chain, NO restart): address by lowercase
// title/shortTitle or numeric ParamID; value normalized 0..1. Delivered
// via setParamNormalized (controller/UI state) + inputParameterChanges
// queue (split plugins read the queue in process() — same rule as
// Vst3Instrument). ponytail: beginEdit/endEdit automation gestures when a
// plugin ignores setParamNormalized (fallback: restart with new preset).
bool setParam(const std::string& key, double value) {
#ifndef HAVE_VST3SDK
(void)key; (void)value;
return false;
#else
auto* s = static_cast<Vst3FxState*>(state_);
if (!s || !s->controller) return false;
Steinberg::Vst::ParamID tag = 0;
bool found = false;
try { // numeric ParamID passes through
size_t consumed = 0;
unsigned long num = std::stoul(key, &consumed);
if (consumed == key.size()) { tag = (Steinberg::Vst::ParamID)num; found = true; }
} catch (...) {}
if (!found) {
std::string k = key;
std::transform(k.begin(), k.end(), k.begin(),
[](unsigned char c) { return (char)std::tolower(c); });
auto it = s->paramByName.find(k);
if (it == s->paramByName.end()) {
std::cerr << "[RealtimeFx] vst3 setParam unknown key: " << key << std::endl;
return false;
}
tag = it->second;
}
Steinberg::Vst::ParamValue v =
(Steinberg::Vst::ParamValue)std::max(0.0, std::min(1.0, value));
{
std::lock_guard<std::mutex> lk(s->paramMutex);
s->controller->setParamNormalized(tag, v);
int32 idx = 0;
if (Steinberg::Vst::IParamValueQueue* q =
s->paramChanges.addParameterData(tag, idx))
q->addPoint(0, v, idx);
}
std::cout << "[RealtimeFx] vst3 setParam '" << key << "' -> tag " << tag
<< " = " << v << std::endl;
return true;
#endif #endif
} }
// Native editor: attach the plugin's IPlugView to a host window (must run // Native editor: attach the plugin's IPlugView to a host window (must run
@@ -1233,6 +1312,7 @@ void RealtimeFxChain::setParam(int slot, const std::string& key, double value) {
if (c && slot >= 0 && (size_t)slot < c->entries.size()) { if (c && slot >= 0 && (size_t)slot < c->entries.size()) {
auto& e = c->entries[(size_t)slot]; auto& e = c->entries[(size_t)slot];
if (e.bfx) e.bfx->setParam(key, value); if (e.bfx) e.bfx->setParam(key, value);
else if (e.fx) e.fx->setParam(key, value);
return; return;
} }
// Chain chưa swap (SET_PARAM tới trước setChain) — stash, worker áp sau swap. // Chain chưa swap (SET_PARAM tới trước setChain) — stash, worker áp sau swap.
@@ -1343,6 +1423,7 @@ void RealtimeFxChain::Impl::workerLoop() {
if (p.slot < 0 || (size_t)p.slot >= chain_->entries.size()) continue; if (p.slot < 0 || (size_t)p.slot >= chain_->entries.size()) continue;
auto& e = chain_->entries[(size_t)p.slot]; auto& e = chain_->entries[(size_t)p.slot];
if (e.bfx) e.bfx->setParam(p.key, p.value); if (e.bfx) e.bfx->setParam(p.key, p.value);
else if (e.fx) e.fx->setParam(p.key, p.value);
} }
pending_.clear(); pending_.clear();
} }
+49
View File
@@ -528,3 +528,52 @@ def test_59_limiter_brickwall_oversample(fx_bridge):
pk_low = float(np.max(np.abs(out_low))) pk_low = float(np.max(np.abs(out_low)))
assert 0.05 <= pk_low <= 0.15, f"low-level peak {pk_low:.4f} lech unity" assert 0.05 <= pk_low <= 0.15, f"low-level peak {pk_low:.4f} lech unity"
# ── 5.C Realtime VST3 setParam live (PLAN_MASTER_ENHANCE C) ───────────────
def test_60_vst3_setparam_live(fx_bridge, adelay, monkeypatch):
"""set_param runtime cho VST3: chain [vst3-delay, builtin limiter], đổi
param 'delay' giữa stream 1.0 → 0.25 → echo dịch ~1s → ~0.25s NGAY trên
session hiện tại (restart → plugin default 1s → echo vẫn ở 1s)."""
monkeypatch.setenv("SF_FX_BRIDGE_PATH", fx_bridge)
from app.core import fx_realtime
chain = [
{"type": "vst3", "path": adelay},
{"type": "builtin", "id": "limiter", "params": {"ceiling": -3.0}},
]
sess = fx_realtime.start_session(chain, SR)
try:
out = bytearray()
nblk = 280 # 1.24s — đủ phủ vùng echo cũ (1s) để chứng minh không còn
for i in range(nblk):
blk = np.zeros((2, 256), dtype=np.float32)
if i == 103: # impulse2 tại 0.6s (26368)
blk[0, 0] = 1.0
blk[1, 0] = 1.0
if i == 86: # 0.5s — setParam VST3 'delay' 1.0 -> 0.25 (normalized)
sess.set_param(0, "delay", 0.25)
inter = np.empty(512, dtype=np.float32)
inter[0::2] = blk[0]
inter[1::2] = blk[1]
sess.write_input(inter.tobytes())
time.sleep(0.006)
for b in sess.read_output():
out += b
time.sleep(0.05)
for b in sess.read_output():
out += b
finally:
sess.close()
arr = np.frombuffer(bytes(out), dtype=np.float32)
assert arr.size > 0, "realtime loop không ra output"
got = arr.reshape(-1, 2).T
def _peak(lo, hi):
if lo >= got.shape[1]:
return 0.0
return float(np.max(np.abs(got[:, lo:hi])))
e2 = 103 * 256 + int(0.25 * SR) # echo delay 0.25s ≈ 37393
assert _peak(e2 - 400, e2 + 400) > 0.5, f"echo delay 0.25s không thấy tại {e2} (peak {_peak(e2-400, e2+400):.3f})"
e_old = 103 * 256 + SR # echo delay cũ 1s ≈ 70468 (restart/no-apply)
assert _peak(e_old - 400, e_old + 400) < 0.3, "param không áp live — echo vẫn ở delay 1s"
assert _peak(0, e2 - 700) < 0.3, "có tín hiệu lạ trước echo 0.25s"