Fix master volume + VST3FX GUI state restore on save/reload
- serializeProjectToSchema now writes master volume_db and mastering gui_open_path/name - deserialize restores masterVolume + masteringSettings.gui_open_path/name - track vst_gui_open_path/name fallback to live state (not just _persistFxGui*) - MasteringModal/FXRackModal reopen effects react to isOpen + restored gui path - openMasterVstGui/openVstGui store gui name for reopen - bridge: /preset capture endpoint + RenderFxJob.captureState for GUI knob state
This commit is contained in:
+138
-61
@@ -11029,8 +11029,10 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
synth_engine: t.synth_engine || undefined,
|
||||
midi_channel: t.midiChannel !== undefined ? t.midiChannel : null,
|
||||
fx_chain: (t.fxChain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type, active: m.active !== false }),
|
||||
vst_fx_chain: (t.vstFxChain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
vst_fx_chain: (t.vstFxChain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
vst_fx_bypass: !!t.vstFxBypass,
|
||||
vst_gui_open_path: (t.id === _persistFxGuiTrackId && _persistFxGuiOpen && _persistFxGuiOpen.path) ? _persistFxGuiOpen.path : (t.vstGuiOpenPath || null),
|
||||
vst_gui_open_name: (t.id === _persistFxGuiTrackId && _persistFxGuiOpen && _persistFxGuiOpen.name) ? _persistFxGuiOpen.name : (t.vstGuiOpenName || null),
|
||||
server_file_id: t.serverFileId || null,
|
||||
items: items
|
||||
};
|
||||
@@ -11133,11 +11135,13 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore, visite
|
||||
fxChain: (t.fx_chain || []).map(m => typeof m === 'string' ? { type: m, active: true } : { type: m.type || m, active: m.active !== false }),
|
||||
vstFxChain: (t.vst_fx_chain || []).map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
vstFxBypass: t.vst_fx_bypass !== undefined ? !!t.vst_fx_bypass : false,
|
||||
vstGuiOpenPath: t.vst_gui_open_path || null,
|
||||
vstGuiOpenName: t.vst_gui_open_name || null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsList, sessionTabsList, masteringSettings) => {
|
||||
const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsList, sessionTabsList, masteringSettings, masterVolume) => {
|
||||
const secondsPerBar = (60.0 / parseFloat(bpmVal || 120)) * 4;
|
||||
const mainTracks = serializeTracksList(tracksList, secondsPerBar);
|
||||
|
||||
@@ -11244,10 +11248,10 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
||||
})(),
|
||||
auto_compute_length: true,
|
||||
tracks: mainTracks,
|
||||
...(masteringSettings && Array.isArray(masteringSettings.vstFxChain) && masteringSettings.vstFxChain.length > 0 ? {
|
||||
...((masteringSettings && Array.isArray(masteringSettings.vstFxChain) && masteringSettings.vstFxChain.length > 0) || (typeof masterVolume === 'number' && isFinite(masterVolume) && masterVolume !== 0) ? {
|
||||
master: {
|
||||
fx_chain: masteringSettings.vstFxChain.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
volume_db: 0,
|
||||
fx_chain: masteringSettings.vstFxChain.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
volume_db: (typeof masterVolume === 'number' && isFinite(masterVolume)) ? masterVolume : 0,
|
||||
mute: false,
|
||||
bypass: !!masteringSettings.vstBypass
|
||||
}
|
||||
@@ -11255,11 +11259,17 @@ const serializeProjectToSchema = (projectId, name, bpmVal, tracksList, subTabsLi
|
||||
},
|
||||
sub_tabs: subTabs,
|
||||
section_store: sectionStore,
|
||||
mastering_settings: masteringSettings || null
|
||||
mastering_settings: masteringSettings ? {
|
||||
...masteringSettings,
|
||||
gui_open_path: (_persistMasterGuiOpen && _persistMasterGuiOpen.path) ? _persistMasterGuiOpen.path : (masteringSettings && masteringSettings.gui_open_path) || null,
|
||||
gui_open_name: (_persistMasterGuiOpen && _persistMasterGuiOpen.name) ? _persistMasterGuiOpen.name : (masteringSettings && masteringSettings.gui_open_name) || null
|
||||
} : null
|
||||
};
|
||||
};
|
||||
|
||||
const deserializeProjectFromSchema = (schemaObj) => {
|
||||
// GUI knob captures are per-project - don't leak presets between projects.
|
||||
_capturedPresetByPath = {};
|
||||
const bpmVal = schemaObj.metadata ? schemaObj.metadata.bpm : 120;
|
||||
const secondsPerBar = (60.0 / bpmVal) * 4;
|
||||
|
||||
@@ -11346,8 +11356,11 @@ const deserializeProjectFromSchema = (schemaObj) => {
|
||||
masteringSettings: {
|
||||
..._migratedMastering,
|
||||
vstFxChain: _masterFx.map(s => ({ type: s.type || 'vst3', path: s.path || '', name: s.name || '', preset_b64: s.preset_b64 || '', bypass: !!s.bypass })),
|
||||
vstBypass: !!_masterBus.bypass
|
||||
vstBypass: !!_masterBus.bypass,
|
||||
gui_open_path: (schemaObj.mastering_settings && schemaObj.mastering_settings.gui_open_path) ? schemaObj.mastering_settings.gui_open_path : null,
|
||||
gui_open_name: (schemaObj.mastering_settings && schemaObj.mastering_settings.gui_open_name) ? schemaObj.mastering_settings.gui_open_name : null
|
||||
},
|
||||
masterVolume: (typeof _masterBus.volume_db === 'number' && isFinite(_masterBus.volume_db)) ? _masterBus.volume_db : 0,
|
||||
zoom: schemaObj.metadata && schemaObj.metadata.zoom ? parseFloat(schemaObj.metadata.zoom) : 1.0
|
||||
};
|
||||
};
|
||||
@@ -12077,6 +12090,11 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
|
||||
// GUI embed state survives FXRackModal unmount so the native window
|
||||
// re-shows automatically when the rack is reopened (close/reopen bug).
|
||||
let _persistFxGuiOpen = null;
|
||||
// Track id whose rack owns the currently-open embedded GUI (for save).
|
||||
let _persistFxGuiTrackId = null;
|
||||
// Latest preset_b64 captured from each plugin's embedded GUI (poll /preset)
|
||||
// - merged into the project on save so knob tweaks survive reload.
|
||||
let _capturedPresetByPath = {};
|
||||
const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
const [activeType, setActiveType] = React.useState('eq');
|
||||
const [addModuleOpen, setAddModuleOpen] = React.useState(false);
|
||||
@@ -12113,21 +12131,41 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => {
|
||||
const setVstChain = (next) => { if (onUpdateTrack) onUpdateTrack(track.id, { vstFxChain: next, vstFxBypass: vstBypass }); };
|
||||
// Embedded VST GUI state (PLAN_MASTERBUS_VST_GUI_EMBED.md Phase 3)
|
||||
const [fxGuiOpen, setFxGuiOpenState] = React.useState(_persistFxGuiOpen); // { path, url } | null
|
||||
const setFxGuiOpen = (v) => { _persistFxGuiOpen = v; setFxGuiOpenState(v); };
|
||||
const setFxGuiOpen = (v) => { _persistFxGuiOpen = v; _persistFxGuiTrackId = v ? (track && track.id) : null; setFxGuiOpenState(v); };
|
||||
const fxGuiOpenRef = React.useRef(null);
|
||||
fxGuiOpenRef.current = fxGuiOpen;
|
||||
const closeFxGui = () => {
|
||||
const g = fxGuiOpenRef.current;
|
||||
if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} }
|
||||
setFxGuiOpen(null);
|
||||
if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: null, vstGuiOpenName: null });
|
||||
};
|
||||
React.useEffect(() => () => { const g = fxGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
|
||||
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
|
||||
React.useEffect(() => {
|
||||
if (!fxGuiOpen || !fxGuiOpen.url) return;
|
||||
const iv = setInterval(() => {
|
||||
fetch(fxGuiOpen.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64) _capturedPresetByPath[fxGuiOpen.path] = d.preset_b64; }).catch(() => {});
|
||||
}, 1500);
|
||||
return () => clearInterval(iv);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fxGuiOpen]);
|
||||
// Reopen the saved GUI when the rack is mounted after a project restore.
|
||||
React.useEffect(() => {
|
||||
if (!track || !track.vstGuiOpenPath || fxGuiOpen) return;
|
||||
const s = (track.vstFxChain || []).find(x => x.path === track.vstGuiOpenPath);
|
||||
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
|
||||
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => {
|
||||
if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url });
|
||||
}).catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [track && track.id, track && track.vstGuiOpenPath]);
|
||||
const toggleVst = (idx) => setVstChain(vstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); };
|
||||
const removeVst = (idx) => { const rem = vstChain[idx]; if (rem && fxGuiOpen && fxGuiOpen.path === rem.path) closeFxGui(); if (rem) delete _capturedPresetByPath[rem.path]; if (idx === activeVstIdx) setActiveVstIdx(-1); setVstChain(vstChain.filter((_, i) => i !== idx)); };
|
||||
const moveVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= vstChain.length) return; const next = [...vstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setVstChain(next); };
|
||||
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) setFxGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const openVstGui = (s, idx) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (fxGuiOpen) closeFxGui(); if (idx !== undefined) setActiveVstIdx(idx); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setFxGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); if (onUpdateTrack && track) onUpdateTrack(track.id, { vstGuiOpenPath: s.path, vstGuiOpenName: s.name || s.path }); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addVst = (path) => { const fx = (fxList || []).find(f => f.path === path); if (!fx) return; const next = [...vstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]; setVstChain(next); setActiveVstIdx(next.length - 1); };
|
||||
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const changeVstPlugin = (idx, path) => { const fx = (fxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setVstChain(vstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const uploadVstPreset = (idx) => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'file';
|
||||
@@ -12665,8 +12703,28 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} }
|
||||
setMasterGuiOpen(null);
|
||||
setActiveMasterVstPath(null);
|
||||
setOzState(prev => { const n = { ...(prev || {}) }; delete n.gui_open_path; delete n.gui_open_name; return n; });
|
||||
};
|
||||
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} } }, []);
|
||||
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
|
||||
React.useEffect(() => {
|
||||
if (!masterGuiOpen || !masterGuiOpen.url) return;
|
||||
const iv = setInterval(() => {
|
||||
fetch(masterGuiOpen.url + '/preset').then(r => r.json()).then(d => { if (d && d.preset_b64) _capturedPresetByPath[masterGuiOpen.path] = d.preset_b64; }).catch(() => {});
|
||||
}, 1500);
|
||||
return () => clearInterval(iv);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [masterGuiOpen]);
|
||||
// Reopen the saved GUI when the panel is opened after a project restore.
|
||||
React.useEffect(() => {
|
||||
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen) return;
|
||||
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
|
||||
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
|
||||
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true }).then(r => {
|
||||
if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url });
|
||||
}).catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, ozState && ozState.gui_open_path]);
|
||||
const setMasterVstChain = (next) => setOzState(prev => ({ ...prev, vstFxChain: next }));
|
||||
// T2.4: push the master VST FX chain to the realtime bridge (live master
|
||||
// mix). v1: master chain only — per-track vstChain stays offline (defer).
|
||||
@@ -12676,12 +12734,12 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
window.NativeBridgeService.setFxChain(JSON.stringify(chain));
|
||||
}, [ozState.vstFxChain]);
|
||||
const toggleMasterVst = (idx) => setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, bypass: !s.bypass } : s));
|
||||
const removeMasterVst = (idx) => setMasterVstChain(masterVstChain.filter((_, i) => i !== idx));
|
||||
const removeMasterVst = (idx) => { const rem = masterVstChain[idx]; if (rem) delete _capturedPresetByPath[rem.path]; setMasterVstChain(masterVstChain.filter((_, i) => i !== idx)); };
|
||||
const moveMasterVst = (idx, dir) => { const j = idx + dir; if (j < 0 || j >= masterVstChain.length) return; const next = [...masterVstChain]; const mv = next.splice(idx, 1)[0]; next.splice(j, 0, mv); setMasterVstChain(next); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url }); else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const openMasterVstGui = (s) => { if (!s || !s.path) { window.showToast && window.showToast('Chọn VST FX trước', 'warning'); return; } if (masterGuiOpen) closeMasterGui(); if (s.path) setActiveMasterVstPath(s.path); if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; } window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true }).then(r => { if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); } else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info'); else window.showToast && window.showToast('Không mở được GUI embed', 'error'); }).catch(err => window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error')); };
|
||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const changeMasterVstPlugin = (idx, path) => { const fx = (masterFxList || []).find(f => f.path === path); delete _capturedPresetByPath[path]; setMasterVstChain(masterVstChain.map((s, i) => i === idx ? { ...s, path, name: fx ? (fx.name || fx.id) : s.name, preset_b64: '' } : s)); };
|
||||
const uploadMasterVstPreset = (idx) => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'file';
|
||||
@@ -17946,7 +18004,7 @@ const App = () => {
|
||||
// Render offline toàn project qua native_bridge (VST FX masterbus + track) → WAV.
|
||||
// Dùng cho nút "Nghe thử" trong FX Rack / Mastering — preview = âm export.
|
||||
window.__renderProjectPreview = async () => {
|
||||
const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const schema = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
try {
|
||||
const res = await window.SonicAPI.renderProject(schema, 'preview_' + Date.now() + '.wav');
|
||||
return res;
|
||||
@@ -18169,7 +18227,7 @@ const App = () => {
|
||||
const restoreLastSessionProject = async () => {
|
||||
var pendingWasNull = !window.__pendingSfsProject;
|
||||
loadPendingSfsProject();
|
||||
if (!pendingWasNull) return;
|
||||
if (!pendingWasNull) return true; // da xu li .sfs pending - khong de bang temp
|
||||
|
||||
var lastId = localStorage.getItem('sonic_project_id');
|
||||
var lastName = localStorage.getItem('sonic_project_name') || 'Dự án';
|
||||
@@ -18203,7 +18261,7 @@ const App = () => {
|
||||
} catch (e) { console.warn('restore load error:', e); }
|
||||
}
|
||||
try {
|
||||
if (!parsed) return;
|
||||
if (!parsed) return false;
|
||||
var restoredBpm = bpm;
|
||||
var restoredTracks = [];
|
||||
var restoredSessionTabs = [];
|
||||
@@ -18218,6 +18276,7 @@ const App = () => {
|
||||
// Explorer) — áp dụng mới giữ zoom hiện tại/localStorage — hết thu nhỏ.
|
||||
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
} else {
|
||||
restoredTracks = (parsed.tracks || []).map(function(t) {
|
||||
var rest = Object.assign({}, t); delete rest.height;
|
||||
@@ -18255,15 +18314,49 @@ const App = () => {
|
||||
if (rt.sections) restoredItemCount += rt.sections.length;
|
||||
});
|
||||
showToast('Đã khôi phục dự án "' + lastName + '" (' + restoredItemCount + ' items).', 'info');
|
||||
return true;
|
||||
} catch(e) {
|
||||
console.warn('restoreLastSessionProject failed:', e);
|
||||
// Stale session id (project deleted / DB reset): clear it so the error
|
||||
// does not repeat on every page load.
|
||||
localStorage.removeItem('sonic_project_id');
|
||||
localStorage.removeItem('sonic_project_name');
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Khoi phuc du an dang lam do tu temp autosave, CHI khi chua co project nao
|
||||
// duoc restore (restoreLastSessionProject tra false). Goi o ca luong login
|
||||
// modal (handleAuthSuccess) lan luong auto-login (checkAuthStatus).
|
||||
const restoreTempProjectIfFresh = async () => {
|
||||
try {
|
||||
const tmp = await window.SonicAPI.getTempProject();
|
||||
if (tmp && tmp.has_temp && tmp.data_json) {
|
||||
let proj = tmp.data_json;
|
||||
if (typeof proj === 'string') { try { proj = JSON.parse(proj); } catch (e) { proj = null; } }
|
||||
if (proj && proj.main_session) {
|
||||
const freshStart = !(tracks && tracks.some(t => (t.clips && t.clips.length > 0) || (t.midiItems && t.midiItems.length > 0) || t.buffer));
|
||||
if (freshStart) {
|
||||
const result = deserializeProjectFromSchema(proj);
|
||||
if (result && result.tracks && result.tracks.length > 0) {
|
||||
setTracks(result.tracks);
|
||||
loadAudioBuffersForTracks(result.tracks);
|
||||
setBpm((result.bpm || 120).toString());
|
||||
if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
|
||||
if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||
setProjectName(tmpName);
|
||||
localStorage.setItem('sonic_project_name', tmpName);
|
||||
showToast('Đã khôi phục dự án đang làm dở từ bản lưu tạm', 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn('[TempRestore] error:', e); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
const savedToken = localStorage.getItem('sonic_token');
|
||||
@@ -18284,7 +18377,10 @@ const App = () => {
|
||||
} else {
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
restoreLastSessionProject();
|
||||
(async () => {
|
||||
const restored = await restoreLastSessionProject();
|
||||
if (!restored) await restoreTempProjectIfFresh();
|
||||
})();
|
||||
}
|
||||
} catch (err) {
|
||||
const cached = localStorage.getItem('sonic_user');
|
||||
@@ -18292,7 +18388,10 @@ const App = () => {
|
||||
try { setCurrentUser(JSON.parse(cached)); } catch (_) { }
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
restoreLastSessionProject();
|
||||
(async () => {
|
||||
const restored = await restoreLastSessionProject();
|
||||
if (!restored) await restoreTempProjectIfFresh();
|
||||
})();
|
||||
} else {
|
||||
localStorage.removeItem('sonic_token');
|
||||
localStorage.removeItem('sonic_user');
|
||||
@@ -18352,6 +18451,7 @@ const App = () => {
|
||||
carlaBridgeActive: false, carlaBridge: { plugin: '', plugin_path: '', connected: false },
|
||||
});
|
||||
}
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
} else {
|
||||
restoredTracks = (proj.tracks || []).map(t => {
|
||||
const { height: _h, ...rest } = t;
|
||||
@@ -18391,7 +18491,10 @@ const App = () => {
|
||||
} else {
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
restoreLastSessionProject();
|
||||
(async () => {
|
||||
const restored = await restoreLastSessionProject();
|
||||
if (!restored) await restoreTempProjectIfFresh();
|
||||
})();
|
||||
const loadPrefs = (p) => {
|
||||
if (!p) return;
|
||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
||||
@@ -18441,35 +18544,6 @@ const App = () => {
|
||||
setInstrumentSelectorData(data);
|
||||
}).catch(() => {});
|
||||
} catch (e) { }
|
||||
// ── Khôi phục dự án đang làm dở (temp autosave) khi load lại app ──
|
||||
// Yêu cầu: "Khi tắt ứng dụng → tự động lưu temp; khi load lại → tải lại
|
||||
// dự án đang làm dở." Chỉ restore khi app khởi động với project TRỐNG
|
||||
// (chưa có clip/midi/audio nào) — không đè lên dự án user đang mở.
|
||||
try {
|
||||
const tmp = await window.SonicAPI.getTempProject();
|
||||
if (tmp && tmp.has_temp && tmp.data_json) {
|
||||
let proj = tmp.data_json;
|
||||
if (typeof proj === 'string') { try { proj = JSON.parse(proj); } catch (e) { proj = null; } }
|
||||
if (proj && proj.main_session) {
|
||||
const freshStart = !(tracks && tracks.some(t => (t.clips && t.clips.length > 0) || (t.midiItems && t.midiItems.length > 0) || t.buffer));
|
||||
if (freshStart) {
|
||||
const result = deserializeProjectFromSchema(proj);
|
||||
if (result && result.tracks && result.tracks.length > 0) {
|
||||
setTracks(result.tracks);
|
||||
loadAudioBuffersForTracks(result.tracks);
|
||||
setBpm((result.bpm || 120).toString());
|
||||
if (result.sessionTabs && result.sessionTabs.length > 0) setSessionTabs(result.sessionTabs);
|
||||
if (result.subTabs && result.subTabs.length > 0) setSubTabs(result.subTabs);
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||
setProjectName(tmpName);
|
||||
localStorage.setItem('sonic_project_name', tmpName);
|
||||
showToast('Đã khôi phục dự án đang làm dở từ bản lưu tạm', 'success');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) { console.warn('[TempRestore] error:', e); }
|
||||
})();
|
||||
}
|
||||
};
|
||||
@@ -18503,9 +18577,9 @@ const App = () => {
|
||||
} : null
|
||||
}));
|
||||
window.SonicStorage.scheduleTempAutoSave(() => {
|
||||
return serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
return serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
});
|
||||
}, [tracks, subTabs, sessionTabs, masteringSettings]);
|
||||
}, [tracks, subTabs, sessionTabs, masteringSettings, masterVolume]);
|
||||
|
||||
// ── Temp autosave SERVER khi tắt ứng dụng (Bug: đóng app mất dự án làm dở) ──
|
||||
// Lưu temp vào thư mục temp của ứng dụng (storage/temp + DB) khi:
|
||||
@@ -18515,7 +18589,7 @@ const App = () => {
|
||||
useEffect(() => {
|
||||
const buildTempPayload = () => {
|
||||
try {
|
||||
const schemaObj = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const schemaObj = serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
const cid = (window.SonicStorage && window.SonicStorage.getClientId) ? window.SonicStorage.getClientId() : '';
|
||||
return JSON.stringify({ data_json: JSON.stringify(schemaObj), client_id: cid });
|
||||
} catch (e) { return null; }
|
||||
@@ -18551,7 +18625,7 @@ const App = () => {
|
||||
window.removeEventListener('pagehide', onUnload);
|
||||
clearInterval(interval);
|
||||
};
|
||||
}, [currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings]);
|
||||
}, [currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume]);
|
||||
|
||||
// ── Realtime sync giữa các client (LAN browser ↔ Tauri standalone) ──
|
||||
// Bug 3: 2 session cùng tài khoản không sync. Poll revision nhẹ 3s; khi
|
||||
@@ -18592,6 +18666,7 @@ const App = () => {
|
||||
// (modal đang chỉnh) — bản đồng bộ từ thiết bị khác
|
||||
// không được revert chain đang sửa (ví dụ vừa xóa VST).
|
||||
if (result.masteringSettings && !masteringModalOpenRef.current) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
const tmpName = (proj.metadata && proj.metadata.title) || 'Dự án tạm';
|
||||
setProjectName(tmpName);
|
||||
localStorage.setItem('sonic_project_name', tmpName);
|
||||
@@ -18622,14 +18697,14 @@ const App = () => {
|
||||
if (currentProjectId.startsWith('local_')) {
|
||||
const finalName = projectName || 'Dự án mới';
|
||||
const localId = currentProjectId;
|
||||
const schemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const schemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
const dataStr = JSON.stringify(schemaObj);
|
||||
localStorage.setItem('sonic_local_project_data', dataStr);
|
||||
localStorage.setItem('sonic_project_id', localId);
|
||||
localStorage.setItem('sonic_project_name', finalName);
|
||||
} else if (currentUser && window.SonicAPI) {
|
||||
try {
|
||||
const schemaObj = serializeProjectToSchema(currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const schemaObj = serializeProjectToSchema(currentProjectId, projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
const dataJson = JSON.stringify(schemaObj);
|
||||
window.SonicAPI.updateCloudProject(currentProjectId, projectName || 'Dự án mới', dataJson).catch(() => {});
|
||||
} catch (e) {
|
||||
@@ -25543,7 +25618,7 @@ const App = () => {
|
||||
try {
|
||||
if (window.SonicStorage) {
|
||||
const nextTracks = (tracks || []).map(t => t.id === trackId ? { ...t, color } : t);
|
||||
window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings));
|
||||
window.SonicStorage.scheduleTempAutoSave(() => serializeProjectToSchema(currentProjectId || 'temp_project', projectName || 'Dự án tạm chưa lưu', bpm, nextTracks, subTabs, sessionTabs, masteringSettings, masterVolume));
|
||||
if (window.SonicStorage.flushTempAutoSave) window.SonicStorage.flushTempAutoSave();
|
||||
}
|
||||
} catch (e) {}
|
||||
@@ -26396,7 +26471,7 @@ const App = () => {
|
||||
const handleSaveLocalProject = (name) => {
|
||||
const finalName = name || projectName || 'Dự án mới';
|
||||
const localId = currentProjectId && currentProjectId.startsWith('local_') ? currentProjectId : 'local_' + Date.now();
|
||||
const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
const dataStr = JSON.stringify(projectSchemaObj);
|
||||
localStorage.setItem('sonic_local_project_data', dataStr);
|
||||
localStorage.setItem('sonic_project_id', localId);
|
||||
@@ -26419,7 +26494,7 @@ const App = () => {
|
||||
const handleSaveCloudProject = async (name, existingProjectId) => {
|
||||
const finalName = name || projectName || 'Dự án mới';
|
||||
var useProjectId = existingProjectId || currentProjectId;
|
||||
const projectSchemaObj = serializeProjectToSchema(useProjectId || 'project_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const projectSchemaObj = serializeProjectToSchema(useProjectId || 'project_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
// Diagnostic: count source vs serialized items
|
||||
var srcMidi = 0, srcClip = 0, srcSec = 0;
|
||||
tracks.forEach(function(t) { srcMidi += (t.midiItems||[]).length; srcClip += (t.clips||[]).length; srcSec += (t.sections||[]).length; });
|
||||
@@ -26482,7 +26557,7 @@ const App = () => {
|
||||
const handleGlobalSave = async () => {
|
||||
try {
|
||||
const finalName = projectName || 'Dự án mới';
|
||||
const schemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const schemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
const dataJson = JSON.stringify(schemaObj);
|
||||
const runtime = (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.runtime) || '';
|
||||
if (runtime === 'desktop' && window.SonicAPI && window.SonicAPI.saveProjectToDisk) {
|
||||
@@ -26520,7 +26595,7 @@ const App = () => {
|
||||
}, []);
|
||||
|
||||
const handleSaveAsCloud = async (newName) => {
|
||||
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
const dataJson = JSON.stringify(projectSchemaObj);
|
||||
try {
|
||||
const res = await window.SonicAPI.saveCloudProject(newName, dataJson);
|
||||
@@ -26540,7 +26615,7 @@ const App = () => {
|
||||
const handleSaveCloud = handleSaveProject;
|
||||
const handleExportSFS = (customName = null) => {
|
||||
const finalName = customName || projectName || 'Dự án SonicForge';
|
||||
const projectSchemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const projectSchemaObj = serializeProjectToSchema(currentProjectId || 'proj_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings, masterVolume);
|
||||
window.SonicStorage.exportProjectToSFS(projectSchemaObj);
|
||||
showToast("Đã xuất dự án (.sfs) thành công!", "success");
|
||||
};
|
||||
@@ -26563,6 +26638,7 @@ const App = () => {
|
||||
restoredSessionTabs = result.sessionTabs;
|
||||
restoredSubTabs = result.subTabs;
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
} else {
|
||||
restored = (proj.tracks || []).map(t => ({
|
||||
...t,
|
||||
@@ -29052,6 +29128,7 @@ STRICT CONSTRAINTS:
|
||||
// Explorer) — áp dụng mới giữ zoom hiện tại/localStorage.
|
||||
if (result.zoom > 1 && setZoom) setZoom(result.zoom);
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
if (result.masterVolume !== undefined && result.masterVolume !== null) setMasterVolume(result.masterVolume);
|
||||
} else {
|
||||
restoredTracks = (parsed.tracks || []).map(function (t) {
|
||||
const rest = Object.assign({}, t); delete rest.height;
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
@@ -55,6 +55,12 @@ int run_open_fx_gui(const std::string& jobPath);
|
||||
// --open-fx-gui.
|
||||
int run_fx_gui_server(const std::string& jobPath);
|
||||
|
||||
// Capture the GUI plugin's current state (component + controller) as base64
|
||||
// of the bridge blob [4B BE compLen][comp][4B BE ctrlLen][ctrl] - the same
|
||||
// format applyPreset accepts. Runs on the GUI pump thread; blocks up to
|
||||
// timeoutMs. outB64 stays empty on timeout or when the plugin has no state.
|
||||
void fxGuiRequestCapture(void* hwnd, std::string& outB64, int timeoutMs);
|
||||
|
||||
// Realtime FX chain (T2.4): live VST3 audio effects on the bridge master mix.
|
||||
// setChain() is async (a worker thread loads the plugins and swaps the chain);
|
||||
// process() runs ONLY on the audio thread; shutdown() joins the worker and must
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
// POST /input -> {type:"mousedown|mousemove|mouseup|wheel|keydown|keyup",
|
||||
// x,y,button,deltaY,keyCode} -> PostMessage to HWND
|
||||
// POST /close -> detach + exit 0
|
||||
// GET /preset -> {"preset_b64":"..."} current plugin state
|
||||
//
|
||||
// The HTTP server runs on a worker thread; the calling (STA) thread runs the
|
||||
// message pump — VST3 editors need COM apartment + pump on the same thread.
|
||||
@@ -44,6 +45,7 @@
|
||||
// Defined in RenderFxJob.cpp (global scope); declared here outside the
|
||||
// anonymous namespace so both TUs see the same symbol.
|
||||
extern void fxGuiRequestResize(HWND hwnd, int w, int h);
|
||||
extern void fxGuiRequestCapture(void* hwnd, std::string& outB64, int timeoutMs);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -575,6 +577,11 @@ void runHttpServer(HWND hwnd, std::atomic<bool>& stop) {
|
||||
SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW);
|
||||
}
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
} else if (req.path == "/preset") {
|
||||
std::string b64;
|
||||
fxGuiRequestCapture(hwnd, b64, 1500);
|
||||
const std::string body = "{\"preset_b64\":\"" + b64 + "\"}";
|
||||
sendResponse(c, 200, "application/json", body.data(), (int)body.size());
|
||||
} else if (req.path == "/close") {
|
||||
sendResponse(c, 200, "application/json", "{\"ok\":true}", 11);
|
||||
stop.store(true);
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
@@ -258,6 +259,24 @@ 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;
|
||||
@@ -619,6 +638,43 @@ public:
|
||||
std::cout << "[RenderFx] preset applied component=" << comp.size()
|
||||
<< " controller=" << ctrl.size() << std::endl;
|
||||
return true;
|
||||
#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.
|
||||
void captureState(std::string& out) {
|
||||
#ifndef HAVE_VST3SDK
|
||||
(void)out;
|
||||
return;
|
||||
#else
|
||||
auto* s = static_cast<Vst3FxState*>(state_);
|
||||
if (!s) return;
|
||||
std::vector<uint8_t> comp, ctrl;
|
||||
{
|
||||
Steinberg::MemoryStream stream;
|
||||
if (s->component && s->component->getState(&stream) == kResultOk && stream.getSize() > 0)
|
||||
comp.assign(stream.getData(), stream.getData() + stream.getSize());
|
||||
}
|
||||
if (!s->controllerIsComponent) {
|
||||
Steinberg::MemoryStream stream;
|
||||
if (s->controller && s->controller->getState(&stream) == kResultOk && stream.getSize() > 0)
|
||||
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);
|
||||
#endif
|
||||
}
|
||||
// Native editor: attach the plugin's IPlugView to a host window (must run
|
||||
@@ -1103,6 +1159,7 @@ void RealtimeFxChain::Impl::workerLoop() {
|
||||
// Worker (HTTP) thread -> pump thread editor-resize request. Declared
|
||||
// outside the anonymous namespace: the definition sits after the namespace.
|
||||
static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h);
|
||||
static void fxGuiCaptureSafe(void* fx, std::string& out);
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1116,6 +1173,16 @@ struct FxGuiCtx {
|
||||
// checkSizeConstraint/onSize must run on the thread that owns the editor.
|
||||
#define WM_FXGUI_RESIZE (WM_APP + 0x51)
|
||||
|
||||
// --- preset capture (GUI state -> project save) ------------------------
|
||||
// getState must run on the pump thread that owns the plugin, so the HTTP
|
||||
// worker posts WM_FXGUI_CAPTURE and waits (up to timeoutMs) for the pump
|
||||
// thread to park the base64 blob. Empty outB64 = timeout or no state.
|
||||
#define WM_FXGUI_CAPTURE (WM_APP + 0x52)
|
||||
static std::mutex& gCapMutex() { static std::mutex m; return m; }
|
||||
static std::condition_variable& gCapCv() { static std::condition_variable cv; return cv; }
|
||||
static std::string& gCapB64() { static std::string s; return s; }
|
||||
static bool& gCapDone() { static bool b = false; return b; }
|
||||
|
||||
LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
FxGuiCtx* ctx = (FxGuiCtx*)GetWindowLongPtrA(hwnd, GWLP_USERDATA);
|
||||
switch (msg) {
|
||||
@@ -1128,6 +1195,17 @@ LRESULT CALLBACK FxGuiWndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
|
||||
case WM_FXGUI_RESIZE:
|
||||
fxGuiResizeSafe((void*)GetPropA(hwnd, "FXGUI_FX"), hwnd, (int)wp, (int)lp);
|
||||
return 0;
|
||||
case WM_FXGUI_CAPTURE: {
|
||||
std::string b64;
|
||||
fxGuiCaptureSafe((void*)GetPropA(hwnd, "FXGUI_FX"), b64);
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(gCapMutex());
|
||||
gCapB64() = std::move(b64);
|
||||
gCapDone() = true;
|
||||
}
|
||||
gCapCv().notify_one();
|
||||
return 0;
|
||||
}
|
||||
default:
|
||||
return DefWindowProcA(hwnd, msg, wp, lp);
|
||||
}
|
||||
@@ -1161,11 +1239,37 @@ static void fxGuiResizeSafe(void* fx, HWND hwnd, int w, int h) {
|
||||
(void)fx; (void)hwnd; (void)w; (void)h;
|
||||
#endif
|
||||
}
|
||||
static void fxGuiCaptureSafe(void* fx, std::string& out) {
|
||||
#ifdef HAVE_VST3SDK
|
||||
__try { static_cast<Vst3Fx*>(fx)->captureState(out); }
|
||||
__except (EXCEPTION_EXECUTE_HANDLER) {}
|
||||
#else
|
||||
(void)fx; (void)out;
|
||||
#endif
|
||||
}
|
||||
// Worker (HTTP) thread asks the GUI pump thread to resize the plugin view.
|
||||
void fxGuiRequestResize(HWND hwnd, int w, int h) {
|
||||
PostMessageA(hwnd, WM_FXGUI_RESIZE, (WPARAM)w, (LPARAM)h);
|
||||
}
|
||||
|
||||
// Capture the GUI plugin state on the pump thread; blocks up to timeoutMs.
|
||||
void fxGuiRequestCapture(void* hwndV, std::string& outB64, int timeoutMs) {
|
||||
HWND hwnd = (HWND)hwndV;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(gCapMutex());
|
||||
gCapDone() = false;
|
||||
gCapB64().clear();
|
||||
}
|
||||
PostMessageA(hwnd, WM_FXGUI_CAPTURE, 0, 0);
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(gCapMutex());
|
||||
if (!gCapCv().wait_for(lk, std::chrono::milliseconds(timeoutMs), [] { return gCapDone(); }))
|
||||
return; // timeout - outB64 stays empty
|
||||
outB64 = gCapB64();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// --- split setup for server mode (startup speed) -----------------------------
|
||||
// 1. Parse job { path, name } — fast. Returns 0 ok, 1 job/argument error.
|
||||
static int fxGuiParseJob(const std::string& jobPath, std::string& path, std::string& name) {
|
||||
|
||||
Reference in New Issue
Block a user