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
+41
View File
@@ -156,6 +156,23 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
closeShm(v);
return 2;
}
// Chờ chain ĐẦU TIÊN được worker build xong (gen>=1) trước khi READY.
// setChain() là ASYNC — worker thread load VST3 + preset mất 1-4s; nếu set
// READY ngay, engine mở WS và audio chảy qua chain RỖNG (passthrough), rồi
// worker swap chain GIỮA STREAM khi load xong → jump dry→VST + latency đổi
// đột ngột → crackle. Chờ swap xong: engine không gửi audio tới khi chain
// thật sự sẵn sàng. (Engine wait_ready timeout = 30s > deadline 28s.)
const auto chainDeadline = std::chrono::steady_clock::now() +
std::chrono::seconds(28);
while (chain.chainGen() == 0 &&
std::chrono::steady_clock::now() < chainDeadline)
sleepMs(10);
if (chain.chainGen() == 0)
std::cerr << "[RealtimeFxLoop] chain load TIMEOUT 28s — READY anyway"
<< std::endl;
else
std::cerr << "[RealtimeFxLoop] chain ready (gen=" << chain.chainGen()
<< ")" << std::endl;
ipc->h.state = FXRT_STATE_READY;
std::cerr << "[RealtimeFxLoop] ready sr=" << sampleRate << " block=" << block
<< " chain=" << (chainJson.empty() ? "(empty)" : "loaded") << std::endl;
@@ -168,6 +185,12 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
}
});
// Chain update (set_chain): engine ghi lại job file seq++ — poll mỗi
// 500ms; seq đổi → setChain ASYNC (worker load chain mới, swap atomic
// dưới mutex — audio thread copy shared_ptr, không dừng) → load/đổi VST
// FX + đổi preset GUI không ngắt âm/crackle.
uint64_t lastSeq = (uint64_t)jsonNumber(job, "seq", 0);
auto lastPoll = std::chrono::steady_clock::now();
const uint32_t n = block;
float L[FXRT_BLOCK * FXRT_IN_SLOTS], R[FXRT_BLOCK * FXRT_IN_SLOTS];
uint32_t inMask = ipc->h.inSlots - 1;
@@ -179,6 +202,24 @@ int run_realtime_fx_loop(const std::string& jobPath, const std::string& shmName,
std::cerr << "[RealtimeFxLoop] parent gone — exiting" << std::endl;
break;
}
{
const auto nowT = std::chrono::steady_clock::now();
if (std::chrono::duration_cast<std::chrono::milliseconds>(nowT - lastPoll).count() >= 500) {
lastPoll = nowT;
const std::string j2 = readFile(jobPath);
if (!j2.empty()) {
const uint64_t s2 = (uint64_t)jsonNumber(j2, "seq", 0);
if (s2 != lastSeq) {
const std::string c2 = extractChainJson(j2);
if (!c2.empty()) {
lastSeq = s2;
std::cerr << "[RealtimeFxLoop] chain update seq=" << s2 << std::endl;
chain.setChain(c2, sampleRate, (int32_t)batchN);
}
}
}
}
}
// SET_PARAM ring (engine -> bridge, Phase 2.8): drain mỗi iteration —
// áp dụng vào chain TRƯỚC block kế tiếp. Ring đầy → drop lệnh cũ.
while (ipc->h.ctrlRead < ipc->h.ctrlWrite) {