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:
@@ -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
@@ -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"
|
||||
|
||||
+2013
-1988
File diff suppressed because one or more lines are too long
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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_;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user