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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user