diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 91566c8..9dc9e91 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -6,7 +6,7 @@ from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Optional, Any from app.config import settings -from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin, render_soundfont_midi_to_audio +from app.core.vst_engine import PluginManager, HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2 from app.core.soundfont_inspector import SoundFontInspector from app.core.soundfont_converter import SoundFontConverter @@ -975,6 +975,7 @@ async def midi_render(req: MidiRenderRequest, current_user: dict = Depends(get_c ) +@_render_locked def _render_midi_notes_pedalboard(instrument_id: str, notes: list, bpm: float, sample_rate: int, preset_id=None, preset_path=None, preset_data_b64=None, soundfont_bank=None, @@ -1162,7 +1163,7 @@ def _carla_bridge_running() -> bool: @router.post("/render") -async def render_project( +def render_project( req: RenderRequest, current_user: dict = Depends(get_current_user) ): diff --git a/app/core/render_engine.py b/app/core/render_engine.py index 26cd2e4..8984043 100644 --- a/app/core/render_engine.py +++ b/app/core/render_engine.py @@ -11,6 +11,7 @@ from app.core.vst_engine import ( HAS_PEDALBOARD, HAS_PYFLUIDSYNTH, apply_preset_to_plugin, + RENDER_LOCK, ) logger = logging.getLogger(__name__) @@ -451,30 +452,31 @@ class PythonRenderEngine: return session_buffer def render_project(self, project_json: dict, output_filepath: str): - bpm = project_json["metadata"]["bpm"] - time_sig_num = project_json["metadata"].get("time_signature_numerator", 4) - main_session = project_json["main_session"] - section_store = project_json.get("section_store", {}) + with RENDER_LOCK: + bpm = project_json["metadata"]["bpm"] + time_sig_num = project_json["metadata"].get("time_signature_numerator", 4) + main_session = project_json["main_session"] + section_store = project_json.get("section_store", {}) - # Compute total project samples - total_bars = main_session.get("length_bars", 16.0) - total_samples = self.bars_to_samples(total_bars, bpm, time_sig_num) - - # Render main session - master_buffer = self.render_session_container( - session=main_session, - section_store=section_store, - bpm=bpm, - time_sig_num=time_sig_num, - total_samples=total_samples, - _cache={}, - ) + # Compute total project samples + total_bars = main_session.get("length_bars", 16.0) + total_samples = self.bars_to_samples(total_bars, bpm, time_sig_num) - # Normalization to prevent clipping - max_peak = np.max(np.abs(master_buffer)) - if max_peak > 1.0: - master_buffer /= max_peak + # Render main session + master_buffer = self.render_session_container( + session=main_session, + section_store=section_store, + bpm=bpm, + time_sig_num=time_sig_num, + total_samples=total_samples, + _cache={}, + ) - # Write final output file - sf.write(output_filepath, master_buffer.T, self.sample_rate) - return output_filepath + # Normalization to prevent clipping + max_peak = np.max(np.abs(master_buffer)) + if max_peak > 1.0: + master_buffer /= max_peak + + # Write final output file + sf.write(output_filepath, master_buffer.T, self.sample_rate) + return output_filepath diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 54089f0..0b3fb2d 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -3,8 +3,21 @@ import os import json import numpy as np import functools +import threading from ctypes import c_char_p +# pedalboard VST3 + pyfluidsynth KHÔNG thread-safe: tuần tự hóa mọi native render +RENDER_LOCK = threading.RLock() + + +def _render_locked(fn): + """Chạy hàm native (VST3/pyfluidsynth) dưới RENDER_LOCK — không thread-safe.""" + @functools.wraps(fn) + def wrapper(*args, **kwargs): + with RENDER_LOCK: + return fn(*args, **kwargs) + return wrapper + def _ensure_fluidsynth_dll_on_path(): """Frozen (PyInstaller): pyfluidsynth tim dll qua ctypes.util.find_library @@ -25,6 +38,7 @@ def midi_note_to_freq(note_number: int) -> float: return 440.0 * (2.0 ** ((note_number - 69) / 12.0)) +@_render_locked def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = 0, program: int = 0, sr: int = 44100, bpm: float = 120.0) -> np.ndarray: """Native FluidSynth (pyfluidsynth) — render MIDI events bằng soundfont THẬT. @@ -92,6 +106,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = except Exception: pass +@_render_locked def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray: beat_duration_sec = 60.0 / max(30.0, bpm) max_duration_sec = 2.0 @@ -336,6 +351,7 @@ class PluginManager: } return list(sf_map.values()) + @_render_locked def load_vst(self, plugin_name: str, preset_data: dict = None): if not HAS_PEDALBOARD: return None diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index c6f6b97..400b845 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index b263f70..70864f4 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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 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 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 diff --git a/app/templates/index.html b/app/templates/index.html index 5663b4a..aef2571 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -50,7 +50,7 @@ - +