fix(latency): threadpool renders + RENDER_LOCK + client pre-render cache

This commit is contained in:
2026-08-16 20:21:48 +07:00
parent 90b5a63719
commit ed26e1706d
6 changed files with 139 additions and 42 deletions
+85 -11
View File
@@ -344,6 +344,79 @@ const stopNativeVstNote = (key) => {
const stopAllNativeVstNotes = () => {
try { Object.keys(_nativeVstPreviews).forEach(function (k) { stopNativeVstNote(k); }); } catch (e) {}
};
// URL cache cho midi-render: key = fingerprint (item+instrument+preset+notes)
// → re-play cùng item dùng lại WAV đã render, không chờ round-trip.
const _nativeVstItemUrls = {};
const _nativeVstItemKey = (track, item, bpm) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId) || '';
const notes = (item && item.notes) || [];
const nk = notes.map(function (n) {
return (n.pitch || 60) + ':' + (n.start_beat || 0) + ':' + (n.duration_beats || 1) + ':' + (n.velocity != null ? n.velocity : 0.8);
}).join(',');
return [item && item.id ? item.id : (instrumentId + ':' + nk), instrumentId, (eng && eng.preset_id) || '', (eng && eng.preset_path) || '', bpm].join('|') + '|' + nk;
} catch (e) { return ''; }
};
const _renderNativeVstItem = (track, item, bpm) => {
try {
const eng = track && track.synth_engine;
const instrumentId = (eng && eng.plugin_id) || (track && track.instrumentId);
if (!instrumentId || !isVstTrackEngine(eng)) return Promise.resolve(null);
const notes = (item && item.notes) || [];
if (!notes.length) return Promise.resolve(null);
return window.SonicAPI.midiRender({
instrument_id: instrumentId,
notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
bpm: parseFloat(bpm) || 120,
preset_id: (eng && eng.preset_id) || undefined,
preset_path: (eng && eng.preset_path) || undefined,
preset_data: (eng && eng.preset_data) || undefined,
}).then(function (res) {
if (!res || !res.success || !res.url) return null;
return API_BASE_URL + res.url;
}).catch(function () { return null; });
} catch (e) { return Promise.resolve(null); }
};
// Dedupe in-flight render: play trong lúc pre-render đang chạy → chờ cùng promise,
// không bắn duplicate request (tránh queue sau RENDER_LOCK + render 2 lần).
const _nativeVstItemPending = {};
const ensureRenderNativeVstItem = (track, item, bpm) => {
const key = _nativeVstItemKey(track, item, bpm);
if (!key) return _renderNativeVstItem(track, item, bpm);
if (_nativeVstItemUrls[key]) return Promise.resolve(_nativeVstItemUrls[key]);
if (_nativeVstItemPending[key]) return _nativeVstItemPending[key];
const p = _renderNativeVstItem(track, item, bpm).then(function (url) {
if (url) _nativeVstItemUrls[key] = url;
return url;
}).then(function (url) {
delete _nativeVstItemPending[key];
return url;
});
_nativeVstItemPending[key] = p;
return p;
};
let _prerenderNativeVstTimer = null;
const prerenderNativeVstItems = (tracks, bpm) => {
try {
if (!isStandaloneSf()) return;
if (_prerenderNativeVstTimer) { clearTimeout(_prerenderNativeVstTimer); _prerenderNativeVstTimer = null; }
_prerenderNativeVstTimer = setTimeout(function () {
_prerenderNativeVstTimer = null;
const seen = {};
(tracks || []).forEach(function (track) {
const nativeVst = isStandaloneSf() && isVstTrackEngine(track && track.synth_engine) && !(window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive());
if (!nativeVst) return;
(track.midiItems || []).forEach(function (item) {
const key = _nativeVstItemKey(track, item, bpm);
if (!key || seen[key] || _nativeVstItemUrls[key]) return;
seen[key] = true;
ensureRenderNativeVstItem(track, item, bpm).then(function () {});
});
});
}, 800);
} catch (e) {}
};
// ── Native VSTi item render (standalone, transport) ────────────────────────
// Render TOÀN BỘ MIDI item bằng pedalboard (VST3) → decode AudioBuffer →
@@ -363,16 +436,10 @@ const scheduleNativeVstItem = (track, item, offsetTime, context, destNode, bpm,
const end = itemStartAbs + ((n.start_beat || 0) + (n.duration_beats || 1)) * secPerBeat;
if (end > itemEndAbs) itemEndAbs = end;
});
window.SonicAPI.midiRender({
instrument_id: instrumentId,
notes: notes.map(function (n) { return { pitch: n.pitch || 60, start_beat: n.start_beat || 0, duration_beats: n.duration_beats || 1, velocity: n.velocity != null ? n.velocity : 0.8 }; }),
bpm: parseFloat(bpm) || 120,
preset_id: (eng && eng.preset_id) || undefined,
preset_path: (eng && eng.preset_path) || undefined,
preset_data: (eng && eng.preset_data) || undefined,
}).then(function (res) {
if (!res || !res.success || !res.url) return;
fetch(API_BASE_URL + res.url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
const key = _nativeVstItemKey(track, item, bpm);
const scheduleUrl = function (url) {
if (!url) return;
fetch(url).then(function (r) { return r.arrayBuffer(); }).then(function (buf) {
context.decodeAudioData(buf, function (audioBuf) {
try {
if (opts && typeof opts.isActive === 'function' && !opts.isActive()) return;
@@ -393,7 +460,12 @@ const scheduleNativeVstItem = (track, item, offsetTime, context, destNode, bpm,
} catch (e) { console.warn('[NativeVST] schedule decode error:', e); }
}, function () {});
}).catch(function () {});
}).catch(function () {});
};
if (key && _nativeVstItemUrls[key]) { scheduleUrl(_nativeVstItemUrls[key]); return; }
ensureRenderNativeVstItem(track, item, bpm).then(function (url) {
if (!url) return;
scheduleUrl(url);
});
} catch (e) { console.warn('[NativeVST] scheduleNativeVstItem error:', e); }
};
@@ -17506,6 +17578,7 @@ const App = () => {
loadAudioBuffersForTracks(restoredTracks).catch(function(err) { console.warn('loadAudioBuffersForTracks error:', err); });
trackMidiChannelsRef.current = {};
preloadTrackInstruments(restoredTracks).catch(function(err) { console.warn('preloadTrackInstruments error:', err); });
prerenderNativeVstItems(restoredTracks, restoredBpm);
setBpm(restoredBpm.toString());
setSelectedTrackId(restoredTracks[0]?.id || '1');
setProjectName(lastName);
@@ -17644,6 +17717,7 @@ const App = () => {
if (restoredTracks.length > 0) {
setTracks(restoredTracks);
setBpm(restoredBpm.toString());
prerenderNativeVstItems(restoredTracks, restoredBpm);
if (restoredSessionTabs.length > 0) {
setSessionTabs(restoredSessionTabs);
}
+8 -4
View File
@@ -57,10 +57,14 @@ const scheduleNativeSfItem=(track,item,offsetTime,context,destNode,bpm,opts)=>{t
// Render 1 note bằng backend pedalboard (VST3 đúng instrument_id/preset) →
// play WAV — giống hệt nativeSf nhưng qua midiRender; map token riêng.
const _nativeVstPreviews={};const playNativeVstNote=(track,pitch,velocity,durationMs,startTime,key)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return;const k=key||(track?track.id:'global');const prev=_nativeVstPreviews[k];const token=(prev?prev.token:0)+1;const durationSec=Math.max(0.2,(durationMs||500)/1000);window.SonicAPI.midiRender({instrument_id:instrumentId,notes:[{pitch:pitch,start_beat:0,duration_beats:durationSec*2,velocity:velocity!=null?velocity:0.8}],bpm:120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return;if(_nativeVstPreviews[k]&&_nativeVstPreviews[k].token!==token)return;// stale
const audio=new Audio(API_BASE_URL+res.url);_nativeVstPreviews[k]={audio:audio,token:token};const ctx=getAudioContext();const delay=startTime?Math.max(0,(startTime-ctx.currentTime)*1000):0;setTimeout(function(){if(!_nativeVstPreviews[k]||_nativeVstPreviews[k].audio!==audio)return;audio.play().catch(function(){});audio._vstStopTimer&&clearTimeout(audio._vstStopTimer);audio._vstStopTimer=setTimeout(function(){try{audio.pause();}catch(e){}},durationSec*1000+400);},delay);}).catch(function(){});}catch(e){console.warn('[NativeVST] playNativeVstNote error:',e);}};const stopNativeVstNote=key=>{try{const k=key||'global';const prev=_nativeVstPreviews[k];if(!prev)return;prev.token++;try{if(prev.audio){prev.audio.pause();prev.audio.currentTime=0;}}catch(e){}}catch(e){}};const stopAllNativeVstNotes=()=>{try{Object.keys(_nativeVstPreviews).forEach(function(k){stopNativeVstNote(k);});}catch(e){}};// ── Native VSTi item render (standalone, transport) ────────────────────────
const audio=new Audio(API_BASE_URL+res.url);_nativeVstPreviews[k]={audio:audio,token:token};const ctx=getAudioContext();const delay=startTime?Math.max(0,(startTime-ctx.currentTime)*1000):0;setTimeout(function(){if(!_nativeVstPreviews[k]||_nativeVstPreviews[k].audio!==audio)return;audio.play().catch(function(){});audio._vstStopTimer&&clearTimeout(audio._vstStopTimer);audio._vstStopTimer=setTimeout(function(){try{audio.pause();}catch(e){}},durationSec*1000+400);},delay);}).catch(function(){});}catch(e){console.warn('[NativeVST] playNativeVstNote error:',e);}};const stopNativeVstNote=key=>{try{const k=key||'global';const prev=_nativeVstPreviews[k];if(!prev)return;prev.token++;try{if(prev.audio){prev.audio.pause();prev.audio.currentTime=0;}}catch(e){}}catch(e){}};const stopAllNativeVstNotes=()=>{try{Object.keys(_nativeVstPreviews).forEach(function(k){stopNativeVstNote(k);});}catch(e){}};// URL cache cho midi-render: key = fingerprint (item+instrument+preset+notes)
// → re-play cùng item dùng lại WAV đã render, không chờ round-trip.
const _nativeVstItemUrls={};const _nativeVstItemKey=(track,item,bpm)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId||'';const notes=item&&item.notes||[];const nk=notes.map(function(n){return(n.pitch||60)+':'+(n.start_beat||0)+':'+(n.duration_beats||1)+':'+(n.velocity!=null?n.velocity:0.8);}).join(',');return[item&&item.id?item.id:instrumentId+':'+nk,instrumentId,eng&&eng.preset_id||'',eng&&eng.preset_path||'',bpm].join('|')+'|'+nk;}catch(e){return'';}};const _renderNativeVstItem=(track,item,bpm)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return Promise.resolve(null);const notes=item&&item.notes||[];if(!notes.length)return Promise.resolve(null);return window.SonicAPI.midiRender({instrument_id:instrumentId,notes:notes.map(function(n){return{pitch:n.pitch||60,start_beat:n.start_beat||0,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8};}),bpm:parseFloat(bpm)||120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return null;return API_BASE_URL+res.url;}).catch(function(){return null;});}catch(e){return Promise.resolve(null);}};// Dedupe in-flight render: play trong lúc pre-render đang chạy → chờ cùng promise,
// không bắn duplicate request (tránh queue sau RENDER_LOCK + render 2 lần).
const _nativeVstItemPending={};const ensureRenderNativeVstItem=(track,item,bpm)=>{const key=_nativeVstItemKey(track,item,bpm);if(!key)return _renderNativeVstItem(track,item,bpm);if(_nativeVstItemUrls[key])return Promise.resolve(_nativeVstItemUrls[key]);if(_nativeVstItemPending[key])return _nativeVstItemPending[key];const p=_renderNativeVstItem(track,item,bpm).then(function(url){if(url)_nativeVstItemUrls[key]=url;return url;}).then(function(url){delete _nativeVstItemPending[key];return url;});_nativeVstItemPending[key]=p;return p;};let _prerenderNativeVstTimer=null;const prerenderNativeVstItems=(tracks,bpm)=>{try{if(!isStandaloneSf())return;if(_prerenderNativeVstTimer){clearTimeout(_prerenderNativeVstTimer);_prerenderNativeVstTimer=null;}_prerenderNativeVstTimer=setTimeout(function(){_prerenderNativeVstTimer=null;const seen={};(tracks||[]).forEach(function(track){const nativeVst=isStandaloneSf()&&isVstTrackEngine(track&&track.synth_engine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());if(!nativeVst)return;(track.midiItems||[]).forEach(function(item){const key=_nativeVstItemKey(track,item,bpm);if(!key||seen[key]||_nativeVstItemUrls[key])return;seen[key]=true;ensureRenderNativeVstItem(track,item,bpm).then(function(){});});});},800);}catch(e){}};// ── Native VSTi item render (standalone, transport) ────────────────────────
// Render TOÀN BỘ MIDI item bằng pedalboard (VST3) → decode AudioBuffer →
// schedule giống scheduleNativeSfItem. opts giống hệt nativeSf.
const scheduleNativeVstItem=(track,item,offsetTime,context,destNode,bpm,opts)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return;const notes=item&&item.notes||[];if(!notes.length)return;const secPerBeat=60.0/(parseInt(bpm)||120);const baseOffsetSec=opts&&opts.baseOffsetSec||0;const itemStartAbs=baseOffsetSec+(item.startTime||0);let itemEndAbs=itemStartAbs+0.05;notes.forEach(function(n){const end=itemStartAbs+((n.start_beat||0)+(n.duration_beats||1))*secPerBeat;if(end>itemEndAbs)itemEndAbs=end;});window.SonicAPI.midiRender({instrument_id:instrumentId,notes:notes.map(function(n){return{pitch:n.pitch||60,start_beat:n.start_beat||0,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8};}),bpm:parseFloat(bpm)||120,preset_id:eng&&eng.preset_id||undefined,preset_path:eng&&eng.preset_path||undefined,preset_data:eng&&eng.preset_data||undefined}).then(function(res){if(!res||!res.success||!res.url)return;fetch(API_BASE_URL+res.url).then(function(r){return r.arrayBuffer();}).then(function(buf){context.decodeAudioData(buf,function(audioBuf){try{if(opts&&typeof opts.isActive==='function'&&!opts.isActive())return;const src=context.createBufferSource();src.buffer=audioBuf;src.connect(destNode);if(offsetTime<itemStartAbs){src.start(context.currentTime+(itemStartAbs-offsetTime),0);}else if(offsetTime<itemEndAbs){src.start(context.currentTime,Math.min(offsetTime-itemStartAbs,Math.max(0,audioBuf.duration-0.05)));}else{return;}if(opts&&opts.limitSec&&opts.limitSec<itemStartAbs+audioBuf.duration){src.stop(context.currentTime+Math.max(0.02,opts.limitSec-Math.max(offsetTime,itemStartAbs)));}if(opts&&opts.sources&&opts.sources.push)opts.sources.push(src);}catch(e){console.warn('[NativeVST] schedule decode error:',e);}},function(){});}).catch(function(){});}).catch(function(){});}catch(e){console.warn('[NativeVST] scheduleNativeVstItem error:',e);}};// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
const scheduleNativeVstItem=(track,item,offsetTime,context,destNode,bpm,opts)=>{try{const eng=track&&track.synth_engine;const instrumentId=eng&&eng.plugin_id||track&&track.instrumentId;if(!instrumentId||!isVstTrackEngine(eng))return;const notes=item&&item.notes||[];if(!notes.length)return;const secPerBeat=60.0/(parseInt(bpm)||120);const baseOffsetSec=opts&&opts.baseOffsetSec||0;const itemStartAbs=baseOffsetSec+(item.startTime||0);let itemEndAbs=itemStartAbs+0.05;notes.forEach(function(n){const end=itemStartAbs+((n.start_beat||0)+(n.duration_beats||1))*secPerBeat;if(end>itemEndAbs)itemEndAbs=end;});const key=_nativeVstItemKey(track,item,bpm);const scheduleUrl=function(url){if(!url)return;fetch(url).then(function(r){return r.arrayBuffer();}).then(function(buf){context.decodeAudioData(buf,function(audioBuf){try{if(opts&&typeof opts.isActive==='function'&&!opts.isActive())return;const src=context.createBufferSource();src.buffer=audioBuf;src.connect(destNode);if(offsetTime<itemStartAbs){src.start(context.currentTime+(itemStartAbs-offsetTime),0);}else if(offsetTime<itemEndAbs){src.start(context.currentTime,Math.min(offsetTime-itemStartAbs,Math.max(0,audioBuf.duration-0.05)));}else{return;}if(opts&&opts.limitSec&&opts.limitSec<itemStartAbs+audioBuf.duration){src.stop(context.currentTime+Math.max(0.02,opts.limitSec-Math.max(offsetTime,itemStartAbs)));}if(opts&&opts.sources&&opts.sources.push)opts.sources.push(src);}catch(e){console.warn('[NativeVST] schedule decode error:',e);}},function(){});}).catch(function(){});};if(key&&_nativeVstItemUrls[key]){scheduleUrl(_nativeVstItemUrls[key]);return;}ensureRenderNativeVstItem(track,item,bpm).then(function(url){if(!url)return;scheduleUrl(url);});}catch(e){console.warn('[NativeVST] scheduleNativeVstItem error:',e);}};// Handle ?sfs=<encoded> from double-clicking a .sfs file (opens domain -> loads project)
(function handleSfsDeepLink(){try{const params=new URLSearchParams(window.location.search);const sfsParam=params.get('sfs');if(!sfsParam)return;const decoded=JSON.parse(decodeURIComponent(sfsParam));window.__pendingSfsProject=decoded;// consumed after auth in App
if(window.history.replaceState){window.history.replaceState({},document.title,window.location.pathname);}}catch(e){window.__pendingSfsProject=null;}})();// Storage for server-side file IDs mapped to track IDs
let serverFileIdMap={};let audioCtx;let masterBus=null;// { input, compressor, analyser, output, masteringActive, dryInput, dryOutput }
@@ -1051,10 +1055,10 @@ const sectionInnerTracks=[];updatedTracks.forEach(function(ut){(ut.sections||[])
const prof=currentUser||function(){try{return JSON.parse(localStorage.getItem('sonic_user')||'null');}catch(e){return null;}}();if(prof&&window.SonicAPI&&window.SonicAPI.listCloudProjects){try{const cloudList=await window.SonicAPI.listCloudProjects();if(cloudList&&cloudList.length>0){const latest=cloudList[0];// backend ORDER BY updated_at DESC
lastId=latest.id;lastName=latest.name||lastName;const proj=await window.SonicAPI.getCloudProject(lastId);if(proj&&proj.data_json)parsed=JSON.parse(proj.data_json);}}catch(e){console.warn('restore cloud fallback error:',e);}}}else{try{if(lastId.startsWith('local_')){var localData=localStorage.getItem('sonic_local_project_data');if(localData)parsed=JSON.parse(localData);}else{var proj=await window.SonicAPI.getCloudProject(lastId);if(proj)parsed=JSON.parse(proj.data_json);}}catch(e){console.warn('restore load error:',e);}}try{if(!parsed)return;var restoredBpm=bpm;var restoredTracks=[];var restoredSessionTabs=[];var restoredSubTabs=[];if(parsed.main_session){var result=deserializeProjectFromSchema(parsed);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;// Bỏ qua zoom ≤ 1 (data cũ lưu 1.0 — lỗi __currentZoom của Media
// 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);}else{restoredTracks=(parsed.tracks||[]).map(function(t){var rest=Object.assign({},t);delete rest.height;return Object.assign({},rest,{buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null});});}setTracks(restoredTracks);loadAudioBuffersForTracks(restoredTracks).catch(function(err){console.warn('loadAudioBuffersForTracks error:',err);});trackMidiChannelsRef.current={};preloadTrackInstruments(restoredTracks).catch(function(err){console.warn('preloadTrackInstruments error:',err);});setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(lastName);setCurrentProjectId(lastId);localStorage.setItem('sonic_project_id',lastId);setSessionTabs(restoredSessionTabs);setSubTabs(restoredSubTabs);// DIAG section-tab block: log tracks + sections của section tab sau restore
if(result.zoom>1&&setZoom)setZoom(result.zoom);if(result.masteringSettings)setMasteringSettings(result.masteringSettings);}else{restoredTracks=(parsed.tracks||[]).map(function(t){var rest=Object.assign({},t);delete rest.height;return Object.assign({},rest,{buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null});});}setTracks(restoredTracks);loadAudioBuffersForTracks(restoredTracks).catch(function(err){console.warn('loadAudioBuffersForTracks error:',err);});trackMidiChannelsRef.current={};preloadTrackInstruments(restoredTracks).catch(function(err){console.warn('preloadTrackInstruments error:',err);});prerenderNativeVstItems(restoredTracks,restoredBpm);setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(lastName);setCurrentProjectId(lastId);localStorage.setItem('sonic_project_id',lastId);setSessionTabs(restoredSessionTabs);setSubTabs(restoredSubTabs);// DIAG section-tab block: log tracks + sections của section tab sau restore
try{(restoredSessionTabs||[]).forEach(function(st){var secCount=0,midiCount=0,clipCount=0;(st.tracks||[]).forEach(function(tr){secCount+=(tr.sections||[]).length;midiCount+=(tr.midiItems||[]).length;clipCount+=(tr.clips||[]).length;});console.log('[Restore] sessionTab',st.id,'sectionId',st.sectionId,'tracks',(st.tracks||[]).length,'sections',secCount,'midi',midiCount,'clips',clipCount);});}catch(e){}var restoredItemCount=0;restoredTracks.forEach(function(rt){if(rt.clips)restoredItemCount+=rt.clips.length;if(rt.midiItems)restoredItemCount+=rt.midiItems.length;if(rt.sections)restoredItemCount+=rt.sections.length;});showToast('Đã khôi phục dự án "'+lastName+'" ('+restoredItemCount+' items).','info');}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');}};useEffect(()=>{const checkAuthStatus=async()=>{const savedToken=localStorage.getItem('sonic_token');if(!savedToken){setIsMandatoryLogin(true);setAuthMode('login');setAuthModalOpen(true);return;}try{const profile=await window.SonicAPI.getProfile();setCurrentUser(profile);localStorage.setItem('sonic_user',JSON.stringify(profile));if(profile.must_change_password){setIsMandatoryLogin(true);setAuthMode('force_change');setAuthModalOpen(true);}else{setIsMandatoryLogin(false);setAuthModalOpen(false);restoreLastSessionProject();}}catch(err){const cached=localStorage.getItem('sonic_user');if(cached){try{setCurrentUser(JSON.parse(cached));}catch(_){}setIsMandatoryLogin(false);setAuthModalOpen(false);restoreLastSessionProject();}else{localStorage.removeItem('sonic_token');localStorage.removeItem('sonic_user');setCurrentUser(null);setIsMandatoryLogin(true);setAuthMode('login');setAuthModalOpen(true);}}};checkAuthStatus();},[]);const loadPendingSfsProject=()=>{const proj=window.__pendingSfsProject;if(!proj)return;try{let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(proj.main_session){const result=deserializeProjectFromSchema(proj);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.masteringSettings){setMasteringSettings(result.masteringSettings);}else{setMasteringSettings({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0,carlaBridgeActive:false,carlaBridge:{plugin:'',plugin_path:'',connected:false}});}}else{restoredTracks=(proj.tracks||[]).map(t=>{const{height:_h,...rest}=t;return{...rest,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null};});}if(restoredTracks.length>0){setTracks(restoredTracks);setBpm(restoredBpm.toString());if(restoredSessionTabs.length>0){setSessionTabs(restoredSessionTabs);}if(restoredSubTabs.length>0){setSubTabs(restoredSubTabs);}showToast(`Đã tải dự án "${proj.metadata?.title||proj.name||'Dự án mới'}" từ liên kết .sfs thành công!`,"success");}}catch(e){showToast("Lỗi tải dự án từ .sfs","error");}finally{window.__pendingSfsProject=null;}};const handleAuthSuccess=user=>{setCurrentUser(user);if(user.must_change_password){setIsMandatoryLogin(true);setAuthMode('force_change');setAuthModalOpen(true);}else{setIsMandatoryLogin(false);setAuthModalOpen(false);restoreLastSessionProject();const loadPrefs=p=>{if(!p)return;if(p.showAIPanel!==undefined)setShowAIPanel(p.showAIPanel);if(p.showExportPanel!==undefined)setShowExportPanel(p.showExportPanel);if(p.showSelectionPanel!==undefined)setShowSelectionPanel(p.showSelectionPanel);if(p.showPythonToolsPanel!==undefined)setShowPythonToolsPanel(p.showPythonToolsPanel);if(p.showMediaExplorer!==undefined)setShowMediaExplorer(p.showMediaExplorer);if(p.showFxRack!==undefined)setShowFxRack(p.showFxRack);if(p.showMidiEvents!==undefined)setShowMidiEvents(p.showMidiEvents);if(p.panelPositions)setPanelPositions(p.panelPositions);if(p.rightSidebarWidth)setRightSidebarWidth(p.rightSidebarWidth);if(p.mediaExplorerHeight)setMediaExplorerHeight(p.mediaExplorerHeight);if(p.selectedProviderId)setSelectedProviderId(p.selectedProviderId);};(async()=>{try{const data=await window.SonicAPI.getPreferences();if(data&&data.preferences)loadPrefs(data.preferences);else{const cached=localStorage.getItem('sonic_preferences');if(cached)loadPrefs(JSON.parse(cached));}}catch(e){const cached=localStorage.getItem('sonic_preferences');if(cached)loadPrefs(JSON.parse(cached));}try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setAiProviders(data.providers);const active=data.providers.find(p=>p.is_active)||data.providers[0];if(active)setSelectedProviderId(active.id);}}catch(e){}try{window.SonicAPI.getSoundfontCatalog().then(cat=>{window.__soundfontCatalog=cat;}).catch(()=>{});}catch(e){}// Re-fetch instrument data after auth (useEffect on mount runs before token is set)
localStorage.removeItem('sonic_project_id');localStorage.removeItem('sonic_project_name');}};useEffect(()=>{const checkAuthStatus=async()=>{const savedToken=localStorage.getItem('sonic_token');if(!savedToken){setIsMandatoryLogin(true);setAuthMode('login');setAuthModalOpen(true);return;}try{const profile=await window.SonicAPI.getProfile();setCurrentUser(profile);localStorage.setItem('sonic_user',JSON.stringify(profile));if(profile.must_change_password){setIsMandatoryLogin(true);setAuthMode('force_change');setAuthModalOpen(true);}else{setIsMandatoryLogin(false);setAuthModalOpen(false);restoreLastSessionProject();}}catch(err){const cached=localStorage.getItem('sonic_user');if(cached){try{setCurrentUser(JSON.parse(cached));}catch(_){}setIsMandatoryLogin(false);setAuthModalOpen(false);restoreLastSessionProject();}else{localStorage.removeItem('sonic_token');localStorage.removeItem('sonic_user');setCurrentUser(null);setIsMandatoryLogin(true);setAuthMode('login');setAuthModalOpen(true);}}};checkAuthStatus();},[]);const loadPendingSfsProject=()=>{const proj=window.__pendingSfsProject;if(!proj)return;try{let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(proj.main_session){const result=deserializeProjectFromSchema(proj);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.masteringSettings){setMasteringSettings(result.masteringSettings);}else{setMasteringSettings({masterConnected:false,activeModule:'eq',eqActive:true,imagerActive:true,maximizerActive:true,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:DEFAULT_MASTER_CHAIN.map(m=>({...m})),compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0,carlaBridgeActive:false,carlaBridge:{plugin:'',plugin_path:'',connected:false}});}}else{restoredTracks=(proj.tracks||[]).map(t=>{const{height:_h,...rest}=t;return{...rest,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null};});}if(restoredTracks.length>0){setTracks(restoredTracks);setBpm(restoredBpm.toString());prerenderNativeVstItems(restoredTracks,restoredBpm);if(restoredSessionTabs.length>0){setSessionTabs(restoredSessionTabs);}if(restoredSubTabs.length>0){setSubTabs(restoredSubTabs);}showToast(`Đã tải dự án "${proj.metadata?.title||proj.name||'Dự án mới'}" từ liên kết .sfs thành công!`,"success");}}catch(e){showToast("Lỗi tải dự án từ .sfs","error");}finally{window.__pendingSfsProject=null;}};const handleAuthSuccess=user=>{setCurrentUser(user);if(user.must_change_password){setIsMandatoryLogin(true);setAuthMode('force_change');setAuthModalOpen(true);}else{setIsMandatoryLogin(false);setAuthModalOpen(false);restoreLastSessionProject();const loadPrefs=p=>{if(!p)return;if(p.showAIPanel!==undefined)setShowAIPanel(p.showAIPanel);if(p.showExportPanel!==undefined)setShowExportPanel(p.showExportPanel);if(p.showSelectionPanel!==undefined)setShowSelectionPanel(p.showSelectionPanel);if(p.showPythonToolsPanel!==undefined)setShowPythonToolsPanel(p.showPythonToolsPanel);if(p.showMediaExplorer!==undefined)setShowMediaExplorer(p.showMediaExplorer);if(p.showFxRack!==undefined)setShowFxRack(p.showFxRack);if(p.showMidiEvents!==undefined)setShowMidiEvents(p.showMidiEvents);if(p.panelPositions)setPanelPositions(p.panelPositions);if(p.rightSidebarWidth)setRightSidebarWidth(p.rightSidebarWidth);if(p.mediaExplorerHeight)setMediaExplorerHeight(p.mediaExplorerHeight);if(p.selectedProviderId)setSelectedProviderId(p.selectedProviderId);};(async()=>{try{const data=await window.SonicAPI.getPreferences();if(data&&data.preferences)loadPrefs(data.preferences);else{const cached=localStorage.getItem('sonic_preferences');if(cached)loadPrefs(JSON.parse(cached));}}catch(e){const cached=localStorage.getItem('sonic_preferences');if(cached)loadPrefs(JSON.parse(cached));}try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setAiProviders(data.providers);const active=data.providers.find(p=>p.is_active)||data.providers[0];if(active)setSelectedProviderId(active.id);}}catch(e){}try{window.SonicAPI.getSoundfontCatalog().then(cat=>{window.__soundfontCatalog=cat;}).catch(()=>{});}catch(e){}// Re-fetch instrument data after auth (useEffect on mount runs before token is set)
try{window.SonicAPI.listPlugins().then(async data=>{try{const catResp=(await window.SonicAPI.getSoundfontCatalog?.())??(await fetch('/api/v1/plugins/soundfonts/catalog').then(r=>r.json()));const catalog=catResp.full_catalog||{};data.soundfonts=(data.soundfonts||[]).map(sf=>{const sfId=sf.id.replace('sf_','');const catEntry=catalog[sfId.toLowerCase()]||catalog[sfId];if(catEntry&&catEntry.instruments)return{...sf,presets:catEntry.instruments};return sf;});}catch(e){console.warn('Catalog fetch error:',e);}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