fix(bridge): load dung VSTi theo track (V9) - bo early-return mui theo flag, load qua Tauri invoke khi co path, guard path sai (WaveObserver vs Nexus), chi mo GUI khi load OK; sync 6 ban JS + index.html ?v; kem cac fix bridge truoc (bugs 1-4, GUI restore)

This commit is contained in:
2026-08-18 16:56:05 +07:00
parent cbcc1f658c
commit 351ee56080
11 changed files with 2234 additions and 2019 deletions
+2 -2
View File
@@ -39,7 +39,7 @@ def find_bridge_exe() -> str:
return ""
def normalize_audio_peak(audio, target_db=-3.0):
def normalize_audio_peak(audio, target_db=0.0):
"""Scale audio (numpy array, peak) về target_db nếu vượt; giữ nguyên nếu dưới.
Chống clip do note MIDI chồng (polyphony) — spec midi_polyphony_loudness §II:
@@ -57,7 +57,7 @@ def normalize_audio_peak(audio, target_db=-3.0):
return audio
def normalize_peak_file(path, target_db=-3.0):
def normalize_peak_file(path, target_db=0.0):
"""Post-render WAV: scan peak, vượt target_db → scale toàn file (giữ format)."""
try:
info = sf.info(path)
+87 -17
View File
@@ -795,6 +795,21 @@ function makeDistortionCurve(k) {
return curve;
}
function makeSoftClipCurve(percent) {
const n = 4096;
const curve = new Float32Array(n);
const p = Math.max(0, Math.min(100, percent));
const t = 1 - (p / 100) * 0.8;
const inv = 1 - t;
for (let i = 0; i < n; ++i) {
const x = (i * 2) / (n - 1) - 1;
const ax = Math.abs(x);
if (ax < t) curve[i] = x;
else curve[i] = Math.sign(x) * (t + inv * Math.tanh((ax - t) / inv));
}
return curve;
}
function applyMasteringSettings(s) {
console.log('[Mastering] applyMasteringSettings active=', !!(s && s.masterConnected), 'bypass=', !!(s && s.isBypassed), 'eqActive=', !!(s && s.eqActive), 'masterBus=', !!masterBus);
if (!masterBus || !audioCtx || !s) return;
@@ -858,10 +873,11 @@ function applyMasteringSettings(s) {
const boostLinear = (s.maximizerActive) ? Math.pow(10, clamp(s.maxGain, -60, 30) / 20) : 1.0;
masterBus.maximizerBoostGain.gain.setTargetAtTime(boostLinear, now, 0.01);
// Soft Clipper (identity passthrough when off — never null curve)
// Soft Clipper (identity passthrough when off — never null curve).
// Unity-gain curve: bo gain an cua atan (slope ~2.1x cu cong +6 dB len
// maxGain -> tin hieu >~-12 dBFS bi hard-clip meo "be").
if (s.maximizerActive && s.maxSoftClip > 0) {
const k = 1 + (clamp(s.maxSoftClip, 0, 100) / 100) * 10;
masterBus.maximizerSoftClipper.curve = makeDistortionCurve(k);
masterBus.maximizerSoftClipper.curve = makeSoftClipCurve(s.maxSoftClip);
} else {
masterBus.maximizerSoftClipper.curve = new Float32Array([-1, 1]);
}
@@ -1717,6 +1733,7 @@ function getAudioContext() {
await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate', { sampleRate: audioCtx.sampleRate });
// Bridge restart → mất hết instrument → reset map để reconnect reload.
window.__bridgeLoadedChannels = {};
window.__bridgeLoadedPaths = {};
for (var retry = 0; retry < 10; retry++) {
await new Promise(r => setTimeout(r, 100));
var st2 = await window.NativeBridgeService.queryStatus();
@@ -6439,13 +6456,23 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }
// reconnect + nút GUI (ensure-load rồi mới open — tránh nền trắng khi bridge
// restart làm mất instrument). __bridgeLoadedChannels (ch→true) đánh dấu đã
// load trong session; reset khi bridge down/restart → load lại đúng lúc.
// V9: track.plugin_path/cache path có thể là plugin KHÁC instrumentId (VD
// WaveObserver trong khi track là Nexus) → basename không chứa tên id → trả
// null để engine resolve theo name / listPlugins — tránh LOAD nhầm VSTi.
const _bridgeSafeVstPath = (path, instrumentId) => {
if (!path || !instrumentId) return path;
const base = String(path).split(/[\\/]/).pop().toLowerCase().replace(/\s+/g, '');
const want = String(instrumentId).toLowerCase().replace(/\s+/g, '');
return base.indexOf(want) !== -1 ? path : null;
};
const loadVstToBridge = async (track) => {
if (!window.NativeBridgeService || !window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return false;
const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id);
if (!instrumentId || (typeof instrumentId === 'string' && instrumentId.startsWith('sf_'))) return false;
if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {};
const bch = window.SonicMidiRouter.allocateChannel(track.id, false);
if (window.__bridgeLoadedChannels[bch]) return true;
if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {};
const tId = track.id || track.trackId;
const bch = window.SonicMidiRouter.allocateChannel(tId, false);
try {
// V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename —
// KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu
@@ -6459,9 +6486,22 @@ const loadVstToBridge = async (track) => {
if (v0 && v0.path) knownPath = v0.path;
if (v0 && v0.type) btype = v0.type;
}
const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: knownPath || null, instrumentType: btype, channel: bch });
// V9: path sai (plugin_path cũ / cache của plugin khác) → bỏ qua, resolve theo tên.
knownPath = _bridgeSafeVstPath(knownPath, instrumentId);
// Đã load ĐÚNG path vào channel này rồi → skip (không LOAD trùng). Bỏ
// early-return mù theo flag — flag sai khi C++ restore instrument khác.
if (window.__bridgeLoadedPaths[bch] === knownPath) return true;
// Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS).
if (knownPath) {
const ok = await window.NativeBridgeService.loadInstrument(knownPath, btype, bch);
if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = knownPath; }
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', knownPath);
return !!ok;
}
// Không có path → nhờ engine resolve theo name (POST). Fail (auth/CORS) →
// fallback listPlugins bên dưới.
const r = await window.SonicAPI.bridgeLoad({ name: instrumentId, path: null, instrumentType: btype, channel: bch });
let p = r && r.path ? r.path : null;
if (!p && knownPath) p = knownPath;
if (!p && window.SonicAPI.listPlugins) {
try {
const pl = await window.SonicAPI.listPlugins();
@@ -6476,12 +6516,14 @@ const loadVstToBridge = async (track) => {
}
if (!p) return false;
const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch);
if (ok) window.__bridgeLoadedChannels[bch] = true;
if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = p; }
console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p);
return !!ok;
} catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; }
};
const ensureAndOpenVstGui = async (trackId, instrumentId) => {
const ensureAndOpenVstGui = async (track) => {
const trackId = track.id || track.trackId;
const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id);
if (!instrumentId || (typeof instrumentId === 'string' && instrumentId.startsWith('sf_'))) return;
if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {};
const bch = (window.SonicMidiRouter && window.SonicMidiRouter.allocateChannel) ? window.SonicMidiRouter.allocateChannel(trackId, false) : 0;
@@ -6497,10 +6539,14 @@ const ensureAndOpenVstGui = async (trackId, instrumentId) => {
} catch (e) { console.warn('[Bridge] GUI via API fail:', trackId, e); }
return;
}
if (!window.__bridgeLoadedChannels[bch]) {
const ok = await loadVstToBridge({ id: trackId, instrumentId });
if (!ok) return;
}
// V9: load (nếu chưa có ĐÚNG instrument) rồi CHỈ mở GUI khi load OK — không
// mở mù khi fail (nếu load Nexus fail mà vẫn OPEN_GUI, C++ mở GUI của
// instrument KHÁC đang restore trên channel — VD WaveObserver thay vì Nexus).
// Bridge restore sẵn ĐÚNG instrument → LOAD trùng path → C++ double-load
// guard skip → ok=true → GUI vẫn mở. Không có instrument → OPEN_GUI defer.
let loadOk = false;
try { loadOk = await loadVstToBridge(track); } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', trackId, e); }
if (!loadOk) return;
try { await openVstGuiRetry(instrumentId, bch, 20); } catch (e) { console.warn('[Bridge] openNativeGUI fail:', e); }
};
// V8 bug 4 + V9 bug 3/6/7: C++ load instrument ASYNC (worker thread) — OPEN_GUI
@@ -10236,7 +10282,7 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`
}, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, activeParentTrackName ? ('(' + activeParentTrackName + ') ' + (st.instrumentName || 'Synth')) : (st.instrumentName || 'Synth'))), (st.instrumentId && typeof st.instrumentId === 'string' && !st.instrumentId.startsWith('sf_') ? React.createElement("button", {
onClick: () => {
ensureAndOpenVstGui(st.trackId, st.instrumentId);
ensureAndOpenVstGui(st);
},
title: "Mở lại GUI VSTi",
className: "px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 shrink-0 bg-zinc-800 text-violet-300 border-violet-800 hover:bg-violet-900/40"
@@ -11067,6 +11113,7 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
duration: st.duration || 4,
instrument_program: st.instrumentProgram,
instrument_name: st.instrumentName,
instrument_id: st.instrumentId || (st.synth_engine && st.synth_engine.plugin_id) || null,
synth_engine: st.synth_engine || null,
current_time: st.currentTime || 0,
color: st.color || null
@@ -11148,6 +11195,7 @@ const deserializeProjectFromSchema = (schemaObj) => {
duration: st.duration || 4,
instrumentProgram: st.instrument_program,
instrumentName: st.instrument_name,
instrumentId: st.instrument_id || (st.synth_engine && st.synth_engine.plugin_id) || null,
synth_engine: st.synth_engine || null,
currentTime: st.current_time || 0,
color: st.color || null
@@ -16097,7 +16145,10 @@ const App = () => {
// channel, nếu không ensureAndOpenVstGui sẽ bỏ qua load khi chọn lại VST
// (flag cũ còn true) → OPEN_GUI đến bridge khi channel chưa có instrument
// → deferred mãi, nút GUI không mở gì.
if (isSf && window.__bridgeLoadedChannels) delete window.__bridgeLoadedChannels[bch];
if (isSf) {
if (window.__bridgeLoadedChannels) delete window.__bridgeLoadedChannels[bch];
if (window.__bridgeLoadedPaths) delete window.__bridgeLoadedPaths[bch];
}
// VST2 (.dll) scan type la 'VST2' - khong hardcode VST3 (se LOAD sai
// loai -> create_instrument nullptr -> fail im lang).
let btype = isSf ? 'SF2' : 'VST3';
@@ -16113,6 +16164,14 @@ const App = () => {
if (v0 && v0.type) btype = v0.type;
}
}
// V9: path sai (plugin khác instrumentId) → bỏ qua, resolve theo name.
if (!isSf) bpath = _bridgeSafeVstPath(bpath, instrumentId);
// V9: VST→VST đổi instrument khác → path khác path đã load → xóa flag để
// load mới (trước đây chỉ xóa khi isSf — flag cũ làm skip nhầm).
if (window.__bridgeLoadedPaths && window.__bridgeLoadedPaths[bch] !== bpath) {
delete window.__bridgeLoadedPaths[bch];
if (window.__bridgeLoadedChannels) delete window.__bridgeLoadedChannels[bch];
}
(async () => {
let p = bpath;
try {
@@ -16129,7 +16188,12 @@ const App = () => {
const _pfx = trackId + ':';
Object.keys(window.__bridgeLastProgram).forEach(k => { if (k.indexOf(_pfx) === 0) delete window.__bridgeLastProgram[k]; });
}
if (ok) { if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {}; window.__bridgeLoadedChannels[bch] = true; }
if (ok) {
if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {};
window.__bridgeLoadedChannels[bch] = true;
if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {};
window.__bridgeLoadedPaths[bch] = p;
}
if (ok && btype === 'VST3' && p) {
try {
if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {};
@@ -17747,11 +17811,17 @@ const App = () => {
// Rust auto-restart KHONG emit bridge-down khi thanh cong → map cu true →
// GUI click skip load → attach null → nen trang.
window.__bridgeLoadedChannels = {};
window.__bridgeLoadedPaths = {};
setBridgeUi(s => ({ ...s, connected: !!st.connected }));
if (!st.connected) {
// Bridge down/restart → C++ mất hết instrument → reset map để lần tới
// load lại (nút GUI ensure-load trước khi open — tránh cửa sổ trắng).
window.__bridgeLoadedChannels = {};
window.__bridgeLoadedPaths = {};
// FIX auto-play: bridge restart → page co the con isPlaying=true (stale)
// → master-silence watchdog tu resume phat lai → am tu dong. Stop ngay
// de page khong con "dang phat" — user muon nghe thi bam play.
try { stopAllPlayback(); setIsPlaying(false); } catch (e) { console.warn('[Bridge] stop-on-down fail:', e); }
return;
}
// Bridge (re)connect → load lại toàn bộ VST track (không mở GUI).
@@ -30598,7 +30668,7 @@ STRICT CONSTRAINTS:
})), /*#__PURE__*/React.createElement("span", { className: "truncate text-xs min-w-0" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" })), (track.instrumentId && typeof track.instrumentId === 'string' && !track.instrumentId.startsWith('sf_') ? /*#__PURE__*/React.createElement("button", {
onClick: (e) => {
e.stopPropagation();
ensureAndOpenVstGui(track.id, track.instrumentId);
ensureAndOpenVstGui(track);
},
title: "Mở lại GUI VSTi",
className: "px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-violet-300 border border-violet-800 rounded text-xs font-bold flex items-center gap-1 shrink-0"
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -50,7 +50,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=20260818" defer></script>
<script src="/static/js/app.precompiled.js?v=202608181642" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
Binary file not shown.
Binary file not shown.
@@ -156,17 +156,22 @@ public:
private:
static std::unique_ptr<INativeInstrument> create_instrument(InstrumentType type);
// Probe-render 3 notes sau khi load, do peak, tinh makeup de note don ra
// ~ -6 dBFS (0.5) - can bang am luong moi engine (VST2/VST3/SF2/SFZ).
static float calibrate(INativeInstrument* inst, double sampleRate, uint32_t blockSize);
mutable std::mutex mu_;
std::map<uint32_t, std::unique_ptr<INativeInstrument>> channels_;
std::map<uint32_t, std::string> paths_; // last assigned path per channel
std::map<uint32_t, InstrumentType> types_; // last assigned type per channel (G2.1)
std::map<uint32_t, float> makeup_; // calibrated per-channel output gain (renderAll ap dung)
// Per-instrument scratch so engines that overwrite (not mix) stay additive.
std::vector<float> scratchL_, scratchR_;
// Channel-level reload flag (see isReloading) — indexed by MIDI channel.
bool reloadingCh_[16] = {};
// G3.3: channel muted after a plugin fault inside processAudioBlock.
bool crashedCh_[16] = {};
float limiterGain_ = 1.0f; // smoothed mix limiter state (renderAll)
// True when the channel's plugin DLL has an attached editor (or its reload
// is in progress): real-time code drops MIDI and skips rendering. Lowercased
// compare inside -- Windows plugin paths are case-insensitive.
+18 -1
View File
@@ -131,6 +131,21 @@ void sendResponse(SOCKET c, int code, const char* contentType,
if (len > 0) send(c, (const char*)data, (int)len, 0);
}
// CORS preflight reply: the embed page (origin 127.0.0.1:8000) POSTs JSON to
// this server (a different port), so the browser sends an OPTIONS request
// first. Without this handler the preflight fell into the /input JSON parser
// and failed with 400, so the embedded GUI looked frozen: frames streamed but
// clicks never reached the plugin.
void sendOptions(SOCKET c) {
const char* r =
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n"
"Access-Control-Allow-Origin: *\r\n"
"Access-Control-Allow-Methods: GET, POST, OPTIONS\r\n"
"Access-Control-Allow-Headers: Content-Type\r\n"
"Content-Length: 0\r\nConnection: close\r\n\r\n";
send(c, r, (int)std::strlen(r), 0);
}
// --- frame capture -----------------------------------------------------------
// Capture the window client rect twice — BitBlt (GDI view) and PrintWindow
// with PW_RENDERFULLCONTENT (DWM composition, includes DirectX/GPU surfaces
@@ -288,7 +303,9 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
HttpReq req;
if (!readRequest(c, req)) { closesocket(c); continue; }
if (req.path == "/ping") {
if (req.method == "OPTIONS") {
sendOptions(c);
} else if (req.path == "/ping") {
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
} else if (req.path == "/frame") {
std::vector<unsigned char> jpg;
+97 -2
View File
@@ -46,6 +46,9 @@ bool FluidSynthInstrument::loadSoundFontFile(const std::string& path, double sam
fluid_settings_setint(FS_SETTINGS, "synth.verbose", 0);
synth = new_fluid_synth(FS_SETTINGS);
if (!synth) return false;
// Unity baseline; auto-calibration trong InstrumentEngineManager::calibrate
// can bang peak ve -6 dBFS cho moi engine (VST2/VST3/SF2/SFZ).
fluid_synth_set_gain(FS_SYNTH, 1.0f);
sfontId = fluid_synth_sfload(FS_SYNTH, path.c_str(), 1);
if (sfontId == -1) return false;
// Reset all channels to font preset 0 (spec §VII: bank0/prog0 piano)
@@ -65,7 +68,12 @@ void FluidSynthInstrument::selectProgram(uint32_t channel, uint32_t bank, uint32
if (!synth || channel >= 16) return;
bank_[channel] = bank;
program_[channel] = program;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program);
// SF2 fallback: nhieu soundfont khong co bank 128 (GM drum) — neu select
// fail, thu lai bank 0 de khong cam (fluidsynth giu preset cu -> sai am).
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank, program) != FLUID_OK && bank != 0) {
bank_[channel] = 0;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
}
}
void FluidSynthInstrument::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
@@ -92,7 +100,10 @@ void FluidSynthInstrument::programChange(uint32_t channel, uint32_t program) {
// Dung bank da nhan tu CC0/CC32 — bank hardcode 0 lam preset o bank != 0
// khong duoc chon (fluid giu preset cu -> ra piano sai).
program_[channel] = program;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program);
if (fluid_synth_program_select(FS_SYNTH, channel, sfontId, bank_[channel], program) != FLUID_OK && bank_[channel] != 0) {
bank_[channel] = 0;
fluid_synth_program_select(FS_SYNTH, channel, sfontId, 0, program);
}
}
void FluidSynthInstrument::pitchBend(uint32_t channel, uint32_t bend14) {
@@ -224,6 +235,9 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
if (!loaded) return false;
// init() AFTER load: FluidSynth creates its synth inside loadSoundFontFile.
if (!inst->init(sampleRate, blockSize)) return false;
// Calibrate output level (makeup) - probe render truoc khi publish vao map
// de renderAll ap dung ngay, khong block audio thread.
const float mk = calibrate(inst.get(), sampleRate, blockSize);
// Replacing an existing instrument drops its voices with the old engine.
// Load may run on a detached thread (VST3 init is slow): only the map
// write is under the mutex so renderAll on the audio loop never stalls.
@@ -244,6 +258,7 @@ bool InstrumentEngineManager::assign(uint32_t channel, InstrumentType type,
// flows to it again. G3.3: a reload also clears the crash-mute.
reloadingCh_[channel] = false;
crashedCh_[channel] = false;
makeup_[channel] = mk;
}
if (oldInst) {
isBypassed_.store(true);
@@ -271,6 +286,7 @@ void InstrumentEngineManager::unload(uint32_t channel) {
channels_.erase(channel);
paths_.erase(channel);
types_.erase(channel);
makeup_.erase(channel);
reloadingCh_[channel] = false;
}
if (oldInst) {
@@ -459,6 +475,63 @@ static bool SafeProcessChannel(INativeInstrument* inst, float* outL, float* outR
}
#endif
// --- Auto-calibration (am luong on dinh moi engine VST2/VST3/SF2/SFZ) ---
// Probe-render 3 notes (60/64/67, velocity 0.8) ngay sau khi load, do peak,
// tinh makeup sao cho note don ra ~ -6 dBFS (0.5). Clamp: khong bu patch cam
// qua muc, khong tat patch to. renderAll ap dung makeup + peak-safety @1.0
// (chord/transient vuot 1.0 se bi keo ve 1.0 - khong clip, khong meo).
static constexpr float kCalTargetPeak = 0.5f;
static constexpr float kCalMinMakeup = 0.125f;
static constexpr float kCalMaxMakeup = 8.0f;
float InstrumentEngineManager::calibrate(INativeInstrument* inst, double sampleRate, uint32_t blockSize) {
if (!inst) return 1.0f;
const uint32_t bs = blockSize ? blockSize : 512;
const uint32_t body = (uint32_t)(sampleRate * 0.6);
const uint32_t tail = (uint32_t)(sampleRate * 0.25);
std::vector<float> L(bs + 64, 0.0f), R(bs + 64, 0.0f);
const int pitches[3] = { 60, 64, 67 };
float peak = 0.0f;
auto renderProbe = [&](uint32_t n) {
#ifdef _WIN32
DWORD code = 0;
#else
uint32_t code = 0;
#endif
if (!SafeProcessChannel(inst, L.data(), R.data(), n, &code)) return false;
for (uint32_t i = 0; i < n; ++i) {
float a = L[i]; if (a < 0.0f) a = -a; if (a > peak) peak = a;
float b = R[i]; if (b < 0.0f) b = -b; if (b > peak) peak = b;
}
return true;
};
for (int p : pitches) {
inst->noteOn(0, (uint32_t)p, 0.8f, 0);
uint32_t off = 0;
while (off < body) {
uint32_t n = bs; if (off + n > body) n = body - off;
if (!renderProbe(n)) { inst->noteOff(0, (uint32_t)p, 0); return 1.0f; }
off += n;
}
inst->noteOff(0, (uint32_t)p, 0);
off = 0;
while (off < tail) {
uint32_t n = bs; if (off + n > tail) n = tail - off;
if (!renderProbe(n)) return 1.0f;
off += n;
}
}
if (peak < 1e-4f) {
std::cerr << "[Calibrate] silent probe peak=" << peak << " makeup=1.0" << std::endl;
return 1.0f;
}
float g = kCalTargetPeak / peak;
if (g < kCalMinMakeup) g = kCalMinMakeup;
if (g > kCalMaxMakeup) g = kCalMaxMakeup;
std::cerr << "[Calibrate] peak=" << peak << " makeup=" << g << std::endl;
return g;
}
void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t numSamples) {
if (isBypassed_.load()) {
std::memset(outputL, 0, numSamples * sizeof(float));
@@ -519,9 +592,31 @@ void InstrumentEngineManager::renderAll(float* outputL, float* outputR, uint32_t
}
continue;
}
// Auto-normalize: makeup calibrated (constant per-channel gain, no steps).
auto mit = makeup_.find(ch);
const float g = (mit != makeup_.end()) ? mit->second : 1.0f;
for (uint32_t i = 0; i < numSamples; ++i) { spL[i] *= g; spR[i] *= g; }
for (uint32_t i = 0; i < numSamples; ++i) {
outputL[i] += spL[i];
outputR[i] += spR[i];
}
}
// Smoothed brickwall limiter on the summed mix: fast attack (~2-3 samples)
// when the mix exceeds 1.0, slow release back to unity. Replaces the old
// per-block hard clamp whose gain step at block boundaries caused zipper
// crackle. limiterGain_ persists across renderAll calls.
const float kAttack = 0.4f;
const float kRelease = 0.0006f;
for (uint32_t i = 0; i < numSamples; ++i) {
float a = outputL[i] < 0.0f ? -outputL[i] : outputL[i];
float b = outputR[i] < 0.0f ? -outputR[i] : outputR[i];
const float m = a > b ? a : b;
const float target = (m * limiterGain_ > 1.0f) ? (1.0f / (m + 1e-12f)) : 1.0f;
if (target < limiterGain_)
limiterGain_ += (target - limiterGain_) * kAttack;
else
limiterGain_ += (1.0f - limiterGain_) * kRelease;
outputL[i] *= limiterGain_;
outputR[i] *= limiterGain_;
}
}
+7 -8
View File
@@ -1170,13 +1170,12 @@ int main(int argc, char* argv[]) {
}
});
}
// FIX auto-play: KHONG tu bat transport khi restore state -- bridge
// khong duoc tu phat am. Frontend la chu transport: page dang play
// that su se tu gui PLAY lai sau reconnect; page fresh thi im lang.
if (snap.playing) {
playheadSamples = (uint64_t)snap.playhead;
transportStopped = false;
g_transportPlaying = true;
g_stateDirty = true;
std::cout << "[NativeBridge] restoring transport PLAY playhead="
<< playheadSamples.load() << std::endl;
std::cout << "[NativeBridge] restore transport SKIPPED (snap.playing=true)"
<< std::endl;
}
} else {
std::cout << "[NativeBridge] no state file to restore (" << stateFile << ")" << std::endl;
@@ -1386,8 +1385,8 @@ int main(int argc, char* argv[]) {
{
std::lock_guard<std::mutex> lock(g_pendingGuiMutex);
for (auto it = g_pendingGui.begin(); it != g_pendingGui.end(); ) {
// G0.1: giu entry khi dang PLAY - handleOpenGui se defer lai
if (transportStopped && instruments.has(it->first)) {
// G0.1: fulfill as soon as instrument is ready, even while playing
if (instruments.has(it->first)) {
readyGui.push_back({ it->first, it->second });
it = g_pendingGui.erase(it);
} else ++it;
+4
View File
@@ -511,6 +511,10 @@ pub fn run() {
break;
}
down_emitted = true;
// FIX auto-play: bao page ngay khi bridge sap restart --
// page stop playback (isPlaying=false) de watchdog recovery
// khong tu resume phat lai (auto-play) sau reconnect.
let _ = pump_handle.emit("bridge-down", ());
if restart_attempts == 0 {
restart_attempts += 1;
let res_dir = pump_handle.path().resource_dir().unwrap_or_default();