M7-M16: VST FX GUI native embed hoàn thiện — fix click-2-lần chuyển slot, anchor lần mở đầu, taskbar icon

- app.jsx: masterGuiOpenRef sync truoc setState — guard chuyen slot doc ref, het race closure (click 1 lan chuyen slot)
- JuceFxGuiHost.cpp: M14 applyAnchor truoc setVisible (het flash giua man hinh); M15 clear WS_EX_APPWINDOW + set TOOLWINDOW (het taskbar icon — JUCE set APPWINDOW 0x40000 thang TOOLWINDOW); M9 owner DAW + M9d skip applyOwner khi an
- JuceFxLoop.cpp: M16 bo clearAnchor khi ed.open (retry ed.open ~700ms xoa rect fresh -> window moi khong anchor duoc lan dau mo panel); chi clear khi Close
- JuceFxEngine/app.jsx/plugins.py/fx_realtime.py: editor session realtime, anchor rect routing, retry ed.open
- Rebuild engine PyInstaller + juce_fx_bridge cmake; binaries dong bo 5 noi (bridge 7464a7ef)
This commit is contained in:
2026-09-06 19:45:14 +07:00
parent ec2c925d09
commit ef521d9981
20 changed files with 2040 additions and 461 deletions
+141 -35
View File
@@ -279,23 +279,7 @@ static std::string base64Encode(const uint8_t* data, size_t len) {
return out;
}
// Append-only variant (no return temporary) so it can run inside an SEH
// __try frame (C2712: __try functions cannot have destructor temporaries).
static void base64EncodeTo(const uint8_t* data, size_t len, std::string& out) {
static const char* tbl = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t cap = ((len + 2) / 3) * 4;
out.clear();
out.reserve(cap);
for (size_t i = 0; i < len; i += 3) {
uint32_t n = (uint32_t)data[i] << 16;
if (i + 1 < len) n |= (uint32_t)data[i + 1] << 8;
if (i + 2 < len) n |= (uint32_t)data[i + 2];
out += tbl[(n >> 18) & 63];
out += tbl[(n >> 12) & 63];
out += (i + 1 < len) ? tbl[(n >> 6) & 63] : '=';
out += (i + 2 < len) ? tbl[n & 63] : '=';
}
}
static int b64val(unsigned char c) {
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
@@ -637,6 +621,110 @@ bool vst3FxLoadInner(Vst3FxState* s, const std::string& path, double sampleRate,
}
#endif // HAVE_VST3SDK
// Canonical preset wrapper (PLAN A) — JUCE XML, đúng bytes
// AudioProcessor::copyXmlToBinary(<VST3PluginState>): "VC2!" + LE u32 xmlLen
// + single-line XML text + NUL. XML = <VST3PluginState><IComponent>len-dot
// </IComponent><IEditController>len-dot</IEditController></VST3PluginState>
// (controller element optional). Cả JuceFxEngine::getStateInformation lẫn
// Vst3Fx::captureState xuất wrapper này → 1 format dùng chung mọi đường
// (GUI fallback, offline --render-fx, --realtime-fx, fxrt juce). Parse ngược
// (setState comp/ctrl) trong applyPreset.
//
// Element text = MemoryBlock::toBase64Encoding (JUCE) — KHÔNG phải RFC 4648
// base64! Format: "<số byte>." rồi mỗi ký tự mã 6 bit của state qua bảng
// ".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+" ('.' =
// 6 bit 0; không '/', không '=' padding), đọc LSB-first từng byte như
// getBitRange/setBitRange. Decode ngược fromBase64Encoding: lấy size trước
// '.' đầu tiên, mỗi char c-43 (0..79) → bảng decode rồi ghi 6 bit; char
// ngoài 0..79 bỏ qua (không tốn bit). (juce_MemoryBlock.cpp.)
static const char kJuceB64EncTbl[] =
".ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+";
static const int kJuceB64DecTbl[80] = {
63, 0, 0, 0, 0, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 0, 0, 0, 0, 0, 0, 0,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
22, 23, 24, 25, 26,
0, 0, 0, 0, 0, 0, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40,
41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52
};
// JUCE bit access — LSB-first per byte, giống getBitRange/setBitRange.
static int juceGetBits(const uint8_t* data, size_t size, size_t bitStart, size_t numBits) {
int res = 0;
for (size_t k = 0; k < numBits; ++k) {
size_t bp = bitStart + k;
if ((bp >> 3) >= size) break;
if (data[bp >> 3] & (uint8_t)(1u << (bp & 7))) res |= (int)(1u << k);
}
return res;
}
static void juceSetBits(uint8_t* data, size_t size, size_t bitStart, size_t numBits, int bitsToSet) {
for (size_t k = 0; k < numBits; ++k) {
size_t bp = bitStart + k;
if ((bp >> 3) >= size) break;
if (bitsToSet & (int)(1u << k))
data[bp >> 3] |= (uint8_t)(1u << (bp & 7));
else
data[bp >> 3] &= (uint8_t)~(1u << (bp & 7));
}
}
// Encode state bytes như MemoryBlock::toBase64Encoding (len-dot, LSB-first).
static void juceStateEncode(const std::vector<uint8_t>& data, std::string& out) {
out += std::to_string(data.size());
out += '.';
size_t numChars = ((data.size() << 3) + 5) / 6; // ceil(size*8/6)
out.reserve(out.size() + numChars);
for (size_t i = 0; i < numChars; ++i)
out += kJuceB64EncTbl[juceGetBits(data.data(), data.size(), i * 6, 6)];
}
// Decode text len-dot JUCE; false nếu không phải (vd RFC 4648) — caller
// fallback. Giống fromBase64Encoding: size trước '.', char ngoài bảng skip.
static bool juceStateDecode(const std::string& text, std::vector<uint8_t>& out) {
size_t dot = text.find('.');
if (dot == std::string::npos) return false;
size_t n = 0;
for (size_t i = 0; i < dot; ++i) {
if (text[i] < '0' || text[i] > '9') return false;
n = n * 10 + (size_t)(text[i] - '0');
if (n > (1u << 26)) return false; // 64MB cap
}
out.assign(n, 0); // setSize(n, true): zero-init
size_t pos = 0; // bit offset
for (size_t i = dot + 1; i < text.size(); ++i) {
int c = (int)(unsigned char)text[i] - 43;
if (c >= 0 && c < 80) {
juceSetBits(out.data(), out.size(), pos, 6, kJuceB64DecTbl[c]);
pos += 6;
}
}
return true;
}
static bool juceXmlWrapperToStates(const std::vector<uint8_t>& bytes,
std::vector<uint8_t>& comp,
std::vector<uint8_t>& ctrl) {
if (bytes.size() < 9 || std::memcmp(bytes.data(), "VC2!", 4) != 0) return false;
uint32_t len = ((uint32_t)bytes[4]) | ((uint32_t)bytes[5] << 8) |
((uint32_t)bytes[6] << 16) | ((uint32_t)bytes[7] << 24);
if (len == 0 || (size_t)len > bytes.size() - 8) return false;
std::string xml((const char*)bytes.data() + 8, (size_t)len);
auto pick = [&xml](const char* tag, std::vector<uint8_t>& out) -> int {
std::string open = std::string("<") + tag + ">";
std::string close = std::string("</") + tag + ">";
size_t a = xml.find(open);
if (a == std::string::npos) return 0; // element absent
a += open.size();
size_t b = xml.find(close, a);
if (b == std::string::npos) return -1; // malformed
std::string text = xml.substr(a, b - a);
if (juceStateDecode(text, out)) return 1; // JUCE len-dot (canonical)
return base64Decode(text, out) ? 1 : -1; // fallback RFC 4648
};
comp.clear();
ctrl.clear();
int rc = pick("IComponent", comp);
if (rc < 0) return false;
rc = pick("IEditController", ctrl);
if (rc < 0) return false;
return true;
}
class Vst3Fx {
public:
Vst3Fx() = default;
@@ -764,7 +852,9 @@ public:
#endif
}
// Apply preset: base64 of a raw .vstpreset FILE (VST3 magic + chunk list)
// OR of the bridge state blob [4B BE compLen][comp][4B BE ctrlLen][ctrl].
// OR of the JUCE XML wrapper (VC2! + len + <VST3PluginState>... — canonical
// PLAN A format) OR of the legacy bridge state blob [4B BE compLen][comp]
// [4B BE ctrlLen][ctrl].
bool applyPreset(const std::string& b64) {
#ifndef HAVE_VST3SDK
(void)b64;
@@ -795,8 +885,10 @@ public:
if (!readChunk(Steinberg::Vst::kComponentState, comp) ||
!readChunk(Steinberg::Vst::kControllerState, ctrl))
return false;
} else if (juceXmlWrapperToStates(bytes, comp, ctrl)) {
// JUCE XML wrapper — canonical preset format (PLAN A).
} else {
// Bridge state blob.
// Legacy bridge state blob (backward-compatible).
auto read32 = [&bytes](size_t off) -> uint32_t {
return ((uint32_t)bytes[off] << 24) | ((uint32_t)bytes[off + 1] << 16) |
((uint32_t)bytes[off + 2] << 8) | (uint32_t)bytes[off + 3];
@@ -832,9 +924,11 @@ public:
#endif
}
// Capture the plugin's CURRENT state (component + edit controller) as the
// bridge blob [4B BE compLen][comp][4B BE ctrlLen][ctrl] base64 — same
// format applyPreset accepts. Lets the GUI bridge report knob tweaks so a
// saved project can restore them. Must run on the thread owning the plugin.
// canonical JUCE XML wrapper (VC2! + len + <VST3PluginState>, PLAN A) —
// cùng format JuceFxEngine::getStateInformation; applyPreset + JUCE
// setStateInformation đều đọc được. Lets the GUI bridge report knob
// tweaks so a saved project can restore them. Must run on the thread
// owning the plugin.
void captureState(std::string& out) {
#ifndef HAVE_VST3SDK
(void)out;
@@ -854,18 +948,28 @@ public:
ctrl.assign(stream.getData(), stream.getData() + stream.getSize());
}
if (comp.empty() && ctrl.empty()) return;
std::vector<uint8_t> blob;
auto append32 = [&blob](uint32_t v) {
blob.push_back((uint8_t)(v >> 24));
blob.push_back((uint8_t)(v >> 16));
blob.push_back((uint8_t)(v >> 8));
blob.push_back((uint8_t)v);
};
append32((uint32_t)comp.size());
blob.insert(blob.end(), comp.begin(), comp.end());
append32((uint32_t)ctrl.size());
blob.insert(blob.end(), ctrl.begin(), ctrl.end());
base64EncodeTo(blob.data(), blob.size(), out);
std::string xml = "<VST3PluginState>";
if (!comp.empty()) {
std::string b;
juceStateEncode(comp, b);
xml += "<IComponent>" + b + "</IComponent>";
}
if (!ctrl.empty()) {
std::string b;
juceStateEncode(ctrl, b);
xml += "<IEditController>" + b + "</IEditController>";
}
xml += "</VST3PluginState>";
out.clear();
out.reserve(8 + xml.size() + 1);
out += "VC2!";
uint32_t xlen = (uint32_t)xml.size();
out.push_back((char)(xlen & 0xFF));
out.push_back((char)((xlen >> 8) & 0xFF));
out.push_back((char)((xlen >> 16) & 0xFF));
out.push_back((char)((xlen >> 24) & 0xFF));
out += xml;
out += '\0';
#endif
}
// Live param change (realtime chain, NO restart): address by lowercase
@@ -1960,7 +2064,9 @@ static int fxGuiCreateWindow(const std::string& name, bool offscreen,
RegisterClassA(&wc);
const int x = offscreen ? -20000 : CW_USEDEFAULT;
const int y = offscreen ? -20000 : CW_USEDEFAULT;
const DWORD exStyle = offscreen ? (WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) : 0;
// WS_EX_TOOLWINDOW cả khi onscreen (float/--open-fx-gui): GUI plugin là cửa
// sổ phụ của DAW — không được tạo icon taskbar riêng (user chỉ thấy app).
const DWORD exStyle = (WS_EX_TOOLWINDOW) | (offscreen ? WS_EX_NOACTIVATE : 0);
// Borderless (WS_POPUP): GetWindowRect == client rect, so anchored
// placement over the embed panel is exact. A captioned/bordered window
// (WS_OVERLAPPEDWINDOW) put the client area ~31px lower/right of the