fix(fx-rt): het crackle master FX - limiter attack smooth + crossfade khi bat/tat chain

- NativeInstrumentEngine: kAttack 0.9->0.1 (het zipper khi sum nhieu track)
- app.jsx: crossfade masterFxDryGain/masterFxGain thay hard-rewire graph luc
  activation/deactivation master chain FX; worklet bao 'started' -> ramp dung
  luc; PDC tre sau khi dry silent (tranh gap 80ms)
- sf-fx-realtime.js: fade-in 512 mau luc FILL xong / resume sau underrun
- index.html + addModule: cache-buster v=202608241200
- rebuild app.precompiled.js, copy bundle vao dist/ + src-tauri/resources/
- keo dai fix cac round truoc: FILL 16, FXRT_BURST_MS, REBUFFER, pacing,
  VST GUI click/focus, solo crackle
- them harness + plans/patch scripts
This commit is contained in:
2026-08-24 07:46:09 +07:00
parent 373a96cf7b
commit 10997a65b9
21 changed files with 1181 additions and 177 deletions
+12 -46
View File
@@ -239,21 +239,18 @@ bool captureHwndToJpeg(HWND hwnd, std::vector<unsigned char>& out, int quality)
return !out.empty();
}
// --- real-input fallback ------------------------------------------------------
// Some editors (JUCE, iZotope) reject synthetic PostMessage input entirely and
// only react to real hardware input while their window is visible. Probe: if 2
// consecutive clicks do not change the frame, switch this plugin to real-input
// mode — briefly move the window topmost to the work-area corner, inject real
// mouse events at the mapped screen position, then restore it off-screen.
std::atomic<bool> g_realInput{false}; // plugin needs real hardware input
// probe: hit (frame changed) resets g_miss; 2 consecutive misses switch
// to real input. A one-off "hit" (e.g. initial settle repaint) must NOT
// lock us onto PostMessage, so there is no permanent "detected" state.
// --- real-input --------------------------------------------------------------
// JUCE/iZotope editors reject synthetic PostMessage input entirely and only
// react to real hardware input while their window is visible, so real input
// is always on: on click the plugin window dances over the embed panel rect
// (anchored) and real mouse events are injected at the mapped screen pos.
// Probe-based fallback (4bb0db2 era) removed: frame-hash probing was defeated
// by animated GUIs (meter repaint) — the frame always changes, g_miss never
// reached 2, PostMessage stayed on and every click silently died.
std::atomic<bool> g_realInput{true}; // always real hardware input
std::atomic<bool> g_dance{false}; // window temporarily on-screen/topmost
std::atomic<DWORD> g_parkUntil{0}; // safety restore deadline (GetTickCount)
POINT g_savedCursor = {0, 0}; // cursor to restore after the dance
int g_miss = 0; // consecutive no-response PostMessage clicks
uint64_t g_preClickHash = 0;
// Anchored mode: the frontend sends the embed panel's screen rect (physical
// px) with every /input, so the real-input dance places the plugin window
// exactly over the panel -- cursor never moves, no park/restore churn. Zero
@@ -261,13 +258,6 @@ uint64_t g_preClickHash = 0;
RECT g_embedRect = {0, 0, 0, 0};
bool g_anchored = false; // window anchored over panel (stays until /close)
uint64_t frameHash(HWND hwnd) {
std::vector<unsigned char> jpg;
if (!captureHwndToJpeg(hwnd, jpg, 70)) return 0;
uint64_t h = 1469598103934665603ull; // FNV-1a 64
for (unsigned char b : jpg) { h ^= b; h *= 1099511628211ull; }
return h;
}
// Lift the window above the foreground app without making it TOPMOST (the
// foreground-lock would otherwise swallow real clicks). Attaching to the
@@ -390,25 +380,6 @@ void relayInput(HWND hwnd, const json_object_s* o) {
// Mouseleave reports -1,-1: finish the drag without clicking a bogus point.
const bool outside = x < 0 || y < 0;
// Probe while still on PostMessage: a changed frame means PostMessage
// responds (reset miss); two consecutive no-response clicks mean the
// editor rejects synthetic input — switch to real hardware input.
if (!g_realInput) {
if (isDown) {
g_preClickHash = frameHash(hwnd);
} else if (isUp && g_preClickHash) {
Sleep(250);
uint64_t nowHash = frameHash(hwnd);
bool resp = nowHash != g_preClickHash;
if (resp) {
g_miss = 0; // PostMessage responded — keep it
} else if (++g_miss >= 2) {
g_realInput = true; // synthetic input rejected — go real hardware input
g_miss = 0;
}
g_preClickHash = 0;
}
}
if (g_realInput && (isDown || isUp || type == "mousemove" || type == "wheel")) {
if (isDown) {
@@ -583,17 +554,12 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
if (!IsWindowVisible(hwnd))
SetWindowPos(hwnd, HWND_NOTOPMOST, -20000, -20000, 0, 0,
SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
if (!g_realInput) {
// Real-input active: the dance/anchor state is owned by the
// input relay — /show must not yank the window off-screen.
g_dance = false;
g_anchored = false;
g_parkUntil = 0;
}
// Dance/anchor state is owned by the input relay — /show must not
// yank the window off-screen (real input is always on).
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/preset") {
std::string b64;
fxGuiRequestCapture(hwnd, b64, 1500);
fxGuiRequestCapture(hwnd, b64, 500); // 1500 blocked the HTTP worker, stalling /input (clicks)
const std::string body = "{\"preset_b64\":\"" + b64 + "\"}";
sendResponse(c, 200, "application/json", body.data(), (int)body.size());
} else if (req.path == "/shm") {