diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index a14aa7b..682a042 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -12628,16 +12628,6 @@ const ExportModal = ({ open, onClose, exportSettings, setExportSettings, isExpor
- {isExporting && ( -
-
-
= 0 ? exportProgress : 25) + '%' }} /> -
-
- {exportProgress >= 0 ? 'Đang xuất... ' + Math.round(exportProgress) + '%' : 'Đang xử lý...'} -
-
- )}
+ )} +
+
+
+ ); +}; + // --- Embedded VST GUI (PLAN_MASTERBUS_VST_GUI_EMBED.md) -------------------- // Frame-capture + input relay: hiển thị native GUI qua HTTP /frame (~10fps), // gửi pointer/key về bridge qua /input. Dùng chung MasteringModal + FXRackModal. @@ -18100,6 +18122,7 @@ const App = () => { const lastTempCompileTimeRef = useRef(0); const nextMetronomeBeatRef = useRef(0); const [showExportPanel, setShowExportPanel] = useState(false); + const [showExportProgress, setShowExportProgress] = useState(false); // modal tiến trình export RIÊNG React.useEffect(() => { if (showExportPanel) setTimeout(() => { try { window.lucide && window.lucide.createIcons(); } catch (e) { } }, 80); }, [showExportPanel]); @@ -28041,6 +28064,8 @@ const App = () => { if (!exportSettings.loop && allOnServer && serverStatus === 'connected') { // Use server-side export setIsExporting(true); + setShowExportPanel(false); + setShowExportProgress(true); setExportProgress(-1); showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...", "info"); try { @@ -28113,6 +28138,7 @@ const App = () => { clientSideExport(exportTracks); } finally { setIsExporting(false); + setShowExportProgress(false); } } else { // Client-side export (existing working code) @@ -28177,6 +28203,8 @@ const App = () => { if (!masterBus) { showToast("Hãy phát thử một lần để khởi tạo audio engine trước.", "warning"); return; } if (isExporting) return; setIsExporting(true); + setShowExportPanel(false); + setShowExportProgress(true); const audioCtx = getAudioContext(); try { const allTracks = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : tracks; @@ -28275,6 +28303,7 @@ const App = () => { showToast("Lỗi bounce: " + err.message, "error"); } finally { setIsExporting(false); + setShowExportProgress(false); } }; const handleSaveLocalProject = (name) => { @@ -28483,6 +28512,8 @@ const App = () => { }; const clientSideExport = async (activeTracks, loopInfo) => { setIsExporting(true); + setShowExportPanel(false); + setShowExportProgress(true); setExportProgress(5); showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...", "info"); // Yield 1 frame → React vẽ thanh "Đang xử lý..." trước khi graph build + @@ -28789,7 +28820,7 @@ const App = () => { if (dres && dres.saved_path) { showToast(`Xuất bản âm thanh hoàn tất → ${dres.saved_path}`, "success"); } else { - showToast("Xuất bản âm thanh hoàn tất!", "success"); + showToast("Không thể ghi file export — engine không lưu được file. Kiểm tra kết nối engine và thư mục export.", "error"); } } // Export hoàn tất → tự đóng modal. @@ -28822,6 +28853,7 @@ const App = () => { } finally { setExportProgress(-1); setIsExporting(false); + setShowExportProgress(false); } }; @@ -34613,6 +34645,11 @@ STRICT CONSTRAINTS: onNoteOn: vkbNoteOn, onNoteOff: vkbNoteOff, bpm: bpm + }), /*#__PURE__*/React.createElement(ExportProgressModal, { + open: showExportProgress, + exportProgress: exportProgress, + isExporting: isExporting, + onClose: () => setShowExportProgress(false) }), /*#__PURE__*/React.createElement(ExportModal, { open: showExportPanel, onClose: () => setShowExportPanel(false), diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 11b89d8..c022fee 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -857,7 +857,10 @@ const specMods=(spectrumModules?spectrumModules():null)||(getModule?[getModule() const applyAll=applyTo||(fn=>{if(typeof window!=='undefined'&&window.__getTrackFxModule){const a=window.__getTrackFxModule(track.id,'eqpro');if(a)fn(a);}if(typeof window!=='undefined'&&window.__getTrackSfFxModule){const b=window.__getTrackSfFxModule(track.id,'eqpro');if(b)fn(b);}});const onDown=e=>{const cv=canvasRef.current,w=cv.clientWidth,h=cv.clientHeight;const{x,y}=toLocal(e);const s=st.current;let hit=false;for(let i=0;it+1);};const onMove=e=>{const s=st.current;if(s.dragId===null)return;const cv=canvasRef.current,w=cv.clientWidth,h=cv.clientHeight;const{x,y}=toLocal(e);const mx=Math.max(0,Math.min(w,x)),my=Math.max(0,Math.min(h,y));const b=s.bands[s.dragId];if(s.mode==='center'){const f=eqproClamp(parseFloat(eqproXToFreq(mx,w).toFixed(1)),EQPRO_F_MIN,EQPRO_F_MAX);// Filters without a Gain control (highpass/lowpass/notch/bandpass) stay on // the 0 dB axis — only frequency is draggable for them. const g=eqproBandHasGain(b.type)?eqproClamp(parseFloat(eqproYToGain(my,h).toFixed(1)),-EQPRO_MAX_DB,EQPRO_MAX_DB):0;b.freq=f;b.gain=g;applyAll(mm=>mm.setBand(s.dragId,{freq:f,gain:g}));}else if(s.mode==='wing'){const q=eqproWingToQ(Math.abs(mx-eqproFreqToX(b.freq,w)));b.q=q;applyAll(mm=>mm.setBand(s.dragId,{q}));}setTick(t=>t+1);};const onUp=e=>{const s=st.current;if(s.dragId!==null)commit();s.dragId=null;s.mode=null;try{canvasRef.current.releasePointerCapture(e.pointerId);}catch(err){}};const onWheel=e=>{e.preventDefault();const s=st.current;if(s.selected===null)return;const b=s.bands[s.selected];b.q=eqproClamp(parseFloat((b.q+(e.deltaY<0?0.2:-0.2)).toFixed(1)),0.1,18);applyAll(mm=>mm.setBand(s.selected,{q:b.q}));setTick(t=>t+1);};const onDblClick=e=>{const cv=canvasRef.current,w=cv.clientWidth,h=cv.clientHeight;const{x,y}=toLocal(e);const s=st.current;const hitIdx=s.bands.findIndex(b=>Math.hypot(x-eqproFreqToX(b.freq,w),y-eqproGainToY(b.gain,h))<=10);if(hitIdx!==-1){s.bands.splice(hitIdx,1);if(s.selected===hitIdx)s.selected=null;else if(s.selected!==null&&s.selected>hitIdx)s.selected--;}else{if(s.bands.length>=EQPRO_MAX_BANDS)return;s.bands.push({type:'peaking',freq:eqproClamp(parseFloat(eqproXToFreq(x,w).toFixed(1)),EQPRO_F_MIN,EQPRO_F_MAX),gain:eqproClamp(parseFloat(eqproYToGain(y,h).toFixed(1)),-EQPRO_MAX_DB,EQPRO_MAX_DB),q:1.2,active:true});s.selected=s.bands.length-1;}applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);};const sel=st.current.selected!==null?st.current.bands[st.current.selected]:null;const selIdx=st.current.selected;const cvW=canvasRef.current?canvasRef.current.clientWidth:460;const cvH=240;const hudColor=selIdx!==null?EQPRO_BAND_COLORS[selIdx%EQPRO_BAND_COLORS.length]:EQPRO_BAND_COLORS[0];const hudXY=(()=>{if(st.current.hudPos){// Clamp so the HUD always stays inside the canvas area (buttons clickable). -return{hx:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientWidth:460)-268,st.current.hudPos.hx)),hy:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientHeight:240)-225,st.current.hudPos.hy))};}if(!sel)return null;const nx=eqproFreqToX(sel.freq,cvW),ny=eqproGainToY(sel.gain,cvH);let hx=nx-130,hy=ny-195;if(hx<8)hx=8;if(hx>cvW-268)hx=cvW-268;if(hy<8)hy=ny+24;return{hx,hy};})();return/*#__PURE__*/React.createElement("div",{className:"space-y-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[11px] font-mono"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Bands: ",/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold"},st.current.bands.length),"/8"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Amount:"),/*#__PURE__*/React.createElement("input",{type:"range",min:0,max:200,value:st.current.amount,onChange:e=>{st.current.amount=parseInt(e.target.value);applyAll(mm=>mm.setAmount(st.current.amount));commit();setTick(t=>t+1);},className:"w-20 h-1 cursor-pointer",style:{accentColor:'#2dd4bf'}}),/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold w-10"},st.current.amount,"%"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;s.bands=JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-slate-800 hover:bg-red-900/60 text-slate-300 border border-slate-700 rounded text-[10px]"},"Reset"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;if(s.bands.length>=EQPRO_MAX_BANDS)return;s.bands.push({type:'peaking',freq:1000,gain:0,q:1.0,active:true});s.selected=s.bands.length-1;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-teal-900/50 hover:bg-teal-800 text-teal-300 border border-teal-700 rounded text-[10px]"},"+ Band"))),/*#__PURE__*/React.createElement("div",{className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-60 block cursor-crosshair rounded-lg border border-slate-800 bg-slate-950",onPointerDown:onDown,onPointerMove:onMove,onPointerUp:onUp,onWheel:onWheel,onDoubleClick:onDblClick}),sel&&hudXY&&/*#__PURE__*/React.createElement("div",{className:"absolute z-10 w-64 rounded-xl p-3 text-[11px] font-mono space-y-2 pointer-events-auto cursor-move",style:{left:hudXY.hx,top:hudXY.hy,background:'rgba(15,23,42,0.94)',border:'1px solid rgba(45,212,191,0.3)',boxShadow:'0 10px 30px rgba(0,0,0,0.8)',backdropFilter:'blur(12px)'},onPointerDown:e=>{const s=st.current;s.dragHud=true;const r=e.currentTarget.getBoundingClientRect();s.hudOffsetX=e.clientX-r.left;s.hudOffsetY=e.clientY-r.top;e.currentTarget.setPointerCapture(e.pointerId);},onPointerMove:e=>{const s=st.current;if(!s.dragHud)return;const cv=canvasRef.current;const w=cv?cv.clientWidth:460;s.hudPos={hx:Math.max(0,Math.min(w-268,e.clientX-s.hudOffsetX-(cv?cv.getBoundingClientRect().left:0))),hy:Math.max(0,e.clientY-s.hudOffsetY-(cv?cv.getBoundingClientRect().top:0))};setTick(t=>t+1);},onPointerUp:e=>{st.current.dragHud=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-700/80 pb-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"w-4 h-4 rounded-full text-slate-950 font-bold flex items-center justify-center text-[10px]",style:{background:hudColor.badge}},selIdx+1),/*#__PURE__*/React.createElement("span",{className:"font-bold text-white uppercase tracking-wider"},"Band ",selIdx+1)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{title:"Bypass band",onClick:()=>{const s=st.current;s.bands[s.selected].active=!s.bands[s.selected].active;applyAll(mm=>mm.setBand(s.selected,{active:s.bands[s.selected].active}));commit();setTick(t=>t+1);},className:`w-5 h-5 rounded flex items-center justify-center text-[10px] ${sel.active?'bg-slate-800 text-slate-400':'bg-amber-600 text-white'}`},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-power-off"})),/*#__PURE__*/React.createElement("button",{title:"Delete band",onClick:()=>{const s=st.current;s.bands.splice(s.selected,1);s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"w-5 h-5 rounded bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white flex items-center justify-center text-[10px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"})))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[9px] text-slate-400 uppercase tracking-widest mb-1"},"Filter Shape"),/*#__PURE__*/React.createElement("select",{value:sel.type,onChange:ev=>{const s=st.current;s.bands[s.selected].type=ev.target.value;applyAll(mm=>mm.setBand(s.selected,{type:ev.target.value}));commit();setTick(t=>t+1);},className:"w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none"},['peaking','highpass','lowpass','lowshelf','highshelf','notch','bandpass'].map(t=>/*#__PURE__*/React.createElement("option",{key:t,value:t},t==='peaking'?'Bell / Peaking':t==='highpass'?'Low Cut / High Pass':t==='lowpass'?'High Cut / Low Pass':t==='lowshelf'?'Low Shelf':t==='highshelf'?'High Shelf':t==='notch'?'Notch / Band Stop':'Band Pass')))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 text-center bg-slate-950/80 p-2 rounded-lg border border-slate-800"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"FREQ"),/*#__PURE__*/React.createElement("div",{className:"text-teal-300 font-bold text-[11px]"},sel.freq>=1000?(sel.freq/1000).toFixed(2)+' kHz':Math.round(sel.freq)+' Hz')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"GAIN"),/*#__PURE__*/React.createElement("div",{className:"text-amber-400 font-bold text-[11px]"},eqproBandHasGain(sel.type)?(sel.gain>0?'+':'')+sel.gain.toFixed(1)+' dB':'0.0 dB')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"Q"),/*#__PURE__*/React.createElement("div",{className:"text-purple-400 font-bold text-[11px]"},sel.q.toFixed(1)))),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 text-center"},"Drag center: Freq & Gain · Wings/Wheel: Q · Dbl-click: add/delete"))));};const ExportModal=({open,onClose,exportSettings,setExportSettings,isExporting,exportProgress,onExport})=>{if(!open)return null;const pickDir=async()=>{try{const r=await fetch(`${API_BASE_URL}/api/v1/plugins/pick-dir`,{method:'POST'});if(r.ok){const d=await r.json();if(d&&d.path)setExportSettings(p=>({...p,exportDir:d.path}));}}catch(e){}};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3.5 h-3.5"})," EXPORT"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"p-4 max-h-[72vh] overflow-y-auto space-y-3"},/*#__PURE__*/React.createElement("div",{className:"bg-[#141414] border border-zinc-800 rounded px-2 py-1.5 space-y-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Tên file export"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.fileName,onChange:e=>setExportSettings(p=>({...p,fileName:e.target.value})),placeholder:"Để trống = dùng tên mặc định",className:"w-full bg-[#0d0d0d] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-end gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex-1 min-w-0"},/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Thư mục export"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 bg-[#0d0d0d] border border-zinc-800 rounded px-1.5 py-0.5 min-h-[22px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-cyan-400 text-[10px] shrink-0"}),exportSettings.exportDir?/*#__PURE__*/React.createElement("a",{className:"text-cyan-400 underline truncate cursor-pointer",title:exportSettings.exportDir,onClick:pickDir},exportSettings.exportDir):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 truncate"},"Chưa chọn — xuất sẽ mở Save-As"))),/*#__PURE__*/React.createElement("button",{onClick:pickDir,className:"shrink-0 px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-[10px] font-bold flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[10px]"})," Chọn thư mục")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.rememberExportDir,onChange:e=>setExportSettings(p=>({...p,rememberExportDir:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Lưu thư mục export làm mặc định"))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Nguồn"),/*#__PURE__*/React.createElement("select",{value:exportSettings.source,onChange:e=>setExportSettings(p=>({...p,source:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"project"},"Project (Mix)"),/*#__PURE__*/React.createElement("option",{value:"track_mix"},"Track Selection"),/*#__PURE__*/React.createElement("option",{value:"active_clip"},"Active Clip"),/*#__PURE__*/React.createElement("option",{value:"clip_selection"},"Clip Selection"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Định dạng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.format,onChange:e=>setExportSettings(p=>({...p,format:e.target.value,sampleRate:'44100',bitDepth:'float',quality:'44khz'})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"wav"},"WAV"),/*#__PURE__*/React.createElement("option",{value:"mp3"},"MP3"),/*#__PURE__*/React.createElement("option",{value:"ogg"},"OGG")))),exportSettings.format==='wav'?/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"SR (Hz)"),/*#__PURE__*/React.createElement("select",{value:exportSettings.sampleRate,onChange:e=>setExportSettings(p=>({...p,sampleRate:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44100"},"44100"),/*#__PURE__*/React.createElement("option",{value:"48000"},"48000"),/*#__PURE__*/React.createElement("option",{value:"96000"},"96000"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Bit"),/*#__PURE__*/React.createElement("select",{value:exportSettings.bitDepth,onChange:e=>setExportSettings(p=>({...p,bitDepth:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"16"},"16"),/*#__PURE__*/React.createElement("option",{value:"24"},"24"),/*#__PURE__*/React.createElement("option",{value:"float"},"32 float"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Kênh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))):/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Chất lượng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.quality,onChange:e=>setExportSettings(p=>({...p,quality:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44khz"},"44kHz"),/*#__PURE__*/React.createElement("option",{value:"lossless"},"Lossless"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Kênh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.dither,onChange:e=>setExportSettings(p=>({...p,dither:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Dither TPDF (PCM 16/24)")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.normalize,onChange:e=>setExportSettings(p=>({...p,normalize:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Normalize")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.monoCheck,onChange:e=>setExportSettings(p=>({...p,monoCheck:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Cảnh báo mono (side/mid)")),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Normalize target"),/*#__PURE__*/React.createElement("select",{value:exportSettings.normalizeTarget,onChange:e=>setExportSettings(p=>({...p,normalizeTarget:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"-1dbtp"},"-1 dBTP"),/*#__PURE__*/React.createElement("option",{value:"-14lufs"},"-14 LUFS")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.loop,onChange:e=>setExportSettings(p=>({...p,loop:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Xuất loop (không fade in / không fade out)")),exportSettings.loop&&/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1 ml-auto"},/*#__PURE__*/React.createElement("span",{className:"text-[7px] text-zinc-500 font-bold uppercase"},"Số ô nhịp"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",max:"256",value:exportSettings.loopBars,onChange:e=>setExportSettings(p=>({...p,loopBars:Math.max(1,parseInt(e.target.value)||1)})),className:"w-16 bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Title (BWF/INFO)"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaTitle,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaTitle:e.target.value})),placeholder:"Tên bài",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Artist"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaArtist,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaArtist:e.target.value})),placeholder:"Nghệ sĩ",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"ISRC"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaIsrc,maxLength:"12",onChange:e=>setExportSettings(p=>({...p,metaIsrc:e.target.value.toUpperCase()})),placeholder:"US-ABC-26-00001",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.realtime,onChange:e=>setExportSettings(p=>({...p,realtime:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Realtime (bounce — chạy lại project thật, giữ nguyên MIDI + FX Rack + Mastering Chain)")))),/*#__PURE__*/React.createElement("div",{className:"p-4 pt-3 border-t border-slate-700 space-y-2"},isExporting&&/*#__PURE__*/React.createElement("div",{className:"bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("div",{className:"h-1.5 bg-zinc-800 rounded-full overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-full bg-cyan-500 transition-all duration-200",style:{width:(exportProgress>=0?exportProgress:25)+'%'}})),/*#__PURE__*/React.createElement("div",{className:"text-[8px] text-zinc-500 mt-1"},exportProgress>=0?'Đang xuất... '+Math.round(exportProgress)+'%':'Đang xử lý...')),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("button",{onClick:onExport,disabled:isExporting,className:"w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3 h-3"})," ",isExporting?'...':'Export'),/*#__PURE__*/React.createElement("button",{onClick:onClose,disabled:isExporting,className:"w-full py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"Hủy")))));};// --- Embedded VST GUI (PLAN_MASTERBUS_VST_GUI_EMBED.md) -------------------- +return{hx:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientWidth:460)-268,st.current.hudPos.hx)),hy:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientHeight:240)-225,st.current.hudPos.hy))};}if(!sel)return null;const nx=eqproFreqToX(sel.freq,cvW),ny=eqproGainToY(sel.gain,cvH);let hx=nx-130,hy=ny-195;if(hx<8)hx=8;if(hx>cvW-268)hx=cvW-268;if(hy<8)hy=ny+24;return{hx,hy};})();return/*#__PURE__*/React.createElement("div",{className:"space-y-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[11px] font-mono"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Bands: ",/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold"},st.current.bands.length),"/8"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Amount:"),/*#__PURE__*/React.createElement("input",{type:"range",min:0,max:200,value:st.current.amount,onChange:e=>{st.current.amount=parseInt(e.target.value);applyAll(mm=>mm.setAmount(st.current.amount));commit();setTick(t=>t+1);},className:"w-20 h-1 cursor-pointer",style:{accentColor:'#2dd4bf'}}),/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold w-10"},st.current.amount,"%"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;s.bands=JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-slate-800 hover:bg-red-900/60 text-slate-300 border border-slate-700 rounded text-[10px]"},"Reset"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;if(s.bands.length>=EQPRO_MAX_BANDS)return;s.bands.push({type:'peaking',freq:1000,gain:0,q:1.0,active:true});s.selected=s.bands.length-1;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-teal-900/50 hover:bg-teal-800 text-teal-300 border border-teal-700 rounded text-[10px]"},"+ Band"))),/*#__PURE__*/React.createElement("div",{className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-60 block cursor-crosshair rounded-lg border border-slate-800 bg-slate-950",onPointerDown:onDown,onPointerMove:onMove,onPointerUp:onUp,onWheel:onWheel,onDoubleClick:onDblClick}),sel&&hudXY&&/*#__PURE__*/React.createElement("div",{className:"absolute z-10 w-64 rounded-xl p-3 text-[11px] font-mono space-y-2 pointer-events-auto cursor-move",style:{left:hudXY.hx,top:hudXY.hy,background:'rgba(15,23,42,0.94)',border:'1px solid rgba(45,212,191,0.3)',boxShadow:'0 10px 30px rgba(0,0,0,0.8)',backdropFilter:'blur(12px)'},onPointerDown:e=>{const s=st.current;s.dragHud=true;const r=e.currentTarget.getBoundingClientRect();s.hudOffsetX=e.clientX-r.left;s.hudOffsetY=e.clientY-r.top;e.currentTarget.setPointerCapture(e.pointerId);},onPointerMove:e=>{const s=st.current;if(!s.dragHud)return;const cv=canvasRef.current;const w=cv?cv.clientWidth:460;s.hudPos={hx:Math.max(0,Math.min(w-268,e.clientX-s.hudOffsetX-(cv?cv.getBoundingClientRect().left:0))),hy:Math.max(0,e.clientY-s.hudOffsetY-(cv?cv.getBoundingClientRect().top:0))};setTick(t=>t+1);},onPointerUp:e=>{st.current.dragHud=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-700/80 pb-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"w-4 h-4 rounded-full text-slate-950 font-bold flex items-center justify-center text-[10px]",style:{background:hudColor.badge}},selIdx+1),/*#__PURE__*/React.createElement("span",{className:"font-bold text-white uppercase tracking-wider"},"Band ",selIdx+1)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{title:"Bypass band",onClick:()=>{const s=st.current;s.bands[s.selected].active=!s.bands[s.selected].active;applyAll(mm=>mm.setBand(s.selected,{active:s.bands[s.selected].active}));commit();setTick(t=>t+1);},className:`w-5 h-5 rounded flex items-center justify-center text-[10px] ${sel.active?'bg-slate-800 text-slate-400':'bg-amber-600 text-white'}`},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-power-off"})),/*#__PURE__*/React.createElement("button",{title:"Delete band",onClick:()=>{const s=st.current;s.bands.splice(s.selected,1);s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"w-5 h-5 rounded bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white flex items-center justify-center text-[10px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"})))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[9px] text-slate-400 uppercase tracking-widest mb-1"},"Filter Shape"),/*#__PURE__*/React.createElement("select",{value:sel.type,onChange:ev=>{const s=st.current;s.bands[s.selected].type=ev.target.value;applyAll(mm=>mm.setBand(s.selected,{type:ev.target.value}));commit();setTick(t=>t+1);},className:"w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none"},['peaking','highpass','lowpass','lowshelf','highshelf','notch','bandpass'].map(t=>/*#__PURE__*/React.createElement("option",{key:t,value:t},t==='peaking'?'Bell / Peaking':t==='highpass'?'Low Cut / High Pass':t==='lowpass'?'High Cut / Low Pass':t==='lowshelf'?'Low Shelf':t==='highshelf'?'High Shelf':t==='notch'?'Notch / Band Stop':'Band Pass')))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 text-center bg-slate-950/80 p-2 rounded-lg border border-slate-800"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"FREQ"),/*#__PURE__*/React.createElement("div",{className:"text-teal-300 font-bold text-[11px]"},sel.freq>=1000?(sel.freq/1000).toFixed(2)+' kHz':Math.round(sel.freq)+' Hz')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"GAIN"),/*#__PURE__*/React.createElement("div",{className:"text-amber-400 font-bold text-[11px]"},eqproBandHasGain(sel.type)?(sel.gain>0?'+':'')+sel.gain.toFixed(1)+' dB':'0.0 dB')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"Q"),/*#__PURE__*/React.createElement("div",{className:"text-purple-400 font-bold text-[11px]"},sel.q.toFixed(1)))),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 text-center"},"Drag center: Freq & Gain · Wings/Wheel: Q · Dbl-click: add/delete"))));};const ExportModal=({open,onClose,exportSettings,setExportSettings,isExporting,exportProgress,onExport})=>{if(!open)return null;const pickDir=async()=>{try{const r=await fetch(`${API_BASE_URL}/api/v1/plugins/pick-dir`,{method:'POST'});if(r.ok){const d=await r.json();if(d&&d.path)setExportSettings(p=>({...p,exportDir:d.path}));}}catch(e){}};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3.5 h-3.5"})," EXPORT"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"p-4 max-h-[72vh] overflow-y-auto space-y-3"},/*#__PURE__*/React.createElement("div",{className:"bg-[#141414] border border-zinc-800 rounded px-2 py-1.5 space-y-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Tên file export"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.fileName,onChange:e=>setExportSettings(p=>({...p,fileName:e.target.value})),placeholder:"Để trống = dùng tên mặc định",className:"w-full bg-[#0d0d0d] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-end gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex-1 min-w-0"},/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Thư mục export"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 bg-[#0d0d0d] border border-zinc-800 rounded px-1.5 py-0.5 min-h-[22px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-cyan-400 text-[10px] shrink-0"}),exportSettings.exportDir?/*#__PURE__*/React.createElement("a",{className:"text-cyan-400 underline truncate cursor-pointer",title:exportSettings.exportDir,onClick:pickDir},exportSettings.exportDir):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 truncate"},"Chưa chọn — xuất sẽ mở Save-As"))),/*#__PURE__*/React.createElement("button",{onClick:pickDir,className:"shrink-0 px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-[10px] font-bold flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[10px]"})," Chọn thư mục")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.rememberExportDir,onChange:e=>setExportSettings(p=>({...p,rememberExportDir:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Lưu thư mục export làm mặc định"))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Nguồn"),/*#__PURE__*/React.createElement("select",{value:exportSettings.source,onChange:e=>setExportSettings(p=>({...p,source:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"project"},"Project (Mix)"),/*#__PURE__*/React.createElement("option",{value:"track_mix"},"Track Selection"),/*#__PURE__*/React.createElement("option",{value:"active_clip"},"Active Clip"),/*#__PURE__*/React.createElement("option",{value:"clip_selection"},"Clip Selection"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Định dạng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.format,onChange:e=>setExportSettings(p=>({...p,format:e.target.value,sampleRate:'44100',bitDepth:'float',quality:'44khz'})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"wav"},"WAV"),/*#__PURE__*/React.createElement("option",{value:"mp3"},"MP3"),/*#__PURE__*/React.createElement("option",{value:"ogg"},"OGG")))),exportSettings.format==='wav'?/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"SR (Hz)"),/*#__PURE__*/React.createElement("select",{value:exportSettings.sampleRate,onChange:e=>setExportSettings(p=>({...p,sampleRate:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44100"},"44100"),/*#__PURE__*/React.createElement("option",{value:"48000"},"48000"),/*#__PURE__*/React.createElement("option",{value:"96000"},"96000"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Bit"),/*#__PURE__*/React.createElement("select",{value:exportSettings.bitDepth,onChange:e=>setExportSettings(p=>({...p,bitDepth:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"16"},"16"),/*#__PURE__*/React.createElement("option",{value:"24"},"24"),/*#__PURE__*/React.createElement("option",{value:"float"},"32 float"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Kênh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))):/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Chất lượng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.quality,onChange:e=>setExportSettings(p=>({...p,quality:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44khz"},"44kHz"),/*#__PURE__*/React.createElement("option",{value:"lossless"},"Lossless"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Kênh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.dither,onChange:e=>setExportSettings(p=>({...p,dither:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Dither TPDF (PCM 16/24)")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.normalize,onChange:e=>setExportSettings(p=>({...p,normalize:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Normalize")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.monoCheck,onChange:e=>setExportSettings(p=>({...p,monoCheck:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Cảnh báo mono (side/mid)")),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Normalize target"),/*#__PURE__*/React.createElement("select",{value:exportSettings.normalizeTarget,onChange:e=>setExportSettings(p=>({...p,normalizeTarget:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"-1dbtp"},"-1 dBTP"),/*#__PURE__*/React.createElement("option",{value:"-14lufs"},"-14 LUFS")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.loop,onChange:e=>setExportSettings(p=>({...p,loop:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Xuất loop (không fade in / không fade out)")),exportSettings.loop&&/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1 ml-auto"},/*#__PURE__*/React.createElement("span",{className:"text-[7px] text-zinc-500 font-bold uppercase"},"Số ô nhịp"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",max:"256",value:exportSettings.loopBars,onChange:e=>setExportSettings(p=>({...p,loopBars:Math.max(1,parseInt(e.target.value)||1)})),className:"w-16 bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Title (BWF/INFO)"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaTitle,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaTitle:e.target.value})),placeholder:"Tên bài",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Artist"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaArtist,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaArtist:e.target.value})),placeholder:"Nghệ sĩ",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"ISRC"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaIsrc,maxLength:"12",onChange:e=>setExportSettings(p=>({...p,metaIsrc:e.target.value.toUpperCase()})),placeholder:"US-ABC-26-00001",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.realtime,onChange:e=>setExportSettings(p=>({...p,realtime:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Realtime (bounce — chạy lại project thật, giữ nguyên MIDI + FX Rack + Mastering Chain)")))),/*#__PURE__*/React.createElement("div",{className:"p-4 pt-3 border-t border-slate-700 space-y-2"},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("button",{onClick:onExport,disabled:isExporting,className:"w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3 h-3"})," ",isExporting?'...':'Export'),/*#__PURE__*/React.createElement("button",{onClick:onClose,disabled:isExporting,className:"w-full py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"Hủy")))));};// ── Export progress modal (RIÊNG, không dùng statusbar — vị trí đó cho tools) ── +// Settings modal đóng ngay khi bấm Export; modal này hiện tiến trình render + +// encode + ghi file, tự đóng khi xong (success/error qua toast). +const ExportProgressModal=({open,exportProgress,isExporting,onClose})=>{if(!open)return null;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[310] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6"},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-md bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"w-3 h-3 border-2 border-cyan-300 border-t-transparent rounded-full animate-spin"}),"ĐANG XUẤT ÂM THANH")),/*#__PURE__*/React.createElement("div",{className:"p-5 space-y-3"},/*#__PURE__*/React.createElement("div",{className:"h-2.5 bg-zinc-800 rounded-full overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-full bg-cyan-500 transition-all duration-200",style:{width:(exportProgress>=0?exportProgress:25)+'%'}})),/*#__PURE__*/React.createElement("div",{className:"text-xs text-zinc-400 text-center"},exportProgress>=0?'Đang xuất... '+Math.round(exportProgress)+'%':'Đang xử lý...'),!isExporting&&/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-full py-1.5 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"Đóng"))));};// --- Embedded VST GUI (PLAN_MASTERBUS_VST_GUI_EMBED.md) -------------------- // Frame-capture + input relay: hiển thị native GUI qua HTTP /frame (~10fps), // gửi pointer/key về bridge qua /input. Dùng chung MasteringModal + FXRackModal. // url = embed_url từ API fx-gui open (embed:true); onClose khi user đóng; @@ -1170,7 +1173,8 @@ const cc=msg.data[1];const val=msg.data[2];if(window.SonicSF&&window.SonicSF.con const lsb=msg.data[1];const msb=msg.data[2];const bendVal=msb<<7|lsb;if(window.SonicSF&&window.SonicSF.pitchBend){var pbTracks=activeTracksRef.current||[];var hasArmedPB=pbTracks.some(function(t){return t.isArmed;});if(hasArmedPB){pbTracks.forEach(function(pt){if(!pt.isArmed)return;var ptCh=assignTrackMidiChannel(pt,pbTracks);window.SonicSF.pitchBend(ptCh,bendVal);});}else{window.SonicSF.pitchBend(midiCh,bendVal);}}}// Forward to active MIDI recorders if(activeMIDIRecordersRef.current){for(let trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){rec.handleMIDIMessage(msg,input.id);}}}};}for(let input of access.inputs.values()){inputs.push(input);attachMidiHandler(input);}setMidiDevices(inputs);access.onstatechange=()=>{const inputs=[];for(let input of access.inputs.values()){inputs.push(input);// Re-attach handler to ensure new devices get it if(!input.onmidimessage){attachMidiHandler(input);}}setMidiDevices(inputs);};}).catch(err=>console.log('MIDI access error:',err));}},[]);const[showAIConfig,setShowAIConfig]=useState(false);const[recordingState,setRecordingState]=useState('IDLE');// 'IDLE' | 'COUNT_IN' | 'RECORDING' -const[recTempMidiNotes,setRecTempMidiNotes]=useState([]);const[recTempAudioBuffer,setRecTempAudioBuffer]=useState(null);const[recStartTimelineTime,setRecStartTimelineTime]=useState(0);const[canvasRedrawCount,setCanvasRedrawCount]=useState(0);const[lastMidiNote,setLastMidiNote]=useState(null);const lastMidiNoteRef=useRef(null);const activeMIDIRecordersRef=useRef({});const activeAudioRecordersRef=useRef({});const pianoRollRecorderRef=useRef(null);const activeMidiPitchesRef=useRef(new Set());const[activeMidiPitches,setActiveMidiPitches]=useState(new Set());const recordingPCMDataRef=useRef({});const recordingStartTimeRef=useRef(0);const recordingSyncRef=useRef(null);const recordingStateRef=useRef(recordingState);recordingStateRef.current=recordingState;const lastTempCompileTimeRef=useRef(0);const nextMetronomeBeatRef=useRef(0);const[showExportPanel,setShowExportPanel]=useState(false);React.useEffect(()=>{if(showExportPanel)setTimeout(()=>{try{window.lucide&&window.lucide.createIcons();}catch(e){}},80);},[showExportPanel]);// ── Virtual MIDI Keyboard (Phase 5) ───────────────────────────────────── +const[recTempMidiNotes,setRecTempMidiNotes]=useState([]);const[recTempAudioBuffer,setRecTempAudioBuffer]=useState(null);const[recStartTimelineTime,setRecStartTimelineTime]=useState(0);const[canvasRedrawCount,setCanvasRedrawCount]=useState(0);const[lastMidiNote,setLastMidiNote]=useState(null);const lastMidiNoteRef=useRef(null);const activeMIDIRecordersRef=useRef({});const activeAudioRecordersRef=useRef({});const pianoRollRecorderRef=useRef(null);const activeMidiPitchesRef=useRef(new Set());const[activeMidiPitches,setActiveMidiPitches]=useState(new Set());const recordingPCMDataRef=useRef({});const recordingStartTimeRef=useRef(0);const recordingSyncRef=useRef(null);const recordingStateRef=useRef(recordingState);recordingStateRef.current=recordingState;const lastTempCompileTimeRef=useRef(0);const nextMetronomeBeatRef=useRef(0);const[showExportPanel,setShowExportPanel]=useState(false);const[showExportProgress,setShowExportProgress]=useState(false);// modal tiến trình export RIÊNG +React.useEffect(()=>{if(showExportPanel)setTimeout(()=>{try{window.lucide&&window.lucide.createIcons();}catch(e){}},80);},[showExportPanel]);// ── Virtual MIDI Keyboard (Phase 5) ───────────────────────────────────── const[vkbOpen,setVkbOpen]=useState(false);const vkbOpenRef=useRef(false);vkbOpenRef.current=vkbOpen;const[chordsPanelOpen,setChordsPanelOpen]=useState(false);const vkbHeldRef=useRef(new Set());const[vkbSettings,setVkbSettings]=useState(function(){try{const saved=JSON.parse(localStorage.getItem('sonic_vkb_settings_v1')||'null');if(saved&&typeof saved==='object')return Object.assign({x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'},saved);}catch(e){}return{x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'};}());const vkbSettingsRef=useRef(vkbSettings);vkbSettingsRef.current=vkbSettings;const vkbNoteOnRef=useRef(null);const vkbNoteOffRef=useRef(null);const vkbUpdateSettings=patch=>{setVkbSettings(prev=>{const next=Object.assign({},prev,patch);try{localStorage.setItem('sonic_vkb_settings_v1',JSON.stringify(next));}catch(e){}return next;});};// Global keydown: F2 toggle + QWERTY notes khi VKB mở. capture:true + đăng ký // TRƯỚC handler transport (~19402) → stopPropagation chặn phím tắt trùng // (s=split, v/g sub-tab) khi VKB đang mở; KHÔNG chặn Space/R. Note KHÔNG bắn @@ -2118,11 +2122,11 @@ const allT=activeTracksRef.current&&activeTracksRef.current.length?activeTracksR let loopStart=0,loopDur=0;if(exportSettings.loop){const loopSecPerBar=60.0/(parseFloat(bpm)||120)*4;loopStart=selectionStart!==null&&selectionEnd!==null&&selectionEnd>selectionStart?selectionStart:currentTime;loopDur=Math.max(0.1,(exportSettings.loopBars||4)*loopSecPerBar);const lctx=getAudioContext();exportTracks=exportTracks.map(t=>{if(!t.buffer)return null;const ls=Math.max(0,Math.floor(loopStart*lctx.sampleRate));const le=Math.min(t.buffer.length,Math.ceil((loopStart+loopDur)*lctx.sampleRate));if(le<=ls)return null;const buf=lctx.createBuffer(t.buffer.numberOfChannels,le-ls,t.buffer.sampleRate);for(let ch=0;chserverFileIdMap[t.id]);// Loop đã cắt buffer phía client → không đi server path (server chỉ có file gốc) if(!exportSettings.loop&&allOnServer&&serverStatus==='connected'){// Use server-side export -setIsExporting(true);setExportProgress(-1);showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...","info");try{const sessionId=`session_${Date.now()}`;const tracksMeta=exportTracks.map(t=>({track_id:t.id,file_id:serverFileIdMap[t.id],volume_db:t.volumeDb,muted:false,clips:[{clip_id:`clip_${t.id}`,start_time_seconds:t.startTime||0,end_time_seconds:(t.startTime||0)+t.buffer.duration,loop_count:1,apply_zero_crossing:true,fade_in_ms:0,fade_out_ms:0}]}));const resp=await fetch(`${API_MULTITRACK}/mix`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:sessionId,export_settings:{sample_rate:parseInt(exportSettings.sampleRate),bit_depth:parseInt(exportSettings.bitDepth),format:exportSettings.format,channels:exportSettings.channels},tracks:tracksMeta})});if(!resp.ok)throw new Error(`Server export failed: ${resp.status}`);const data=await resp.json();showToast("Đang xử lý trên máy chủ...","info");// Poll for result +setIsExporting(true);setShowExportPanel(false);setShowExportProgress(true);setExportProgress(-1);showToast("Đang gửi yêu cầu xuất âm thanh đến máy chủ...","info");try{const sessionId=`session_${Date.now()}`;const tracksMeta=exportTracks.map(t=>({track_id:t.id,file_id:serverFileIdMap[t.id],volume_db:t.volumeDb,muted:false,clips:[{clip_id:`clip_${t.id}`,start_time_seconds:t.startTime||0,end_time_seconds:(t.startTime||0)+t.buffer.duration,loop_count:1,apply_zero_crossing:true,fade_in_ms:0,fade_out_ms:0}]}));const resp=await fetch(`${API_MULTITRACK}/mix`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({session_id:sessionId,export_settings:{sample_rate:parseInt(exportSettings.sampleRate),bit_depth:parseInt(exportSettings.bitDepth),format:exportSettings.format,channels:exportSettings.channels},tracks:tracksMeta})});if(!resp.ok)throw new Error(`Server export failed: ${resp.status}`);const data=await resp.json();showToast("Đang xử lý trên máy chủ...","info");// Poll for result const result=await pollTaskResult(data.task_id,20);if(result.success){// Get the uploaded file from server const fileId=result.output_file_id||result.output_path?.split('/').pop();if(fileId){const downloadUrl=`${API_AUDIO}/download/${fileId}`;showToast("Xuất bản âm thanh từ máy chủ hoàn tất!","success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog((projectName||'Project')+'.wav',b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=fileId;a.click();}});// Export server hoàn tất → tự đóng modal (toast Tải về vẫn hiện). setShowExportPanel(false);}}else{throw new Error(result.error||'Server processing failed');}}catch(err){showToast("Lỗi xuất máy chủ: "+err.message+". Chuyển sang xuất client.","warning");// Fall back to client-side export -clientSideExport(exportTracks);}finally{setIsExporting(false);}}else{// Client-side export (existing working code) +clientSideExport(exportTracks);}finally{setIsExporting(false);setShowExportProgress(false);}}else{// Client-side export (existing working code) clientSideExport(exportTracks,{loopStart,loopDur});}};// ── D4: Mastering render qua engine (D3 export stage) ── // /api/v1/plugins/render — project schema + mastering chain qua native_bridge // (VST3 masterbus), resample/normalize/dither PCM, mono side/mid check. @@ -2131,7 +2135,7 @@ const renderMasteringExport=async()=>{if(isExporting)return;if(serverStatus!=='c // trong OfflineAudioContext. Vì vậy bounce REALTIME: chạy lại toàn bộ project // (audio + MIDI + sections qua FX racks + mastering chain, đúng như nghe) và // capture PCM từ master bus, sau đó encode WAV trung thực. -const triggerBounceExport=async()=>{if(!masterBus){showToast("Hãy phát thử một lần để khởi tạo audio engine trước.","warning");return;}if(isExporting)return;setIsExporting(true);const audioCtx=getAudioContext();try{const allTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;// MAIN SESSION end time (items trên track main — sections tính theo bounds, +const triggerBounceExport=async()=>{if(!masterBus){showToast("Hãy phát thử một lần để khởi tạo audio engine trước.","warning");return;}if(isExporting)return;setIsExporting(true);setShowExportPanel(false);setShowExportProgress(true);const audioCtx=getAudioContext();try{const allTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracks;// MAIN SESSION end time (items trên track main — sections tính theo bounds, // không kéo dài theo nội dung SECTION-TAB) — bounce không cắt sớm/cắt thiếu. // LOOP export: độ dài = loopBars*secondsPerBar, bắt đầu từ selection/playhead, // cắt cứng biên — không fade, không kéo tail. @@ -2146,7 +2150,7 @@ await new Promise(res=>setTimeout(res,Math.ceil(durationLimit*1000)+900));stopAl const totalFrames=chunks.reduce((a,c)=>a+c.length/2,0);let stereoBuf=new Float32Array(totalFrames*2);let off=0;chunks.forEach(c=>{stereoBuf.set(c,off);off+=c.length;});// LOOP export: cắt cứng [loopStart, loopStart+loopDur) — bỏ phần trước + phần dư tail if(loopDur>0){const sf=Math.max(0,Math.floor(loopStart*captureRate))*2;const ef=Math.min(stereoBuf.length,Math.floor((loopStart+loopDur)*captureRate)*2);if(ef>sf)stereoBuf=stereoBuf.subarray(sf,ef);}const frames=stereoBuf.length/2;const bytesPerSample=isFloatBit?4:bitDepth/8;const headerSize=44;const fileSizeBytes=headerSize+frames*bytesPerSample*outChannels;const view=new DataView(new ArrayBuffer(fileSizeBytes));const writeString=(o,s)=>{for(let i=0;i>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{setIsExporting(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ +const s=Math.max(-1,Math.min(1,tpdfDither(raw)));if(bitDepth===24){const v=Math.floor(s<0?s*0x800000:s*0x7FFFFF)&0xFFFFFF;view.setUint8(o,v&0xFF);view.setUint8(o+1,v>>8&0xFF);view.setUint8(o+2,v>>16&0xFF);}else if(bitDepth===16)view.setInt16(o,Math.floor(s<0?s*0x8000:s*0x7FFF),true);else view.setUint8(o,Math.floor((s+1)*127.5),true);}o+=bytesPerSample;}}const blob=new Blob([view],{type:'audio/wav'});await downloadBlobViaServer(blob,`sonicforge-bounce-${captureRate}hz-${bitDepth}bit.wav`).catch(()=>{});showToast("Bounce hoàn tất — file gồm MIDI + FX Rack + Mastering Chain!","success");}catch(err){showToast("Lỗi bounce: "+err.message,"error");}finally{setIsExporting(false);setShowExportProgress(false);}};const handleSaveLocalProject=name=>{const finalName=name||projectName||'Dự án mới';const localId=currentProjectId&¤tProjectId.startsWith('local_')?currentProjectId:'local_'+Date.now();const projectSchemaObj=serializeProjectToSchema(localId,finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataStr=JSON.stringify(projectSchemaObj);localStorage.setItem('sonic_local_project_data',dataStr);localStorage.setItem('sonic_project_id',localId);localStorage.setItem('sonic_project_name',finalName);setCurrentProjectId(localId);setProjectName(finalName);window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast(`Đã lưu dự án local "${finalName}" thành công!`,"success");// Đã đăng nhập → đồng bộ lên Cloud (fire-and-forget): project mở được từ // máy khác / browser ẩn danh khi đăng nhập cùng tài khoản. if(currentUser&&window.SonicAPI&&window.SonicAPI.saveCloudProject){window.SonicAPI.saveCloudProject(finalName,dataStr).then(function(res){if(res&&res.project_id){console.log('[Cloud] local project synced:',res.project_id);}}).catch(function(e){console.warn('Cloud sync local project failed:',e);});}};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,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});// 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;});var serItems=0;if(projectSchemaObj.main_session&&projectSchemaObj.main_session.tracks){projectSchemaObj.main_session.tracks.forEach(function(st){if(st.items)serItems+=st.items.length;});}Object.keys(projectSchemaObj.section_store||{}).forEach(function(sk){(projectSchemaObj.section_store[sk].tracks||[]).forEach(function(st){if(st.items)serItems+=st.items.length;});});const dataJson=JSON.stringify(projectSchemaObj);try{if(existingProjectId){await window.SonicAPI.updateCloudProject(existingProjectId,finalName,dataJson);setProjectName(finalName);setCurrentProjectId(existingProjectId);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',existingProjectId);showToast('Đã ghi đè Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').','success');}else if(currentProjectId&&!currentProjectId.startsWith('local_')){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);localStorage.setItem('sonic_project_id',currentProjectId);showToast('Đã lưu Cloud "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+' id='+currentProjectId+').',serItems===0?'warning':'success');}else{const res=await window.SonicAPI.saveCloudProject(finalName,dataJson);const newProjId=res.project_id;setProjectName(finalName);localStorage.setItem('sonic_project_name',finalName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast('Đã lưu Cloud mới "'+finalName+'" (src midi='+srcMidi+' clip='+srcClip+' sec='+srcSec+' | ser='+serItems+').',serItems===0?'warning':'success');}}catch(err){showToast(err.message||"Lỗi lưu dự án lên Cloud","error");}};const handleSaveProject=async()=>{if(!currentProjectId){setSaveProjectModalOpen(true);return;}if(currentProjectId.startsWith('local_')){handleSaveLocalProject(projectName);}else{if(!currentUser){showToast('Cần đăng nhập để lưu Cloud. currentUser='+(currentUser?'OK':'NULL')+' token='+(localStorage.getItem('sonic_token')?'exists':'missing'),'warning');return;}handleSaveCloudProject(projectName);}};const handleSaveProjectRef=useRef(handleSaveProject);handleSaveProjectRef.current=handleSaveProject;// ── Ctrl-S (Save) toàn cục ── @@ -2154,7 +2158,7 @@ var srcMidi=0,srcClip=0,srcSec=0;tracks.forEach(function(t){srcMidi+=(t.midiItem // Projects). Docker/headless: lưu lên Cloud (đã login) hoặc local (.sfs). const handleGlobalSave=async()=>{try{const finalName=projectName||'Dự án mới';const schemaObj=serializeProjectToSchema(currentProjectId||'proj_'+Date.now(),finalName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataJson=JSON.stringify(schemaObj);const runtime=window.SonicRuntime&&window.SonicRuntime.capabilities&&window.SonicRuntime.capabilities.runtime||'';if(runtime==='desktop'&&window.SonicAPI&&window.SonicAPI.saveProjectToDisk){// Desktop → thư mục của hệ điều hành const res=await window.SonicAPI.saveProjectToDisk(finalName,dataJson);showToast('Đã lưu dự án: '+(res&&res.path||finalName),'success');}else if(window.SonicAPI){// Docker / headless → Cloud (nếu đã đăng nhập), ngược lại local -if(currentProjectId&&!currentProjectId.startsWith('local_')&¤tUser){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);showToast('Đã lưu dự án lên Cloud','success');}else if(currentUser){await handleSaveAsCloud(finalName);}else{handleSaveLocalProject(finalName);}}}catch(err){showToast('Lỗi lưu dự án: '+(err.message||err),'error');}};const handleGlobalSaveRef=useRef(handleGlobalSave);handleGlobalSaveRef.current=handleGlobalSave;useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();handleGlobalSaveRef.current();}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[]);const handleSaveAsCloud=async newName=>{const projectSchemaObj=serializeProjectToSchema('project_'+Date.now(),newName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataJson=JSON.stringify(projectSchemaObj);try{const res=await window.SonicAPI.saveCloudProject(newName,dataJson);const newProjId=res.project_id;setProjectName(newName);localStorage.setItem('sonic_project_name',newName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast(`Đã lưu dự án dưới tên mới "${newName}" lên server!`,"success");}catch(err){showToast(err.message||"Lỗi Save As lên server","error");}};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,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast("Đã xuất dự án (.sfs) thành công!","success");};const handleImportSFS=()=>{const input=document.createElement('input');input.type='file';input.accept='.sfs,application/json';input.onchange=async e=>{if(!e.target.files[0])return;try{const proj=await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);let restored=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(proj.main_session){const result=deserializeProjectFromSchema(proj);restored=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.masteringSettings)setMasteringSettings(result.masteringSettings);if(result.masterVolume!==undefined&&result.masterVolume!==null)setMasterVolume(result.masterVolume);if(result.masterVolL!==undefined&&result.masterVolL!==null)setMasterVolL(result.masterVolL);if(result.masterVolR!==undefined&&result.masterVolR!==null)setMasterVolR(result.masterVolR);if(result.masterLink!==undefined&&result.masterLink!==null)setMasterLink(result.masterLink);if(result.masterMuted!==undefined&&result.masterMuted!==null)setMasterMuted(result.masterMuted);if(result.masterAutoVol!==undefined)setMasterAutoVol(result.masterAutoVol);if(result.masterAutoOn!==undefined)setMasterAutoOn(result.masterAutoOn);}else{restored=(proj.tracks||[]).map(t=>({...t,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null}));}if(restored.length>0){setTracks(restored);loadAudioBuffersForTracks(restored);setBpm(restoredBpm.toString());setProjectName(proj.name||'Dự án mới');setCurrentProjectId(null);if(restoredSessionTabs.length>0)setSessionTabs(restoredSessionTabs);if(restoredSubTabs.length>0)setSubTabs(restoredSubTabs);localStorage.setItem('sonic_project_name',proj.name||'Dự án mới');localStorage.removeItem('sonic_project_id');showToast(`Đã nạp dự án "${proj.name||'Dự án mới'}" từ tệp .sfs thành công!`,"success");}}catch(err){showToast(err.message||"Lỗi mở tệp .sfs","error");}};input.click();};const clientSideExport=async(activeTracks,loopInfo)=>{setIsExporting(true);setExportProgress(5);showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...","info");// Yield 1 frame → React vẽ thanh "Đang xử lý..." trước khi graph build + +if(currentProjectId&&!currentProjectId.startsWith('local_')&¤tUser){await window.SonicAPI.updateCloudProject(currentProjectId,finalName,dataJson);showToast('Đã lưu dự án lên Cloud','success');}else if(currentUser){await handleSaveAsCloud(finalName);}else{handleSaveLocalProject(finalName);}}}catch(err){showToast('Lỗi lưu dự án: '+(err.message||err),'error');}};const handleGlobalSaveRef=useRef(handleGlobalSave);handleGlobalSaveRef.current=handleGlobalSave;useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key.toLowerCase()==='s'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();handleGlobalSaveRef.current();}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[]);const handleSaveAsCloud=async newName=>{const projectSchemaObj=serializeProjectToSchema('project_'+Date.now(),newName,bpm,tracks,subTabs,sessionTabs,masteringSettings,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});const dataJson=JSON.stringify(projectSchemaObj);try{const res=await window.SonicAPI.saveCloudProject(newName,dataJson);const newProjId=res.project_id;setProjectName(newName);localStorage.setItem('sonic_project_name',newName);if(newProjId){setCurrentProjectId(newProjId);localStorage.setItem('sonic_project_id',newProjId);}showToast(`Đã lưu dự án dưới tên mới "${newName}" lên server!`,"success");}catch(err){showToast(err.message||"Lỗi Save As lên server","error");}};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,masterVolume,{volL:masterVolL,volR:masterVolR,link:masterLink,muted:masterMuted});window.SonicStorage.exportProjectToSFS(projectSchemaObj);showToast("Đã xuất dự án (.sfs) thành công!","success");};const handleImportSFS=()=>{const input=document.createElement('input');input.type='file';input.accept='.sfs,application/json';input.onchange=async e=>{if(!e.target.files[0])return;try{const proj=await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);let restored=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(proj.main_session){const result=deserializeProjectFromSchema(proj);restored=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.masteringSettings)setMasteringSettings(result.masteringSettings);if(result.masterVolume!==undefined&&result.masterVolume!==null)setMasterVolume(result.masterVolume);if(result.masterVolL!==undefined&&result.masterVolL!==null)setMasterVolL(result.masterVolL);if(result.masterVolR!==undefined&&result.masterVolR!==null)setMasterVolR(result.masterVolR);if(result.masterLink!==undefined&&result.masterLink!==null)setMasterLink(result.masterLink);if(result.masterMuted!==undefined&&result.masterMuted!==null)setMasterMuted(result.masterMuted);if(result.masterAutoVol!==undefined)setMasterAutoVol(result.masterAutoVol);if(result.masterAutoOn!==undefined)setMasterAutoOn(result.masterAutoOn);}else{restored=(proj.tracks||[]).map(t=>({...t,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null}));}if(restored.length>0){setTracks(restored);loadAudioBuffersForTracks(restored);setBpm(restoredBpm.toString());setProjectName(proj.name||'Dự án mới');setCurrentProjectId(null);if(restoredSessionTabs.length>0)setSessionTabs(restoredSessionTabs);if(restoredSubTabs.length>0)setSubTabs(restoredSubTabs);localStorage.setItem('sonic_project_name',proj.name||'Dự án mới');localStorage.removeItem('sonic_project_id');showToast(`Đã nạp dự án "${proj.name||'Dự án mới'}" từ tệp .sfs thành công!`,"success");}}catch(err){showToast(err.message||"Lỗi mở tệp .sfs","error");}};input.click();};const clientSideExport=async(activeTracks,loopInfo)=>{setIsExporting(true);setShowExportPanel(false);setShowExportProgress(true);setExportProgress(5);showToast("Đang trộn âm thanh đa kênh (Offline Mixdown)...","info");// Yield 1 frame → React vẽ thanh "Đang xử lý..." trước khi graph build + // OfflineAudioContext render chặn main thread. await new Promise(res=>setTimeout(res,30));let lastPaint=0;// throttle setProgress 150ms — tránh re-render toàn App mỗi chunk try{const targetRate=parseInt(exportSettings.sampleRate);const isFloatWav=exportSettings.bitDepth==='float';const bitDepth=isFloatWav?32:parseInt(exportSettings.bitDepth);// MAIN SESSION end time (items trên track main — sections theo bounds) — @@ -2179,13 +2183,13 @@ let mix=null;let vstTi=0;for(const t of activeTracks){const now=Date.now();if(no // (vòng lặp đồng bộ dài sẽ chặn main thread, bar không bao giờ hiện). const ENC_CHUNK=65536;for(let base=0;basepeak)peak=Math.abs(sample);if(bitDepth===8){view.setUint8(offset,Math.floor((sample+1.0)*127.5));}else if(bitDepth===16){view.setInt16(offset,Math.floor(sample<0?sample*0x8000:sample*0x7FFF),true);}else if(bitDepth===24){const val24=Math.floor(sample<0?sample*0x800000:sample*0x7FFFFF);view.setUint8(offset,val24&0xFF);view.setUint8(offset+1,val24>>8&0xFF);view.setUint8(offset+2,val24>>16&0xFF);}else if(isFloatWav){view.setFloat32(offset,sample,true);}offset+=bytesPerSample;}}// Throttle setState 150ms → tránh re-render toàn App (1.7MB JSX) mỗi // chunk — đó là thứ làm export chậm rõ rệt. -const now=Date.now();if(now-lastPaint>=150){setExportProgress(60+Math.round(encEnd/Math.max(1,exportLength)*32));lastPaint=now;}await new Promise(res=>setTimeout(res,0));}if(peak<0.001){showToast("Cảnh báo: file export gần như CÂM (không có tín hiệu âm) — kiểm tra nguồn track / MIDI cache preview.","warning");}const blob=new Blob([view],{type:'audio/wav'});const wavName=`sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;setExportProgress(95);if((exportSettings.format==='mp3'||exportSettings.format==='ogg')&&serverStatus==='connected'){setIsExporting(true);setExportProgress(-1);showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...","info");try{const file=new File([blob],`session_export.wav`,{type:'audio/wav'});const formData=new FormData();formData.append('file',file);const uploadResp=await fetch(`${API_AUDIO}/upload`,{method:'POST',body:formData});if(!uploadResp.ok)throw new Error("Upload to transcode server failed");const uploadData=await uploadResp.json();const fileId=uploadData.file_id;const exportResp=await fetch(`${API_AUDIO}/export`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:fileId,format:exportSettings.format,sample_rate:targetRate,bit_depth:bitDepth})});if(!exportResp.ok)throw new Error("Transcode request failed");const exportData=await exportResp.json();const result=await pollTaskResult(exportData.task_id,20);if(result.success){const outId=result.output_file_id;const downloadUrl=`${API_AUDIO}/download/${outId}`;showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`,"success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog(`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`,b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;a.click();}});}else{throw new Error(result.error||'Server encoding failed');}}catch(transcodeErr){showToast(`Lỗi chuyển đổi: ${transcodeErr.message}.`,"warning","Tải WAV thay thế",()=>{downloadBlobViaServer(blob,wavName).catch(()=>{});});}}else{if(exportSettings.format!=='wav'){showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.","warning");}const dres=await downloadBlobViaServer(blob,wavName).catch(()=>null);if(dres&&dres.saved_path){showToast(`Xuất bản âm thanh hoàn tất → ${dres.saved_path}`,"success");}else{showToast("Xuất bản âm thanh hoàn tất!","success");}}// Export hoàn tất → tự đóng modal. +const now=Date.now();if(now-lastPaint>=150){setExportProgress(60+Math.round(encEnd/Math.max(1,exportLength)*32));lastPaint=now;}await new Promise(res=>setTimeout(res,0));}if(peak<0.001){showToast("Cảnh báo: file export gần như CÂM (không có tín hiệu âm) — kiểm tra nguồn track / MIDI cache preview.","warning");}const blob=new Blob([view],{type:'audio/wav'});const wavName=`sonicforge-session-${targetRate}hz-${bitDepth}bit.wav`;setExportProgress(95);if((exportSettings.format==='mp3'||exportSettings.format==='ogg')&&serverStatus==='connected'){setIsExporting(true);setExportProgress(-1);showToast("Đang gửi yêu cầu nén định dạng lên máy chủ...","info");try{const file=new File([blob],`session_export.wav`,{type:'audio/wav'});const formData=new FormData();formData.append('file',file);const uploadResp=await fetch(`${API_AUDIO}/upload`,{method:'POST',body:formData});if(!uploadResp.ok)throw new Error("Upload to transcode server failed");const uploadData=await uploadResp.json();const fileId=uploadData.file_id;const exportResp=await fetch(`${API_AUDIO}/export`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:fileId,format:exportSettings.format,sample_rate:targetRate,bit_depth:bitDepth})});if(!exportResp.ok)throw new Error("Transcode request failed");const exportData=await exportResp.json();const result=await pollTaskResult(exportData.task_id,20);if(result.success){const outId=result.output_file_id;const downloadUrl=`${API_AUDIO}/download/${outId}`;showToast(`Xuất bản âm thanh định dạng ${exportSettings.format.toUpperCase()} thành công!`,"success","Tải về",async()=>{try{const br=await fetch(downloadUrl);if(!br.ok)throw new Error('HTTP '+br.status);const b=await br.blob();const sp=await saveExportViaDialog(`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`,b);if(sp)showToast(`Đã lưu → ${sp}`,"success");}catch(e){const a=document.createElement('a');a.href=downloadUrl;a.download=`sonicforge-session-${targetRate}hz-${bitDepth}bit.${exportSettings.format}`;a.click();}});}else{throw new Error(result.error||'Server encoding failed');}}catch(transcodeErr){showToast(`Lỗi chuyển đổi: ${transcodeErr.message}.`,"warning","Tải WAV thay thế",()=>{downloadBlobViaServer(blob,wavName).catch(()=>{});});}}else{if(exportSettings.format!=='wav'){showToast("Đang ngoại tuyến. Tải về định dạng WAV thay thế.","warning");}const dres=await downloadBlobViaServer(blob,wavName).catch(()=>null);if(dres&&dres.saved_path){showToast(`Xuất bản âm thanh hoàn tất → ${dres.saved_path}`,"success");}else{showToast("Không thể ghi file export — engine không lưu được file. Kiểm tra kết nối engine và thư mục export.","error");}}// Export hoàn tất → tự đóng modal. setShowExportPanel(false);}finally{// Restore the LIVE master bus after offline render: initMasterBus(offlineCtx) // ghi đè cả `masterBus` lẫn `window.masterBus` bằng graph OfflineAudioContext // (state closed) → track route (window.masterBus.input) đổ vào context câm → // play/bounce sau export CÂM. Khôi phục bus live (giữ wiring cũ nếu còn sống, // ngược lại rebuild graph trên audioCtx thật) và luôn trả lại window.masterBus. -try{const liveCtx=audioCtx;let restored=savedMasterBus;if(!(restored&&liveCtx&&restored.output&&restored.output.context===liveCtx&&liveCtx.state!=='closed')){masterBus=null;initMasterBus(liveCtx||getAudioContext());restored=masterBus;}masterBus=restored;window.masterBus=restored;}catch(e){masterBus=savedMasterBus;window.masterBus=savedMasterBus;}try{toggleMasteringOnMaster(!!(masteringSettings&&masteringSettings.masterConnected),!!(masteringSettings&&masteringSettings.isBypassed));}catch(e){}Object.keys(offlineNodes).forEach(k=>{try{if(offlineNodes[k].fxStopFn)offlineNodes[k].fxStopFn();}catch(e){}});}}catch(err){showToast("Lỗi xuất âm thanh: "+err.message,"error");}finally{setExportProgress(-1);setIsExporting(false);}};// ── AI Analysis (server-side with fallback) ── +try{const liveCtx=audioCtx;let restored=savedMasterBus;if(!(restored&&liveCtx&&restored.output&&restored.output.context===liveCtx&&liveCtx.state!=='closed')){masterBus=null;initMasterBus(liveCtx||getAudioContext());restored=masterBus;}masterBus=restored;window.masterBus=restored;}catch(e){masterBus=savedMasterBus;window.masterBus=savedMasterBus;}try{toggleMasteringOnMaster(!!(masteringSettings&&masteringSettings.masterConnected),!!(masteringSettings&&masteringSettings.isBypassed));}catch(e){}Object.keys(offlineNodes).forEach(k=>{try{if(offlineNodes[k].fxStopFn)offlineNodes[k].fxStopFn();}catch(e){}});}}catch(err){showToast("Lỗi xuất âm thanh: "+err.message,"error");}finally{setExportProgress(-1);setIsExporting(false);setShowExportProgress(false);}};// ── AI Analysis (server-side with fallback) ── const triggerAIAnalysis=async()=>{setAnalysisState({status:'Connecting to AI Engine...',data:null,isRunning:true});// Check if we have a file on the server to analyze const activeTrack=tracks.find(t=>t.id===selectedTrackId);const serverFileId=serverFileIdMap[selectedTrackId];if(serverFileId&&serverStatus==='connected'){try{const resp=await fetch(`${API_AUDIO}/analyze-ai`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({file_id:serverFileId,api_base_url:aiConfig.baseUrl!==API_BASE_URL?aiConfig.baseUrl:null,model:aiConfig.model})});if(resp.ok){const data=await resp.json();showToast("Đang phân tích cấu trúc trên máy chủ...","info");try{const result=await pollTaskResult(data.task_id,15);if(result.bpm){if(selLeft!==null&&selRight!==null&&activeTrack&&activeTrack.buffer){const snapStart=findZeroCrossing(activeTrack.buffer,selLeft);const snapEnd=findZeroCrossing(activeTrack.buffer,selRight);if(selectionMode==='local'){setLocalSelectionStart(snapStart);setLocalSelectionEnd(snapEnd);}else{setSelectionStart(snapStart);setSelectionEnd(snapEnd);}}setAnalysisState({status:'Hoàn thành phân tích (Server)',data:result,isRunning:false});showToast(`AI Server: ${result.bpm} BPM | ${result.beats?.length||0} beats detected (Zero-Crossing Aligned)`,'success');return;}}catch(e){console.warn('Server AI poll failed, using fallback:',e);}}}catch(err){console.warn('Server AI analysis failed, using fallback:',err);}}// Fallback: client-side analysis with simulated BPM setAnalysisState({status:'Processing structural detection...',data:null,isRunning:true});setTimeout(()=>{let bpm=120;// Try to detect BPM from buffer @@ -2361,7 +2365,7 @@ stopAllPlayback();window.SonicSF.stopAll();stopAllNativeSfNotes();stopAllNativeV const ctx=getAudioContext();const elapsed=ctx.currentTime-startAudioTimeRef.current;const oldSpeed=activePlaybackSpeedRef.current;const ratio=oldSpeed/newSpeed;startBufferOffsetRef.current=startBufferOffsetRef.current+elapsed*oldSpeed;startOffsetTimeRef.current=(startOffsetTimeRef.current+elapsed)*ratio;startAudioTimeRef.current=ctx.currentTime;activePlaybackSpeedRef.current=newSpeed;}}),/*#__PURE__*/React.createElement("div",{onMouseDown:handleSubTabResizeMouseDown,className:"absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize z-30 hover:bg-purple-500/50 transition-colors"})),st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionEnd>st.selectionStart&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 pointer-events-none",style:{left:`${Math.min(st.selectionStart,st.selectionEnd)*zoom}px`,width:`${Math.abs(st.selectionEnd-st.selectionStart)*zoom}px`}})))));})())),/*#__PURE__*/React.createElement("div",{className:"w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",onMouseDown:startColResize}),renderDock('right','Right')),renderDock('bottom','Bottom'),showMixer&&/*#__PURE__*/React.createElement("div",{className:"flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",style:{height:mixerHeight+'px'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",onMouseDown:e=>{e.preventDefault();var startY=e.clientY;var startH=mixerHeight;var onMove=function(ev){var newH=Math.max(80,Math.min(400,startH-(ev.clientY-startY)));setMixerHeight(newH);localStorage.setItem('studio_mixer_height',newH.toString());};var onUp=function(){document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-horizontal",className:"w-3 h-3 text-zinc-600"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-zinc-400 uppercase tracking-wider"},"MIXER")),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowMixer(false),className:"p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"},/*#__PURE__*/React.createElement(MasterStripConsole,{masterVolume:masterVolume,setMasterVolume:setMasterVolume,masterVolL:masterVolL,setMasterVolL:setMasterVolL,masterVolR:masterVolR,setMasterVolR:setMasterVolR,masterLink:masterLink,setMasterLink:setMasterLink,masterMuted:masterMuted,setMasterMuted:setMasterMuted,masterAutoVol:masterAutoVol,setMasterAutoVol:setMasterAutoVol,masterAutoOn:masterAutoOn,setMasterAutoOn:setMasterAutoOn,showMasteringModal:showMasteringModal,setShowMasteringModal:setShowMasteringModal,masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings,isPlaying:isPlaying}),activeTracks.length>0&&/*#__PURE__*/React.createElement("div",{className:"w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"}),activeTracks.map(function(track,idx){return/*#__PURE__*/React.createElement(TrackStripConsole,{key:track.id,track:track,index:idx,onUpdateTrack:updateTrackProp,trackVuRefs:trackVuRefs});}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",style:{height:mediaExplorerPanelHeight+'px',display:showMediaExplorer?'flex':'none'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",onMouseDown:e=>{e.preventDefault();var startY=e.clientY;var startH=mediaExplorerPanelHeight;var onMove=function(ev){var newH=Math.max(120,Math.min(520,startH-(ev.clientY-startY)));setMediaExplorerPanelHeight(newH);localStorage.setItem('studio_media_explorer_height',newH.toString());};var onUp=function(){document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-horizontal",className:"w-3 h-3 text-zinc-600"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-emerald-400 uppercase tracking-wider"},"Media Explorer (F6)")),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowMediaExplorer(false),className:"p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 min-h-0 overflow-hidden bg-[#262626]"},/*#__PURE__*/React.createElement(MediaExplorerPanel,{height:mediaExplorerPanelHeight,clipboardRef:clipboardRef,active:showMediaExplorer}))));})(),/*#__PURE__*/React.createElement("div",{className:"h-9 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-[18px] text-zinc-500 select-none shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4"},/*#__PURE__*/React.createElement("span",null,"Status: ",isPlaying?'Playing':'Stopped'),activeTab!=='main'&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-semibold uppercase"},"Sub-Tab"),activeTab==='main'&&/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-semibold uppercase"},"Track: ID ",selectedTrackId),selectionMode==='local'&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-semibold uppercase text-[18px]"},"Local Sel"),selectionMode==='global'&&/*#__PURE__*/React.createElement("span",{className:"text-purple-400 font-semibold uppercase text-[18px]"},"Global Sel"),hasAnySolo&&/*#__PURE__*/React.createElement("span",{className:"text-amber-500 font-semibold"},"Solo: ",tracks.filter(t=>t.solo).length," track(s)"),isLoopingSelection&&selectionMode==='local'&&/*#__PURE__*/React.createElement("span",{className:"text-emerald-400 font-semibold uppercase text-[18px]"},"Solo Loop"),isLoopingSelection&&selectionMode!=='local'&&/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-semibold uppercase text-[18px]"},"Master Loop")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 ml-3"},/*#__PURE__*/React.createElement("span",{className:"text-[18px] font-bold text-zinc-400 uppercase"},"Bars:"),/*#__PURE__*/React.createElement("input",{type:"number",min:"0",value:beginBar,onChange:e=>{const b=parseInt(e.target.value)||0;setBeginBar(b);const beatDuration=60/parseInt(bpm||120);const t=b*beatDuration*4;clearLocalSelection();setSelectionMode('global');setSelectionStart(t);setSelectionEnd(t+beatDuration*4);},className:"w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"-"),/*#__PURE__*/React.createElement("input",{type:"number",min:"0",value:endBar,onChange:e=>{const b=parseInt(e.target.value)||0;setEndBar(b);const beatDuration=60/parseInt(bpm||120);const t=b*beatDuration*4;// selectionEnd = vạch bar b (không +1 bar) — khớp hiển thị endBar // (quét 8 bars hiển thị "0 đến 8"): nhập 8 → selection tới vạch 8. setSelectionEnd(t);setNumberBar(Math.max(0,b-beginBar));},className:"w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"#"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",value:numberBar,readOnly:true,className:"w-[60px] bg-black text-zinc-400 text-[18px] px-1 py-0 rounded border border-zinc-800 text-center font-mono"}),/*#__PURE__*/React.createElement("button",{onClick:()=>setSelectionFollowsTempo(prev=>!prev),className:`px-1.5 py-0 text-[18px] rounded border font-bold ${selectionFollowsTempo?'bg-amber-700 text-white border-amber-500':'bg-zinc-800 text-zinc-400 border-zinc-700'}`,title:selectionFollowsTempo?"Selection theo tempo (đổi BPM → selection thay đổi)":"Selection theo thời gian (cố định)"},selectionFollowsTempo?"♪T":"⏱T"),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-6 bg-zinc-800 mx-1"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"Start:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selLeft!==null?formatTime(selLeft):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));clearLocalSelection();setSelectionMode('global');setSelectionStart(secs);}},className:"w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"End:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selRight!==null?formatTime(selRight):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));setSelectionEnd(secs);}},className:"w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"Len:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selLeft!==null&&selRight!==null?formatTime(Math.abs(selRight-selLeft)):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3&&selLeft!==null){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));setSelectionEnd(selLeft+secs);}},className:"w-24 bg-black text-amber-300 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>setShowExportPanel(p=>!p),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showExportPanel?'bg-cyan-900 text-cyan-300':'text-zinc-500 hover:text-zinc-300'}`,title:`Export (floating modal)`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"text-[7px] opacity-60"},showExportPanel?panelPositions.export[0].toUpperCase():'')),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowAIPanel(p=>!p),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showAIPanel?'bg-purple-900 text-purple-300':'text-zinc-500 hover:text-zinc-300'}`,title:`AI Panel (${panelPositions.ai})`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"text-[7px] opacity-60"},showAIPanel?panelPositions.ai[0].toUpperCase():'')),/*#__PURE__*/React.createElement("button",{onClick:()=>window.__toggleMixerRef(),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMixer?'bg-indigo-900 text-indigo-300':'text-zinc-500 hover:text-zinc-300'}`,title:"Mixer Panel (F7)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders-horizontal",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>window.__toggleMediaExplorerRef(),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMediaExplorer?'bg-emerald-900 text-emerald-300':'text-zinc-500 hover:text-zinc-300'}`,title:"Media Explorer Panel (F6)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"folder-open",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("span",{className:"w-[1px] h-3 bg-zinc-800 mx-1"}),/*#__PURE__*/React.createElement("span",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"info",className:"w-3 h-3 text-zinc-600"})),prHint?/*#__PURE__*/React.createElement("span",{className:"text-cyan-400"},prHint):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 italic"},"Adaptive tips: hover vào vùng làm việc để xem hướng dẫn")))),contextMenu&&(contextMenu.isSubTab?/*#__PURE__*/React.createElement("div",{className:"fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",style:{left:Math.min(contextMenu.x,window.innerWidth-260),top:contextMenu.y+200>window.innerHeight?undefined:contextMenu.y,bottom:contextMenu.y+200>window.innerHeight?window.innerHeight-contextMenu.y:undefined,maxHeight:'60vh'},onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"px-3 py-1 text-xs text-zinc-500 font-bold uppercase border-b border-zinc-800 pb-1 mb-1"},"Selection: ",formatTime(subTabs.find(s=>s.id===contextMenu.subTabId)?.selectionStart||0)," - ",formatTime(subTabs.find(s=>s.id===contextMenu.subTabId)?.selectionEnd||0)),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabCut(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-rose-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Cut"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+X")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabCopy(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5 text-zinc-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Copy"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+C")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabPaste(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Paste"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+V")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabDelete(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3.5 h-3.5 text-red-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Delete Selected Segment"),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 text-xs font-semibold font-mono ml-auto"},"Del")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabLoop(contextMenu.subTabId,4);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"repeat",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Loop Selection 4 times"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+L"))):/*#__PURE__*/React.createElement("div",{className:"fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",style:{left:Math.min(contextMenu.x,window.innerWidth-260),top:contextMenu.y+260>window.innerHeight?undefined:contextMenu.y,bottom:contextMenu.y+260>window.innerHeight?window.innerHeight-contextMenu.y:undefined,maxHeight:'60vh'},onClick:e=>e.stopPropagation()},contextMenu.sectionId?/*#__PURE__*/React.createElement("button",{onClick:contextMenuEditSection,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-edit",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Edit Section"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+E")):/*#__PURE__*/React.createElement("button",{onClick:contextMenuEdit,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-edit",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Edit"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+E")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuSplit,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Split"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"S")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuMerge,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"combine",className:"w-3.5 h-3.5 text-purple-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Merge"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+M")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("div",{className:"px-3 py-0.5 text-[10px] text-zinc-500 font-bold uppercase tracking-wider"},"Insert"),!sessionTabs.some(s=>s.id===activeTab)&&/*#__PURE__*/React.createElement("button",{onClick:()=>{insertSectionAtPlayhead();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"folder-plus",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Section")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertMidiItemAtPlayhead();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5 text-purple-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert MIDI Item")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertSoundClipAtCursor();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-audio",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Sound Clip")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertTrackBelow();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus-square",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Track")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:contextMenuCopy,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5 text-zinc-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Copy"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+C")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuCut,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-rose-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Cut"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+X")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuPaste,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Paste"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+V")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:contextMenuDelete,className:"w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3.5 h-3.5 text-red-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Delete"),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 text-xs font-semibold font-mono ml-auto pl-8"},"Del")))),toastMessage&&/*#__PURE__*/React.createElement("div",{className:"absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"bell",className:`w-4 h-4 ${toastMessage.type==='success'?'text-emerald-400':toastMessage.type==='error'?'text-rose-400':toastMessage.type==='warning'?'text-amber-400':'text-cyan-400'}`})),toastMessage.text,toastMessage.onActionClick&&/*#__PURE__*/React.createElement("button",{onClick:()=>{toastMessage.onActionClick();setToastMessage(null);},className:"ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"},toastMessage.actionText||'Tải về'))),appWarningModal&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#222222] border border-zinc-800 rounded-lg shadow-2xl p-6 max-w-md w-full text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 text-cyan-400 font-bold"},/*#__PURE__*/React.createElement("h3",{className:"text-base font-bold"},appWarningModal.title)),/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-300 mb-5 leading-relaxed whitespace-pre-line"},appWarningModal.message),/*#__PURE__*/React.createElement("div",{className:"flex justify-end gap-2"},appWarningModal.isAlert?/*#__PURE__*/React.createElement("button",{onClick:()=>setAppWarningModal(null),className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"},"Đóng"):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("button",{onClick:()=>setAppWarningModal(null),className:"px-4 py-2 bg-zinc-700 hover:bg-zinc-600 active:bg-zinc-800 text-slate-300 rounded text-xs font-semibold transition"},"Hủy"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const fn=appWarningModal.onConfirm;setAppWarningModal(null);if(fn)fn();},className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"},"Xác nhận"))))),tabContextMenu&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[190]",onClick:()=>setTabContextMenu(null),onContextMenu:e=>{e.preventDefault();setTabContextMenu(null);}}),/*#__PURE__*/React.createElement("div",{className:"fixed bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl p-2 z-[200] flex gap-1.5",style:{top:tabContextMenu.y,left:tabContextMenu.x},onClick:e=>e.stopPropagation()},['#f43f5e','#f59e0b','#10b981','#06b6d4','#8b5cf6','#64748b'].map(color=>/*#__PURE__*/React.createElement("button",{key:color,className:"w-5 h-5 rounded-full border border-zinc-700 hover:scale-110 active:scale-95 transition",style:{backgroundColor:color},onClick:()=>{handleSetTabColor(tabContextMenu.tabId,tabContextMenu.tabType,color);setTabContextMenu(null);}})))),/*#__PURE__*/React.createElement(AuthModal,{isOpen:authModalOpen,mode:authMode,forceMandatory:isMandatoryLogin,onClose:()=>setAuthModalOpen(false),onSuccess:handleAuthSuccess}),/*#__PURE__*/React.createElement(ProfileModal,{isOpen:profileModalOpen,onClose:()=>setProfileModalOpen(false),tracks:tracks,setTracks:setTracks,setSelectedTrackId:setSelectedTrackId,projectName:projectName,setProjectName:setProjectName,currentProjectId:currentProjectId,setCurrentProjectId:setCurrentProjectId,showToast:showToast,loadAudioBuffersForTracks:loadAudioBuffersForTracks,setAppWarningModal:setAppWarningModal,bpm:bpm,setBpm:setBpm,setMasteringSettings:setMasteringSettings,setSessionTabs:setSessionTabs,setSubTabs:setSubTabs,trackMidiChannelsRef:trackMidiChannelsRef}),/*#__PURE__*/React.createElement(SaveProjectModal,{isOpen:saveProjectModalOpen,onClose:()=>setSaveProjectModalOpen(false),projectName:projectName,onSaveCloud:(newName,existingId)=>{handleSaveCloudProject(newName,existingId);},onSaveLocal:newName=>{handleSaveLocalProject(newName);}}),/*#__PURE__*/React.createElement(SaveAsModal,{isOpen:saveAsModalOpen,onClose:()=>setSaveAsModalOpen(false),projectName:projectName,onSaveCloud:newName=>{handleSaveAsCloud(newName);},onSaveLocal:newName=>{handleExportSFS(newName);setProjectName(newName);localStorage.setItem('sonic_project_name',newName);}}),/*#__PURE__*/React.createElement(OpenProjectModal,{isOpen:openProjectModalOpen,onClose:()=>setOpenProjectModalOpen(false),onOpenCloud:(projId,projName)=>{setOpenProjectModalOpen(false);handleOpenProject(projId,projName);},onOpenLocal:()=>{setOpenProjectModalOpen(false);handleImportSFS();}}),/*#__PURE__*/React.createElement(AIConfigModal,{isOpen:aiConfigModalOpen,onClose:()=>setAiConfigModalOpen(false),onConfigSaved:providers=>{setAiProviders(providers);const active=providers.find(p=>p.is_active)||providers[0];if(active)setSelectedProviderId(active.id);}}),/*#__PURE__*/React.createElement(AboutModal,{isOpen:aboutModalOpen,onClose:()=>setAboutModalOpen(false)}),/*#__PURE__*/React.createElement(HelpModal,{isOpen:helpModalOpen,onClose:()=>setHelpModalOpen(false),lang:prefs.language}),/*#__PURE__*/React.createElement(PreferencesModal,{isOpen:preferencesModalOpen,onClose:()=>setPreferencesModalOpen(false),prefs:prefs,onPrefsChange:handlePrefsChange}),/*#__PURE__*/React.createElement(SystemManagerModal,{isOpen:systemManagerModalOpen,onClose:()=>setSystemManagerModalOpen(false)}),/*#__PURE__*/React.createElement(PluginManagerModal,{isOpen:pluginManagerModalOpen,onClose:()=>setPluginManagerModalOpen(false),pluginsData:pluginsData,onInsertInstrument:inst=>{// Chèn instrument (soundfont bank/program) vào track đang chọn — nút Synth -const tid=selectedTrackId||activeTracks&&activeTracks[0]&&activeTracks[0].id;if(tid&&inst&&inst.instrumentId){setTrackInstrumentWithProgram(tid,inst.instrumentId,inst.program,inst.displayName||inst.name,inst.bank);showToast('Đã chèn nhạc cụ: '+(inst.displayName||inst.name),'success');}setPluginManagerModalOpen(false);}}),/*#__PURE__*/React.createElement(AIPresetModal,{isOpen:aiPresetModalOpen,onClose:()=>{setAiPresetModalOpen(false);setAiPresetVersion(v=>v+1);}}),/*#__PURE__*/React.createElement(MasteringModal,{isOpen:showMasteringModal,onClose:()=>setShowMasteringModal(false),masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings}),fxRackTarget&&/*#__PURE__*/React.createElement(FXRackModal,{track:(activeTracksRef.current||[]).find(t=>t.id===fxRackTarget.trackId)||tracks.find(t=>t.id===fxRackTarget.trackId)||null,onUpdateTrack:updateTrackProp,onClose:()=>setFxRackTarget(null)}),vkbOpen&&/*#__PURE__*/React.createElement(VirtualMidiKeyboard,{settings:vkbSettings,onChange:vkbUpdateSettings,tracks:activeTracks,activePitches:activeMidiPitches,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,onClose:()=>window.__toggleVkb()}),chordsPanelOpen&&/*#__PURE__*/React.createElement(ChordsPanel,{onClose:()=>window.__toggleChordsPanel(),onInsert:insertChordProgression,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,bpm:bpm}),/*#__PURE__*/React.createElement(ExportModal,{open:showExportPanel,onClose:()=>setShowExportPanel(false),exportSettings:exportSettings,setExportSettings:setExportSettings,isExporting:isExporting,exportProgress:exportProgress,onExport:()=>exportSettings.realtime?triggerBounceExport():triggerWavExport()}),instrumentSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:closeInstrumentSelector},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Select Instrument"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("select",{value:instrumentSelectorTrackId||'',onChange:e=>{var tid=e.target.value;if(tid){openInstrumentSelector(tid);}},className:"bg-[#1e1e1e] text-zinc-300 border border-zinc-700 rounded px-2 py-1 text-xs font-mono cursor-pointer"},activeTracks.map(function(at){return/*#__PURE__*/React.createElement("option",{key:at.id,value:at.id},at.name+' ('+(at.instrumentName||'Synth')+')');})),/*#__PURE__*/React.createElement("button",{onClick:closeInstrumentSelector,className:"text-slate-400 hover:text-slate-200"},"\u2715"))),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:sfPresetSearchQuery,onChange:e=>setSfPresetSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border border-zinc-700 rounded px-2 py-1.5 outline-none my-3 text-sm"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-4",style:{height:"420px"}},/*#__PURE__*/React.createElement("div",{className:"w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setSelectedSoundFontId(null);setSynthCategory(null);},className:"w-full text-left px-3 py-2 text-sm rounded "+(!selectedSoundFontId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},"All Instruments"),(instrumentSelectorData?.soundfonts||[]).map(sf=>{const sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;const sfName=sf.display||sf.name||sf.id;return/*#__PURE__*/React.createElement("button",{key:sfId,onClick:()=>{setSelectedSoundFontId(sfId);setSfPresets(null);const baseId=sfId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(baseId).then(data=>{if(data&&data.presets){const mapping=data.presets.map(p=>({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)}));setSfPresets(mapping);setInstrumentSelectorData(prev=>prev?{...prev,soundfonts:(prev.soundfonts||[]).map(s=>s.id===sf.id?{...s,presets:data.presets}:s)}:prev);}else{setSfPresets([]);}}).catch(()=>{setSfPresets([]);});},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectedSoundFontId===sfId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},sfName);}),// ── VST Instruments (scanned bởi Plugin Manager) ── +const tid=selectedTrackId||activeTracks&&activeTracks[0]&&activeTracks[0].id;if(tid&&inst&&inst.instrumentId){setTrackInstrumentWithProgram(tid,inst.instrumentId,inst.program,inst.displayName||inst.name,inst.bank);showToast('Đã chèn nhạc cụ: '+(inst.displayName||inst.name),'success');}setPluginManagerModalOpen(false);}}),/*#__PURE__*/React.createElement(AIPresetModal,{isOpen:aiPresetModalOpen,onClose:()=>{setAiPresetModalOpen(false);setAiPresetVersion(v=>v+1);}}),/*#__PURE__*/React.createElement(MasteringModal,{isOpen:showMasteringModal,onClose:()=>setShowMasteringModal(false),masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings}),fxRackTarget&&/*#__PURE__*/React.createElement(FXRackModal,{track:(activeTracksRef.current||[]).find(t=>t.id===fxRackTarget.trackId)||tracks.find(t=>t.id===fxRackTarget.trackId)||null,onUpdateTrack:updateTrackProp,onClose:()=>setFxRackTarget(null)}),vkbOpen&&/*#__PURE__*/React.createElement(VirtualMidiKeyboard,{settings:vkbSettings,onChange:vkbUpdateSettings,tracks:activeTracks,activePitches:activeMidiPitches,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,onClose:()=>window.__toggleVkb()}),chordsPanelOpen&&/*#__PURE__*/React.createElement(ChordsPanel,{onClose:()=>window.__toggleChordsPanel(),onInsert:insertChordProgression,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,bpm:bpm}),/*#__PURE__*/React.createElement(ExportProgressModal,{open:showExportProgress,exportProgress:exportProgress,isExporting:isExporting,onClose:()=>setShowExportProgress(false)}),/*#__PURE__*/React.createElement(ExportModal,{open:showExportPanel,onClose:()=>setShowExportPanel(false),exportSettings:exportSettings,setExportSettings:setExportSettings,isExporting:isExporting,exportProgress:exportProgress,onExport:()=>exportSettings.realtime?triggerBounceExport():triggerWavExport()}),instrumentSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:closeInstrumentSelector},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Select Instrument"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("select",{value:instrumentSelectorTrackId||'',onChange:e=>{var tid=e.target.value;if(tid){openInstrumentSelector(tid);}},className:"bg-[#1e1e1e] text-zinc-300 border border-zinc-700 rounded px-2 py-1 text-xs font-mono cursor-pointer"},activeTracks.map(function(at){return/*#__PURE__*/React.createElement("option",{key:at.id,value:at.id},at.name+' ('+(at.instrumentName||'Synth')+')');})),/*#__PURE__*/React.createElement("button",{onClick:closeInstrumentSelector,className:"text-slate-400 hover:text-slate-200"},"\u2715"))),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:sfPresetSearchQuery,onChange:e=>setSfPresetSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border border-zinc-700 rounded px-2 py-1.5 outline-none my-3 text-sm"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-4",style:{height:"420px"}},/*#__PURE__*/React.createElement("div",{className:"w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setSelectedSoundFontId(null);setSynthCategory(null);},className:"w-full text-left px-3 py-2 text-sm rounded "+(!selectedSoundFontId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},"All Instruments"),(instrumentSelectorData?.soundfonts||[]).map(sf=>{const sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;const sfName=sf.display||sf.name||sf.id;return/*#__PURE__*/React.createElement("button",{key:sfId,onClick:()=>{setSelectedSoundFontId(sfId);setSfPresets(null);const baseId=sfId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(baseId).then(data=>{if(data&&data.presets){const mapping=data.presets.map(p=>({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)}));setSfPresets(mapping);setInstrumentSelectorData(prev=>prev?{...prev,soundfonts:(prev.soundfonts||[]).map(s=>s.id===sf.id?{...s,presets:data.presets}:s)}:prev);}else{setSfPresets([]);}}).catch(()=>{setSfPresets([]);});},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectedSoundFontId===sfId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},sfName);}),// ── VST Instruments (scanned bởi Plugin Manager) ── instrumentSelectorData?.vst_instruments?.length>0&&/*#__PURE__*/React.createElement("div",{className:"mt-3 text-[10px] text-zinc-500 uppercase font-bold px-1 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3 text-violet-400"}),"VST Instruments"),(instrumentSelectorData?.vst_instruments||[]).map(v=>/*#__PURE__*/React.createElement("button",{key:'vst_modal_'+(v.id||v.name),onClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,v.id,v.name||v.id);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-cyan-400 shrink-0"},v.type||"VST")))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto space-y-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),sfPresets===null?(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},"Loading instruments...")):sfPresets.length>0?sfPresets.filter(p=>!selectedSoundFontId||p._sfId===selectedSoundFontId).filter(p=>!sfPresetSearchQuery||(p.name||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())||(p._sfName||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:i,onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program),onDoubleClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800/60 hover:bg-amber-800 text-zinc-300 truncate flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 shrink-0"},p._sfDisplay),p.bank===128?/*#__PURE__*/React.createElement("span",{className:"mr-1"},"🥁"):null,p.name||'Preset '+p.program)):(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},sfPresetSearchQuery?"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 ph\u00f9 h\u1ee3p.":"No presets found.")))))),fxSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:()=>setFxSelectorTrackId(null)},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-64 p-4 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400 mb-3"},"Track FX"),/*#__PURE__*/React.createElement("div",{className:"space-y-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'chorus'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300"},"Chorus"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'reverb'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300"},"Reverb")))),instrumentDropdownTrackId&&instrumentDropdownBtnRect&&/*#__PURE__*/React.createElement("div",{"data-instr-dropdown":"",className:"fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",style:{top:instrumentDropdownBtnRect.bottom+4,left:Math.max(4,Math.min(instrumentDropdownBtnRect.left,window.innerWidth-224))}},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:instrumentSearchQuery,onChange:e=>setInstrumentSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs outline-none"}),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentDropdownTrackId,null),className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),filteredInstruments.soundfonts.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"SoundFonts"),filteredInstruments.soundfonts.map((sf,i)=>/*#__PURE__*/React.createElement("button",{key:"sfd_"+i,onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,"sf_"+sf.id,sf.display||sf.name||sf.id,undefined,undefined);},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},sf.display||sf.name||sf.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-amber-400 shrink-0 ml-1"},"SF"))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Instruments"),filteredInstruments.vst.map((v,i)=>/*#__PURE__*/React.createElement("div",{key:"vstd_"+i,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,v.id,v.name||v.id);},className:"flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-cyan-400 shrink-0 ml-1"},v.type||"VST")))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Presets (.vstpreset)"),(window.SonicRuntime&&window.SonicRuntime.presets||[]).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:"psd_"+i,onClick:()=>setTrackPreset(instrumentDropdownTrackId,p.id),className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name||p.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-emerald-400 shrink-0 ml-1"},"PRESET"))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;window.SonicAPI.uploadPreset(f).then(function(r){if(r&&r.success){if(window.SonicRuntime)window.SonicRuntime.refreshPresets();showToast('Đã upload preset: '+(r.original_name||r.name||''),'success');}else{showToast('Upload preset thất bại','error');}}).catch(function(err){showToast('Upload lỗi: '+(err.message||err),'error');});};inp.click();},className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",title:"Upload preset .vstpreset xuất từ native GUI"},"\u2B06 Upload preset (t\u1EEB native GUI...)"),!filteredInstruments.soundfonts.length&&!filteredInstruments.vst.length&&/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-500 py-4 text-center"},"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o"))));};// React onWheel is passive -> e.preventDefault() can't stop panel scroll, so the // ruler slips away from the cursor mid-scroll. Native capture listener (non-passive) // blocks scrolling whenever the wheel starts over a [data-sf-wheel] container. diff --git a/app/templates/index.html b/app/templates/index.html index 15852a2..404fd0e 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -51,7 +51,7 @@ - +