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:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user