From 1de6bd6b57b12ca31eeff62f37e308d0822366f1 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Sun, 2 Aug 2026 17:08:54 +0700 Subject: [PATCH] feat: Media Explorer persist tree width, canvas realtime, Synth instrument preview MIDI --- app/static/js/app.jsx | 160 +++++++++++++++++++++++++++++-- app/static/js/app.precompiled.js | 14 ++- app/templates/index.html | 2 +- wiki.md | 5 + 4 files changed, 165 insertions(+), 16 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index f25f1c8..d8575d0 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9025,13 +9025,27 @@ const MediaExplorerPanel = ({ height }) => { const [currentTime, setCurrentTime] = React.useState(0); const [peaks, setPeaks] = React.useState(null); const [audioBuffer, setAudioBuffer] = React.useState(null); + const [midiNotes, setMidiNotes] = React.useState(null); + const [midiTotal, setMidiTotal] = React.useState(4); const [computerRoots, setComputerRoots] = React.useState(null); const [computerTree, setComputerTree] = React.useState({}); const [computerPath, setComputerPath] = React.useState(null); const [computerFiles, setComputerFiles] = React.useState([]); const [computerMode, setComputerMode] = React.useState('server'); const [clientRoot, setClientRoot] = React.useState(null); - const [treeWidth, setTreeWidth] = React.useState(176); + const [synthInst, setSynthInst] = React.useState(function() { + var saved = localStorage.getItem('studio_media_explorer_synth'); + return saved ? JSON.parse(saved) : null; + }()); + const [synthOpen, setSynthOpen] = React.useState(false); + const [synthList, setSynthList] = React.useState(null); + const [synthLoading, setSynthLoading] = React.useState(false); + const synthListRef = React.useRef(null); + synthListRef.current = synthList; + const [treeWidth, setTreeWidth] = React.useState(function() { + var saved = localStorage.getItem('studio_media_explorer_tree_width'); + return saved ? parseInt(saved) : 176; + }()); const treeWidthRef = React.useRef(176); treeWidthRef.current = treeWidth; const startTreeResize = (e) => { @@ -9039,11 +9053,14 @@ const MediaExplorerPanel = ({ height }) => { const startX = e.clientX; const startW = treeWidthRef.current; const onMove = (ev) => { - setTreeWidth(Math.max(110, Math.min(420, startW + (ev.clientX - startX)))); + const newW = Math.max(110, Math.min(420, startW + (ev.clientX - startX))); + treeWidthRef.current = newW; + setTreeWidth(newW); }; const onUp = () => { document.removeEventListener('mousemove', onMove); document.removeEventListener('mouseup', onUp); + localStorage.setItem('studio_media_explorer_tree_width', treeWidthRef.current.toString()); }; document.addEventListener('mousemove', onMove); document.addEventListener('mouseup', onUp); @@ -9064,7 +9081,10 @@ const MediaExplorerPanel = ({ height }) => { const isMidiFile = f => f && (f.kind === 'midi' || /\.(mid|midi)$/i.test(f.name || f.original_name || '')); const fileDuration = f => { if (!f) return 0; - if (isMidiFile(f)) return (f.lengthQn || 16) * 60 / (f.bpm || 120); + if (isMidiFile(f)) { + if (midiTotal && midiNotes && midiNotes.length) return midiTotal; + return (f.lengthQn || 16) * 60 / (f.bpm || 120); + } const matches = selected && ((f.path && f.path === selected.path) || (!f.path && (f.file_id || f.fileId) === (selected.file_id || selected.fileId))); return f.duration || (audioBuffer && matches ? audioBuffer.duration : 0) || 0; }; @@ -9268,6 +9288,71 @@ const MediaExplorerPanel = ({ height }) => { return fid ? `${API_BASE_URL}/api/v1/audio/download/${fid}` : null; }; + const toggleSynthDropdown = () => { + if (synthOpen) { setSynthOpen(false); return; } + setSynthOpen(true); + if (synthListRef.current) return; + setSynthLoading(true); + (window.SonicAPI && window.SonicAPI.listPlugins ? window.SonicAPI.listPlugins() : Promise.resolve({ soundfonts: [] })) + .then(async (data) => { + const sfonts = (data && data.soundfonts) || []; + if (!sfonts.length) { setSynthList([]); setSynthLoading(false); return; } + const results = await Promise.all(sfonts.map(sf => { + const baseId = String(sf.id || '').replace('sf_', ''); + return (window.SonicAPI.listSoundfontInstruments ? window.SonicAPI.listSoundfontInstruments(baseId) : Promise.resolve({ presets: [] })) + .then(r => ({ sf, presets: (r && r.presets) || [] })) + .catch(() => ({ sf, presets: [] })); + })); + setSynthList(results); + setSynthLoading(false); + }) + .catch(() => { setSynthList([]); setSynthLoading(false); }); + }; + + const selectSynthInst = (inst) => { + setSynthInst(inst); + setSynthOpen(false); + localStorage.setItem('studio_media_explorer_synth', JSON.stringify(inst)); + }; + + const playMidiPreview = async (f, token) => { + // Play real MIDI file through selected synth instrument (SonicSF) + if (!f || !window.SonicSF) return; + try { + const buf = await readLocalFileBuffer(f); + if (!buf) return; + if (selectTokenRef.current !== (token || selectTokenRef.current)) return; + const midiResult = (typeof parseMidiFile === 'function' ? parseMidiFile : window.parseMidiFile)(buf); + if (!midiResult || !midiResult.length) return; + const ctx = getAudioContext(); + const bpmVal = parseInt(synthInst && synthInst.bpm) || 120; + const secondsPerBeat = 60.0 / bpmVal; + const startWallTime = ctx.currentTime + 0.05; + const program = synthInst ? synthInst.program : undefined; + const sfId = synthInst ? synthInst.sfId : undefined; + const bank = synthInst ? synthInst.bank : 0; + if (synthInst && window.SonicSF.selectInstrument) { + try { await window.SonicSF.selectInstrument(0, bank, program, sfId); } catch (e2) {} + } + const prog = (synthInst && synthInst.program !== undefined) ? synthInst.program : undefined; + const eng = synthInst ? { soundfont_id: sfId, soundfont_bank: bank, soundfont_program: prog !== undefined ? prog : 0 } : undefined; + midiResult.forEach(track => { + (track.notes || []).forEach(note => { + const startSec = (note.start_beat || 0) * secondsPerBeat; + const durMs = Math.max(80, (note.duration_beats || 1) * secondsPerBeat * 1000); + window.SonicSF.playNote(note.pitch || 60, (note.velocity || 0.8), durMs, startWallTime + startSec, prog, null, 0, eng); + }); + }); + // Keep a fake clock so canvas playhead animates; loop uses playStateRef + playStateRef.current = { source: null, ctx, startedAt: startWallTime, fakeStart: startWallTime, midiTotal: midiResult[0].duration || 4 }; + setMidiNotes(midiResult[0].notes || []); + setMidiTotal(midiResult[0].duration || 4); + setIsPlaying(true); + setIsPaused(false); + startCanvasClock(); + } catch (e) { console.error('MIDI preview failed', e); } + }; + const stopMediaPlayback = React.useCallback(() => { if (playStateRef.current) { try { if (playStateRef.current.source) playStateRef.current.source.stop(); } catch (e) {} @@ -9284,6 +9369,11 @@ const MediaExplorerPanel = ({ height }) => { if (!f) return; stopMediaPlayback(); if (isMidiFile(f)) { + if (f.handle || f.path || f.file_id || f.fileId) { + // Real MIDI file: play through selected synth instrument + await playMidiPreview(f, token); + return; + } playStateRef.current = { source: null, ctx: null, fakeStart: performance.now() / 1000 }; setIsPlaying(true); setIsPaused(false); @@ -9365,17 +9455,30 @@ const MediaExplorerPanel = ({ height }) => { ctx.strokeStyle = '#333'; for (let y = 0; y < h - 14; y += 10) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(w, y); ctx.stroke(); } ctx.strokeStyle = '#444'; - const beats = (f.lengthQn || 16); + const totalBeats = midiNotes && midiNotes.length ? Math.max(midiTotal * (f.bpm || 120) / 60, 4) : (f.lengthQn || 16); + const beats = totalBeats; for (let b = 0; b <= beats; b += 4) { const x = (b / beats) * w; ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, h - 14); ctx.stroke(); } ctx.fillStyle = '#9ca3af'; - const events = f.events || 76; - for (let i = 0; i < events; i++) { - const nx = ((i * 17 + 76) % 95) / 100 * (w - 20); - const ny = ((i * 13 + 76) % (h - 30)) + 4; - ctx.fillRect(nx, ny, Math.max(8, (i % 5 + 1) * 10), 3); + if (midiNotes && midiNotes.length) { + // Real piano-roll: rows = pitches (48..84), columns = beats + const pitchMin = 48, pitchMax = 84; + const pitchRange = Math.max(1, pitchMax - pitchMin); + midiNotes.forEach(n => { + const x = (n.start_beat / beats) * w; + const nw = Math.max(3, (n.duration_beats / beats) * w); + const y = h - 14 - 8 - (((Math.min(pitchMax, Math.max(pitchMin, n.pitch)) - pitchMin) / pitchRange) * (h - 30)); + ctx.fillRect(x, y, nw, 5); + }); + } else { + const events = f.events || 76; + for (let i = 0; i < events; i++) { + const nx = ((i * 17 + 76) % 95) / 100 * (w - 20); + const ny = ((i * 13 + 76) % (h - 30)) + 4; + ctx.fillRect(nx, ny, Math.max(8, (i % 5 + 1) * 10), 3); + } } } else { const pk = peaks && peaks.length > 0 ? peaks : null; @@ -9408,7 +9511,7 @@ const MediaExplorerPanel = ({ height }) => { } }; - React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, selected, folder, isPlaying]); + React.useEffect(() => { drawCanvas(currentTime); }, [peaks, audioBuffer, midiNotes, selected, folder, isPlaying]); React.useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []); const handleSelect = (f) => { @@ -9419,6 +9522,7 @@ const MediaExplorerPanel = ({ height }) => { setCurrentTime(0); setPeaks(null); setAudioBuffer(null); + setMidiNotes(null); stopMediaPlayback(); if (f.kind === 'other') return; if (autoPlay) { playSelected(f, token); } @@ -9583,6 +9687,41 @@ const MediaExplorerPanel = ({ height }) => { +
+ + {synthOpen && ( +
+
+ Select Instrument + +
+ {synthLoading &&
Loading...
} + {!synthLoading && (!synthList || synthList.length === 0) && ( +
Không có SoundFont nào
+ )} +
selectSynthInst(null)}> + None (mặc định) +
+ {!synthLoading && synthList && synthList.map(group => ( +
+
{group.sf.display || group.sf.name || group.sf.id}
+ {(group.presets || []).slice(0, 200).map((p, pi) => { + const progId = p.id || p.name || ('preset_' + pi); + return ( +
selectSynthInst({ sfId: group.sf.id, sfName: group.sf.display || group.sf.name || group.sf.id, bank: p.bank || 0, program: p.program, name: p.name || ('Program ' + p.program) })}> + {p.bank === 128 ? : } {p.name || ('Program ' + p.program)} +
+ ); + })} +
+ ))} +
+ )} +
@@ -16399,6 +16538,7 @@ const App = () => { } return result.length > 0 ? result : null; }; + window.parseMidiFile = parseMidiFile; // ── Load File on Track (with server upload) ── const loadFileOnTrack = async (trackId, file) => { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index d22aac8..754e8e4 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -232,12 +232,16 @@ for(let i=0;i{window.removeEventListener('resize',resizeAll);if(animFrameRef.current)cancelAnimationFrame(animFrameRef.current);};},[isOpen]);React.useEffect(()=>{if(!isOpen){stopAudioDemo();knobsInitializedRef.current=false;}else{setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},100);}},[isOpen]);if(!isOpen)return null;const switchModule=name=>setOzState(prev=>({...prev,activeModule:name}));const bandKnob=(param,min,max,val,unit,label,freq,color,filterType)=>/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-2 flex flex-col justify-between items-center rounded-lg"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[10px] font-bold w-full",style:{color}},/*#__PURE__*/React.createElement("span",null,label),/*#__PURE__*/React.createElement("span",{className:"text-[9px] font-mono text-slate-400"},freq)),/*#__PURE__*/React.createElement("div",{className:"my-1"},/*#__PURE__*/React.createElement(MasteringKnob,{param:param,min:min,max:max,value:ozState[param]!==undefined?ozState[param]:val,unit:unit,color:color,onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono text-center mt-1"},filterType));return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[100] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{id:"masteringModalBody",className:"flex flex-col bg-slate-950 text-slate-200 w-[95vw] h-[92vh] max-w-[1400px] rounded-2xl border border-slate-800/80 shadow-2xl overflow-hidden",onClick:e=>e.stopPropagation(),style:{fontFamily:"'Inter', sans-serif"}},/*#__PURE__*/React.createElement("header",{className:"h-12 bg-slate-900/90 border-b border-slate-800/80 flex items-center justify-between px-4 z-30 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("div",{className:"w-7 h-7 rounded-lg bg-cyan-600 flex items-center justify-center font-bold text-white shadow-lg shadow-cyan-950"},/*#__PURE__*/React.createElement("i",{"data-lucide":"zap",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("h1",{className:"text-xs font-bold tracking-wider text-white flex items-center gap-2"},"MASTERING SUITE ",/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/80 px-1.5 py-0.5 rounded oz-font-mono"},"WEB MASTERING V10.5")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 bg-slate-950 px-3 py-1 rounded-lg border border-slate-800/80"},/*#__PURE__*/React.createElement("button",{onClick:startAudioDemo,className:"px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 transition-all shadow-md shadow-cyan-950"},/*#__PURE__*/React.createElement("i",{"data-lucide":"play",className:"w-3 h-3"})," ",/*#__PURE__*/React.createElement("span",null,"Play Reference")),/*#__PURE__*/React.createElement("button",{onClick:stopAudioDemo,className:"px-3 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs font-semibold flex items-center gap-1.5 transition-all"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3 h-3"})," ",/*#__PURE__*/React.createElement("span",null,"Stop")),/*#__PURE__*/React.createElement("div",{className:"h-4 w-[1px] bg-slate-800 mx-1"}),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 text-xs oz-font-mono"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400 text-[11px]"},"Preset:"),/*#__PURE__*/React.createElement("select",{className:"bg-slate-900 border border-slate-700 rounded px-2 py-0.5 text-xs text-cyan-300 outline-none focus:border-cyan-500"},/*#__PURE__*/React.createElement("option",{value:"adaptive"},"Adaptive Dynamic Master"),/*#__PURE__*/React.createElement("option",{value:"edm_club"},"EDM / Club Punch Maximizer"),/*#__PURE__*/React.createElement("option",{value:"wide_space"},"Cinematic Stereo Expansion"),/*#__PURE__*/React.createElement("option",{value:"transparent"},"Transparent High-Clarity Limiter")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 text-xs oz-font-mono"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400 text-[11px]"},"Target LUFS:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold bg-slate-950 border border-cyan-900/60 px-2 py-0.5 rounded"},"-11.0 LUFS"),/*#__PURE__*/React.createElement("button",{onClick:()=>{getAudioContext();setMasterConnected(prev=>!prev);},className:`px-2 py-0.5 rounded text-[10px] font-bold border transition-colors ${masterConnected?'bg-emerald-700 border-emerald-500 text-white':'bg-slate-800 border-slate-700 text-slate-300 hover:bg-slate-700'}`},masterConnected?'Master ON':'Master OFF'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-2 py-0.5 rounded text-[10px] font-bold border transition-colors bg-slate-800 border-slate-700 text-slate-300 hover:bg-[#c2410c] hover:border-red-600 hover:text-white"},"Đóng"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-500 hover:text-slate-300 ml-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"})))),/*#__PURE__*/React.createElement("div",{className:"h-16 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-2 overflow-x-auto shrink-0 oz-scrollbar"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 oz-font-mono shrink-0"},"CHAIN:"),/*#__PURE__*/React.createElement("div",{onClick:()=>switchModule('eq'),className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all ${ozState.activeModule==='eq'?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setOzState(prev=>({...prev,eqActive:!prev.eqActive}));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold",style:{backgroundColor:ozState.eqActive?'#38bdf8':'#334155',color:ozState.eqActive?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200"},"Dynamic EQ"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-cyan-400 oz-font-mono"},"4-Band Peak"))),/*#__PURE__*/React.createElement("i",{"data-lucide":"activity",className:"w-3.5 h-3.5 text-slate-500"})),/*#__PURE__*/React.createElement("div",{onClick:()=>switchModule('imager'),className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all ${ozState.activeModule==='imager'?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setOzState(prev=>({...prev,imagerActive:!prev.imagerActive}));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold",style:{backgroundColor:ozState.imagerActive?'#38bdf8':'#334155',color:ozState.imagerActive?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200"},"Imager"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-400 oz-font-mono"},"4-Band Width"))),/*#__PURE__*/React.createElement("i",{"data-lucide":"radio",className:"w-3.5 h-3.5 text-slate-500"})),/*#__PURE__*/React.createElement("div",{onClick:()=>switchModule('maximizer'),className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all ${ozState.activeModule==='maximizer'?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setOzState(prev=>({...prev,maximizerActive:!prev.maximizerActive}));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold",style:{backgroundColor:ozState.maximizerActive?'#38bdf8':'#334155',color:ozState.maximizerActive?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200"},"Maximizer"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-400 oz-font-mono"},"IRC IV True Peak"))),/*#__PURE__*/React.createElement("i",{"data-lucide":"gauge",className:"w-3.5 h-3.5 text-slate-500"})),/*#__PURE__*/React.createElement("div",{className:"w-24 h-12 rounded-lg border border-dashed border-slate-800 flex items-center justify-center text-slate-600 hover:text-slate-400 hover:border-slate-600 cursor-pointer transition-all shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("main",{className:"flex-1 flex overflow-hidden min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar"},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs oz-font-mono shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"headphones",className:"w-3 h-3 text-cyan-400"})," Delta Listen"),/*#__PURE__*/React.createElement("select",{className:"bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none"},/*#__PURE__*/React.createElement("option",{value:"irc4"},"IRC IV - Classic"),/*#__PURE__*/React.createElement("option",{value:"irc3"},"IRC III - Balanced"),/*#__PURE__*/React.createElement("option",{value:"irc2"},"IRC II - Crisp"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 text-[11px]"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Learn Input Gain:"),/*#__PURE__*/React.createElement("button",{className:"bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors"},"-11.0 LUFS"))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='eq'?'':'hidden'}`},/*#__PURE__*/React.createElement("div",{className:"relative w-full h-64 bg-slate-950 border border-slate-800/90 rounded-xl overflow-hidden shadow-inner cursor-crosshair"},/*#__PURE__*/React.createElement("canvas",{ref:eqCanvasRef,className:"w-full h-full block"})),/*#__PURE__*/React.createElement("div",{className:"oz-panel p-3 rounded-xl grid grid-cols-4 gap-3"},bandKnob('eqLowGain',-12,12,1.5,'dB','BAND 1 (LOW)','100 Hz','#22d3ee','Shelf Filter'),bandKnob('eqMid1Gain',-12,12,-1.0,'dB','BAND 2 (MID LOW)','822 Hz','#fbbf24','Dynamic Bell (Q: 0.7)'),bandKnob('eqMid2Gain',-12,12,2.0,'dB','BAND 3 (MID HIGH)','3.2 kHz','#a855f7','Dynamic Bell (Q: 1.2)'),bandKnob('eqHighGain',-12,12,1.8,'dB','BAND 4 (HIGH)','10 kHz','#34d399','High Shelf'))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='imager'?'':'hidden'}`},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-12 gap-4 flex-1"},/*#__PURE__*/React.createElement("div",{className:"col-span-8 bg-slate-950 border border-slate-800/90 rounded-xl p-3 flex flex-col relative shadow-inner"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-400 uppercase oz-font-mono mb-2"},/*#__PURE__*/React.createElement("i",{"data-lucide":"radio",className:"w-3.5 h-3.5"})," Polar Vectorscope & Correlation Meter"),/*#__PURE__*/React.createElement("div",{className:"flex-1 relative w-full h-56 flex items-center justify-center"},/*#__PURE__*/React.createElement("canvas",{ref:imagerCanvasRef,className:"w-full h-full block"}))),/*#__PURE__*/React.createElement("div",{className:"col-span-4 oz-panel p-3 rounded-xl flex flex-col justify-between"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-slate-300 uppercase oz-font-mono"},"4-Band Stereo Width"),/*#__PURE__*/React.createElement("div",{className:"space-y-3 my-auto"},[{id:'w1',label:'Band 1 (0-100Hz)',color:'#22d3ee',val:ozState.w1},{id:'w2',label:'Band 2 (100-1kHz)',color:'#fbbf24',val:ozState.w2},{id:'w3',label:'Band 3 (1k-6kHz)',color:'#a855f7',val:ozState.w3},{id:'w4',label:'Band 4 (6k-20kHz)',color:'#34d399',val:ozState.w4}].map(b=>/*#__PURE__*/React.createElement("div",{key:b.id},/*#__PURE__*/React.createElement("div",{className:"flex justify-between text-[11px] font-mono mb-1"},/*#__PURE__*/React.createElement("span",{style:{color:b.color,fontWeight:700}},b.label),/*#__PURE__*/React.createElement("span",{id:b.id+'Val'},b.val,"%")),/*#__PURE__*/React.createElement("input",{type:"range",min:"-100",max:"100",value:b.val,onChange:e=>setOzState(prev=>({...prev,[b.id]:parseInt(e.target.value)})),className:"w-full h-1 cursor-pointer",style:{accentColor:b.color}}))))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='maximizer'?'':'hidden'}`},/*#__PURE__*/React.createElement("div",{className:"oz-panel p-4 rounded-xl flex-1 grid grid-cols-12 gap-6 items-center"},/*#__PURE__*/React.createElement("div",{className:"col-span-4 flex flex-col items-center justify-center border-r border-slate-800 pr-4"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-slate-400 uppercase oz-font-mono mb-3"},"Maximizer Gain Boost"),/*#__PURE__*/React.createElement("div",{className:"my-2"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxGain",min:0,max:12,value:ozState.maxGain,unit:"dB",color:"#22d3ee",size:"large",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"w-full mt-4 bg-slate-950 p-2 rounded-lg border border-slate-800 text-xs oz-font-mono flex flex-col gap-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center w-full"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Ceiling Level:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold"},ozState.ceiling.toFixed(2)," dB")),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"0",step:"0.1",value:ozState.ceiling,onChange:e=>setOzState(prev=>({...prev,ceiling:parseFloat(e.target.value)})),className:"w-full h-1 cursor-pointer accent-cyan-400"}))),/*#__PURE__*/React.createElement("div",{className:"col-span-8 grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxUpward",min:0,max:10,value:ozState.maxUpward,unit:"dB",label:"UPWARD COMPRESS",color:"#22d3ee",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxSoftClip",min:0,max:100,value:ozState.maxSoftClip,unit:"%",label:"SOFT CLIPPER",color:"#fbbf24",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 p-3 border border-slate-800 rounded-xl flex flex-col items-center justify-center h-full"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"maxTransient",min:0,max:100,value:ozState.maxTransient,unit:"%",label:"TRANSIENT EMPHASIS",color:"#34d399",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))}))))),/*#__PURE__*/React.createElement("div",{className:"border-t border-slate-800 bg-slate-900/60 p-3 flex flex-col shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between pb-1.5 border-b border-slate-800/80 mb-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-[11px] font-bold text-white uppercase tracking-wider"},"Wave Observer"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 text-cyan-400 border border-cyan-800/60 px-1.5 py-0.2 rounded font-mono"},"Real-time Oscilloscope")),/*#__PURE__*/React.createElement("div",{className:"flex gap-2"},['Scope','Settings','Help','About'].map(tab=>/*#__PURE__*/React.createElement("button",{key:tab,className:`px-2 py-0.5 rounded text-[10px] font-bold ${tab==='Scope'?'bg-cyan-950 text-cyan-300 border border-cyan-800/60':'text-slate-400 hover:text-slate-200'}`},tab)))),/*#__PURE__*/React.createElement("div",{className:"relative w-full h-32 bg-[#090d16] border border-slate-800/80 rounded-lg overflow-hidden mb-2"},/*#__PURE__*/React.createElement("canvas",{ref:woCanvasRef,className:"w-full h-full block"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between gap-4 text-[10px] oz-font-mono text-slate-400 mt-1 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 bg-slate-950 px-2.5 py-1.5 rounded border border-slate-800/80 shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-300"},"Input"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 w-20"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[8px] text-slate-500 w-2"},"L"),/*#__PURE__*/React.createElement("div",{className:"w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center"},/*#__PURE__*/React.createElement("div",{ref:woLeftMeterRef,className:"h-full bg-cyan-400 transition-all duration-75",style:{width:'0%'}}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[8px] text-slate-500 w-2"},"R"),/*#__PURE__*/React.createElement("div",{className:"w-16 h-1.5 bg-slate-900 rounded-sm overflow-hidden flex items-center"},/*#__PURE__*/React.createElement("div",{ref:woRightMeterRef,className:"h-full bg-teal-500 transition-all duration-75",style:{width:'0%'}}))))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4 bg-slate-950 px-3 py-1 rounded border border-slate-800/80 flex-1 justify-around"},/*#__PURE__*/React.createElement("span",{className:"font-bold text-slate-300 uppercase tracking-widest text-[9px]"},"Scope"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"Channel"),/*#__PURE__*/React.createElement("select",{value:woChannel,onChange:e=>setWoChannel(e.target.value),className:"bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none"},/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo"),/*#__PURE__*/React.createElement("option",{value:"left"},"Left Only"),/*#__PURE__*/React.createElement("option",{value:"right"},"Right Only"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"Mode"),/*#__PURE__*/React.createElement("select",{value:woMode,onChange:e=>setWoMode(e.target.value),className:"bg-slate-900 border border-slate-700 rounded px-1.5 py-0.5 text-[9px] text-slate-300 outline-none"},/*#__PURE__*/React.createElement("option",{value:"waveform"},"Waveform"),/*#__PURE__*/React.createElement("option",{value:"envelope"},"Envelope"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"Duration:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold w-10 text-right"},woDuration.toFixed(3),"s"),/*#__PURE__*/React.createElement("input",{type:"range",min:"0.5",max:"5.0",step:"0.1",value:woDuration,onChange:e=>setWoDuration(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer accent-cyan-400"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",null,"V.Zoom:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold w-12 text-right"},woZoom.toFixed(1)," dB"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"24",step:"0.5",value:woZoom,onChange:e=>setWoZoom(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer accent-cyan-400"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>setWoPaused(!woPaused),className:`px-3 py-1 rounded font-bold border transition shadow-md shrink-0 flex items-center gap-1 ${woPaused?'bg-amber-800 hover:bg-amber-700 border-amber-500 text-amber-100':'bg-slate-800 hover:bg-slate-700 border-slate-700 text-slate-200'}`},woPaused?'Resume':'Pause')))),/*#__PURE__*/React.createElement("div",{className:"w-64 bg-slate-900 border-l border-slate-800/80 p-3 flex flex-col justify-between shadow-2xl oz-font-mono text-xs z-20 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-2 mb-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-white tracking-wider uppercase flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-3 h-3 text-cyan-400"})," I/O METERS"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-300 px-1 py-0.5 rounded"},"TRUE PEAK")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 text-[10px] text-center mb-2"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800/80 p-1.5 rounded"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold"},"IN PEAK"),/*#__PURE__*/React.createElement("div",{id:"inPeakText",className:"text-cyan-400 font-bold oz-font-mono"},"-inf dB")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800/80 p-1.5 rounded"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold"},"OUT PEAK"),/*#__PURE__*/React.createElement("div",{id:"outPeakText",className:"text-emerald-400 font-bold oz-font-mono"},"-inf dB"))),/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-slate-950 border border-slate-800/90 rounded-xl p-2 flex justify-around relative overflow-hidden my-1"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center h-full"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 mb-1"},"IN"),/*#__PURE__*/React.createElement("div",{className:"flex-1 w-5 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end"},/*#__PURE__*/React.createElement("canvas",{ref:inMeterCanvasRef,className:"w-full h-full block"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center h-full"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 mb-1"},"OUT"),/*#__PURE__*/React.createElement("div",{className:"flex-1 w-6 bg-slate-900 border border-slate-800 rounded relative overflow-hidden flex items-end"},/*#__PURE__*/React.createElement("canvas",{ref:outMeterCanvasRef,className:"w-full h-full block"})))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-1.5 mt-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>setOzState(prev=>({...prev,isBypassed:!prev.isBypassed})),className:`py-1.5 rounded text-[10px] font-bold border transition-colors ${ozState.isBypassed?'bg-cyan-600 border-cyan-500 text-white':'bg-slate-800 hover:bg-slate-700 text-slate-200 border-slate-700'}`},"Bypass"),/*#__PURE__*/React.createElement("button",{className:"bg-slate-800 hover:bg-slate-700 text-slate-200 py-1.5 rounded text-[10px] font-bold border border-slate-700 transition-colors"},"Gain Match"),/*#__PURE__*/React.createElement("button",{className:"bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] border border-slate-700"},"Codec"),/*#__PURE__*/React.createElement("button",{className:"bg-slate-800 hover:bg-slate-700 text-slate-300 py-1.5 rounded text-[10px] border border-slate-700"},"Dither"))))));};// ── Media Explorer Panel (from md/51_MEDIA_EXPLORER.md) ── -const MEDIA_LIBRARY_SAMPLES=[{name:"MIDI_Loop_01.mid",events:95,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_02_Bass.mid",events:48,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_03_Lead.mid",events:110,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_04.mid",events:76,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_05_Bass.mid",events:52,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_06.mid",events:88,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"}];const MediaExplorerPanel=({height})=>{const[userFiles,setUserFiles]=React.useState([]);const[folder,setFolder]=React.useState('library');const[selected,setSelected]=React.useState(null);const[filterText,setFilterText]=React.useState('');const[viewMode,setViewMode]=React.useState('details');const[isPlaying,setIsPlaying]=React.useState(false);const[isPaused,setIsPaused]=React.useState(false);const[isLooping,setIsLooping]=React.useState(false);const[autoPlay,setAutoPlay]=React.useState(true);const[pitch,setPitch]=React.useState(0.0);const[rate,setRate]=React.useState(1.0);const[volumeDb,setVolumeDb]=React.useState(0.0);const[currentTime,setCurrentTime]=React.useState(0);const[peaks,setPeaks]=React.useState(null);const[audioBuffer,setAudioBuffer]=React.useState(null);const[computerRoots,setComputerRoots]=React.useState(null);const[computerTree,setComputerTree]=React.useState({});const[computerPath,setComputerPath]=React.useState(null);const[computerFiles,setComputerFiles]=React.useState([]);const[computerMode,setComputerMode]=React.useState('server');const[clientRoot,setClientRoot]=React.useState(null);const[treeWidth,setTreeWidth]=React.useState(176);const treeWidthRef=React.useRef(176);treeWidthRef.current=treeWidth;const startTreeResize=e=>{e.preventDefault();const startX=e.clientX;const startW=treeWidthRef.current;const onMove=ev=>{setTreeWidth(Math.max(110,Math.min(420,startW+(ev.clientX-startX))));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const canvasRef=React.useRef(null);const playStateRef=React.useRef(null);const rafRef=React.useRef(null);const selectTokenRef=React.useRef(0);React.useEffect(()=>{if(window.SonicAPI&&window.SonicAPI.listMyFiles){window.SonicAPI.listMyFiles([]).then(data=>setUserFiles(data||[])).catch(()=>{});}return()=>{stopMediaPlayback();};// eslint-disable-next-line react-hooks/exhaustive-deps -},[]);const isMidiFile=f=>f&&(f.kind==='midi'||/\.(mid|midi)$/i.test(f.name||f.original_name||''));const fileDuration=f=>{if(!f)return 0;if(isMidiFile(f))return(f.lengthQn||16)*60/(f.bpm||120);const matches=selected&&(f.path&&f.path===selected.path||!f.path&&(f.file_id||f.fileId)===(selected.file_id||selected.fileId));return f.duration||(audioBuffer&&matches?audioBuffer.duration:0)||0;};const folderFiles=React.useMemo(()=>{if(folder==='library')return MEDIA_LIBRARY_SAMPLES;if(folder==='computer'){const node=computerTree[computerPath]||{dirs:[]};return[...(node.dirs||[]),...computerFiles];}return userFiles.filter(f=>folder==='uploads'?(f.type||'Upload')==='Upload':(f.type||'Processed')==='Processed');},[folder,userFiles,computerFiles,computerTree,computerPath]);const visibleFiles=React.useMemo(()=>{const q=filterText.toLowerCase().trim();if(!q)return folderFiles;return folderFiles.filter(f=>(f.name||f.original_name||'').toLowerCase().includes(q));},[folderFiles,filterText]);const loadWaveform=async f=>{const token=selectTokenRef.current;if(f&&(f.handle||f.path)){try{const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==token)return;if(!buf){setPeaks(null);return;}const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==token)return;setAudioBuffer(decoded);const data=decoded.getChannelData(0);const count=600;const step=Math.max(1,Math.floor(data.length/count));const pk=[];for(let i=0;im)m=v;}pk.push(m);}setPeaks(pk);}catch(e){if(selectTokenRef.current===token)setPeaks(null);}return;}const fid=f.file_id||f.fileId;if(!fid){setPeaks(null);return;}try{const resp=await fetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);const data=await resp.json();if(selectTokenRef.current!==token)return;setPeaks(data.peaks||[]);}catch(e){setPeaks(null);}};const openMyComputer=async()=>{setFolder('computer');if(window.showDirectoryPicker){try{const handle=await window.showDirectoryPicker({mode:'read'});setComputerMode('client');setClientRoot(handle);const rootEntry={name:handle.name,path:'root',is_dir:true,handle};setComputerRoots([rootEntry]);setComputerPath('root');setComputerFiles([]);setComputerTree({root:{handle,parent:null,dirs:[],expanded:true}});await browseComputerDir(rootEntry);return;}catch(e){if(e&&e.name==='AbortError')return;window.showToast&&window.showToast('Không thể mở thư mục: '+e.message,'error');return;}}// Fallback: server API (trình duyệt không hỗ trợ File System Access API) +const MEDIA_LIBRARY_SAMPLES=[{name:"MIDI_Loop_01.mid",events:95,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_02_Bass.mid",events:48,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_03_Lead.mid",events:110,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_04.mid",events:76,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_05_Bass.mid",events:52,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"},{name:"MIDI_Loop_06.mid",events:88,lengthQn:16,time:"0:08.000",tpqn:480,bpm:130,kind:"midi"}];const MediaExplorerPanel=({height})=>{const[userFiles,setUserFiles]=React.useState([]);const[folder,setFolder]=React.useState('library');const[selected,setSelected]=React.useState(null);const[filterText,setFilterText]=React.useState('');const[viewMode,setViewMode]=React.useState('details');const[isPlaying,setIsPlaying]=React.useState(false);const[isPaused,setIsPaused]=React.useState(false);const[isLooping,setIsLooping]=React.useState(false);const[autoPlay,setAutoPlay]=React.useState(true);const[pitch,setPitch]=React.useState(0.0);const[rate,setRate]=React.useState(1.0);const[volumeDb,setVolumeDb]=React.useState(0.0);const[currentTime,setCurrentTime]=React.useState(0);const[peaks,setPeaks]=React.useState(null);const[audioBuffer,setAudioBuffer]=React.useState(null);const[midiNotes,setMidiNotes]=React.useState(null);const[midiTotal,setMidiTotal]=React.useState(4);const[computerRoots,setComputerRoots]=React.useState(null);const[computerTree,setComputerTree]=React.useState({});const[computerPath,setComputerPath]=React.useState(null);const[computerFiles,setComputerFiles]=React.useState([]);const[computerMode,setComputerMode]=React.useState('server');const[clientRoot,setClientRoot]=React.useState(null);const[synthInst,setSynthInst]=React.useState(function(){var saved=localStorage.getItem('studio_media_explorer_synth');return saved?JSON.parse(saved):null;}());const[synthOpen,setSynthOpen]=React.useState(false);const[synthList,setSynthList]=React.useState(null);const[synthLoading,setSynthLoading]=React.useState(false);const synthListRef=React.useRef(null);synthListRef.current=synthList;const[treeWidth,setTreeWidth]=React.useState(function(){var saved=localStorage.getItem('studio_media_explorer_tree_width');return saved?parseInt(saved):176;}());const treeWidthRef=React.useRef(176);treeWidthRef.current=treeWidth;const startTreeResize=e=>{e.preventDefault();const startX=e.clientX;const startW=treeWidthRef.current;const onMove=ev=>{const newW=Math.max(110,Math.min(420,startW+(ev.clientX-startX)));treeWidthRef.current=newW;setTreeWidth(newW);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);localStorage.setItem('studio_media_explorer_tree_width',treeWidthRef.current.toString());};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const canvasRef=React.useRef(null);const playStateRef=React.useRef(null);const rafRef=React.useRef(null);const selectTokenRef=React.useRef(0);React.useEffect(()=>{if(window.SonicAPI&&window.SonicAPI.listMyFiles){window.SonicAPI.listMyFiles([]).then(data=>setUserFiles(data||[])).catch(()=>{});}return()=>{stopMediaPlayback();};// eslint-disable-next-line react-hooks/exhaustive-deps +},[]);const isMidiFile=f=>f&&(f.kind==='midi'||/\.(mid|midi)$/i.test(f.name||f.original_name||''));const fileDuration=f=>{if(!f)return 0;if(isMidiFile(f)){if(midiTotal&&midiNotes&&midiNotes.length)return midiTotal;return(f.lengthQn||16)*60/(f.bpm||120);}const matches=selected&&(f.path&&f.path===selected.path||!f.path&&(f.file_id||f.fileId)===(selected.file_id||selected.fileId));return f.duration||(audioBuffer&&matches?audioBuffer.duration:0)||0;};const folderFiles=React.useMemo(()=>{if(folder==='library')return MEDIA_LIBRARY_SAMPLES;if(folder==='computer'){const node=computerTree[computerPath]||{dirs:[]};return[...(node.dirs||[]),...computerFiles];}return userFiles.filter(f=>folder==='uploads'?(f.type||'Upload')==='Upload':(f.type||'Processed')==='Processed');},[folder,userFiles,computerFiles,computerTree,computerPath]);const visibleFiles=React.useMemo(()=>{const q=filterText.toLowerCase().trim();if(!q)return folderFiles;return folderFiles.filter(f=>(f.name||f.original_name||'').toLowerCase().includes(q));},[folderFiles,filterText]);const loadWaveform=async f=>{const token=selectTokenRef.current;if(f&&(f.handle||f.path)){try{const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==token)return;if(!buf){setPeaks(null);return;}const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==token)return;setAudioBuffer(decoded);const data=decoded.getChannelData(0);const count=600;const step=Math.max(1,Math.floor(data.length/count));const pk=[];for(let i=0;im)m=v;}pk.push(m);}setPeaks(pk);}catch(e){if(selectTokenRef.current===token)setPeaks(null);}return;}const fid=f.file_id||f.fileId;if(!fid){setPeaks(null);return;}try{const resp=await fetch(`${API_BASE_URL}/api/v1/audio/waveform/${fid}?num_peaks=600`);const data=await resp.json();if(selectTokenRef.current!==token)return;setPeaks(data.peaks||[]);}catch(e){setPeaks(null);}};const openMyComputer=async()=>{setFolder('computer');if(window.showDirectoryPicker){try{const handle=await window.showDirectoryPicker({mode:'read'});setComputerMode('client');setClientRoot(handle);const rootEntry={name:handle.name,path:'root',is_dir:true,handle};setComputerRoots([rootEntry]);setComputerPath('root');setComputerFiles([]);setComputerTree({root:{handle,parent:null,dirs:[],expanded:true}});await browseComputerDir(rootEntry);return;}catch(e){if(e&&e.name==='AbortError')return;window.showToast&&window.showToast('Không thể mở thư mục: '+e.message,'error');return;}}// Fallback: server API (trình duyệt không hỗ trợ File System Access API) setComputerMode('server');if(computerRoots)return;setComputerRoots(null);try{const resp=await fetch(`${API_BASE_URL}/api/v1/media/computer`);if(!resp.ok)throw new Error('HTTP '+resp.status);const data=await resp.json();const roots=data.roots||[];setComputerRoots(roots.length?roots:[{path:'/',name:'Root (/)',is_dir:true}]);if(!roots.length)window.showToast&&window.showToast('Không tìm thấy ổ đĩa nào','warning');}catch(e){setComputerRoots([{path:'/',name:'Root (/)',is_dir:true}]);window.showToast&&window.showToast('Không thể truy cập My Computer: '+e.message,'error');}};const browseClientDir=async entry=>{const handle=entry&&entry.handle;if(!handle||!handle.entries)return;const dirs=[];const files=[];const parentPath=entry.path;try{for await(const[name,h]of handle.entries()){if(name.startsWith('.'))continue;if(h.kind==='directory'){dirs.push({name,path:parentPath+'/'+name,is_dir:true,handle:h});}else{const ext=(name.split('.').pop()||'').toLowerCase();const kind=ext==='mid'||ext==='midi'?'midi':['wav','mp3','ogg','flac','aiff','aif','m4a','aac','opus'].includes(ext)?'audio':'other';let size=0;try{const f=await h.getFile();size=f.size;}catch(e2){}files.push({name,path:parentPath+'/'+name,is_dir:false,size_mb:size?+(size/1048576).toFixed(2):0,ext:'.'+ext,kind,handle:h});}}dirs.sort((a,b)=>a.name.localeCompare(b.name));files.sort((a,b)=>a.name.localeCompare(b.name));setComputerPath(parentPath);setComputerFiles(files);setSelected(null);setCurrentTime(0);stopMediaPlayback();setComputerTree(prev=>({...prev,[parentPath]:{handle,parent:entry.parent||null,dirs,expanded:true}}));}catch(e){window.showToast&&window.showToast('Không thể đọc thư mục: '+e.message,'error');}};const browseComputerDir=async entry=>{if(!entry)return;if(computerMode==='client'||entry.handle){if(entry.handle&&entry.handle.entries){const parentInfo=computerTree[entry.path];await browseClientDir({...entry,parent:parentInfo?parentInfo.parent:null});return;}}const path=entry.path||entry;if(!path)return;try{const resp=await fetch(`${API_BASE_URL}/api/v1/media/browse?path=${encodeURIComponent(path)}`);if(!resp.ok)throw new Error('HTTP '+resp.status);const data=await resp.json();setComputerPath(data.path);setComputerFiles(data.files||[]);setSelected(null);setCurrentTime(0);stopMediaPlayback();setComputerTree(prev=>({...prev,[path]:{dirs:data.dirs||[],expanded:true}}));}catch(e){window.showToast&&window.showToast('Không thể mở thư mục: '+e.message,'error');}};const toggleComputerDir=async entry=>{if(!entry)return;const path=entry.path||entry;const node=computerTree[path];if(node&&node.expanded){setComputerTree(prev=>({...prev,[path]:{...prev[path],expanded:false}}));}else{await browseComputerDir(entry);}};const goComputerParent=()=>{if(!computerPath)return;if(computerMode==='client'||clientRoot){const node=computerTree[computerPath];const parentHandle=node&&node.parent;if(parentHandle){const parentEntry=computerRoots&&computerRoots[0]&&parentHandle===clientRoot?computerRoots[0]:{name:parentHandle.name,path:computerPath.split('/').slice(0,-1).join('/')||'root',is_dir:true,handle:parentHandle};browseComputerDir({...parentEntry,parent:parentHandle===clientRoot?null:computerTree[parentEntry.path]?computerTree[parentEntry.path].parent:null});}return;}const isUnix=computerPath.startsWith('/');const parts=computerPath.split(/[\\/]/).filter(Boolean);parts.pop();if(isUnix){browseComputerDir(parts.length?'/'+parts.join('/'):'/');}else{// Windows: quay về root ổ đĩa nếu đã lên tới đỉnh -browseComputerDir(parts.length?parts.join('\\'):computerPath.split(/[\\/]/)[0]+'\\');}};const readLocalFileBuffer=async f=>{if(f&&f.handle&&typeof f.handle.getFile==='function'){const file=await f.handle.getFile();return await file.arrayBuffer();}const url=filePreviewUrl(f);if(!url)return null;const resp=await fetch(url);return await resp.arrayBuffer();};const filePreviewUrl=f=>{if(f&&f.path&&!(f.handle&&f.handle.getFile))return`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(f.path)}`;const fid=f&&(f.file_id||f.fileId);return fid?`${API_BASE_URL}/api/v1/audio/download/${fid}`:null;};const stopMediaPlayback=React.useCallback(()=>{if(playStateRef.current){try{if(playStateRef.current.source)playStateRef.current.source.stop();}catch(e){}try{if(playStateRef.current.source)playStateRef.current.source.disconnect();}catch(e){}playStateRef.current=null;}if(rafRef.current)cancelAnimationFrame(rafRef.current);rafRef.current=null;setIsPlaying(false);setIsPaused(false);},[]);const playSelected=async(f,token)=>{if(!f)return;stopMediaPlayback();if(isMidiFile(f)){playStateRef.current={source:null,ctx:null,fakeStart:performance.now()/1000};setIsPlaying(true);setIsPaused(false);startCanvasClock();return;}const fid=f.file_id||f.fileId;const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==(token||selectTokenRef.current))return;if(!buf)return;try{const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==(token||selectTokenRef.current))return;setAudioBuffer(decoded);const src=ctx.createBufferSource();src.buffer=decoded;src.loop=isLooping;src.playbackRate.value=rate;const gain=ctx.createGain();const linear=volumeDb<=-50?0:Math.pow(10,volumeDb/20);gain.gain.value=linear;src.connect(gain);gain.connect(ctx.destination);src.start();const startedAt=ctx.currentTime;playStateRef.current={source:src,ctx,startedAt};setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('Preview failed',e);}};const togglePause=()=>{const st=playStateRef.current;if(!st)return;if(isPaused){if(st.ctx)st.ctx.resume();setIsPaused(false);}else{if(st.ctx)st.ctx.suspend();setIsPaused(true);}};const startCanvasClock=()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);const tick=()=>{rafRef.current=requestAnimationFrame(tick);let t=currentTime;const st=playStateRef.current;if(st&&!isPaused){t=st.ctx?st.ctx.currentTime-st.startedAt:performance.now()/1000-st.fakeStart;}setCurrentTime(t);drawCanvas(t);};tick();};const drawCanvas=t=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const w=canvas.clientWidth,h=canvas.clientHeight;if(!w||!h)return;canvas.width=w*2;canvas.height=h*2;ctx.scale(2,2);ctx.clearRect(0,0,w,h);ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const f=selected;if(!f){ctx.fillStyle='#555';ctx.font='12px JetBrains Mono, monospace';ctx.fillText('No file selected',10,h/2);return;}const dur=fileDuration(f);if(isMidiFile(f)){ctx.strokeStyle='#333';for(let y=0;y0?peaks:null;if(pk){const mid=h/2;ctx.fillStyle='#22c55e';for(let i=0;i{if(f&&f.handle&&typeof f.handle.getFile==='function'){const file=await f.handle.getFile();return await file.arrayBuffer();}const url=filePreviewUrl(f);if(!url)return null;const resp=await fetch(url);return await resp.arrayBuffer();};const filePreviewUrl=f=>{if(f&&f.path&&!(f.handle&&f.handle.getFile))return`${API_BASE_URL}/api/v1/media/file?path=${encodeURIComponent(f.path)}`;const fid=f&&(f.file_id||f.fileId);return fid?`${API_BASE_URL}/api/v1/audio/download/${fid}`:null;};const toggleSynthDropdown=()=>{if(synthOpen){setSynthOpen(false);return;}setSynthOpen(true);if(synthListRef.current)return;setSynthLoading(true);(window.SonicAPI&&window.SonicAPI.listPlugins?window.SonicAPI.listPlugins():Promise.resolve({soundfonts:[]})).then(async data=>{const sfonts=data&&data.soundfonts||[];if(!sfonts.length){setSynthList([]);setSynthLoading(false);return;}const results=await Promise.all(sfonts.map(sf=>{const baseId=String(sf.id||'').replace('sf_','');return(window.SonicAPI.listSoundfontInstruments?window.SonicAPI.listSoundfontInstruments(baseId):Promise.resolve({presets:[]})).then(r=>({sf,presets:r&&r.presets||[]})).catch(()=>({sf,presets:[]}));}));setSynthList(results);setSynthLoading(false);}).catch(()=>{setSynthList([]);setSynthLoading(false);});};const selectSynthInst=inst=>{setSynthInst(inst);setSynthOpen(false);localStorage.setItem('studio_media_explorer_synth',JSON.stringify(inst));};const playMidiPreview=async(f,token)=>{// Play real MIDI file through selected synth instrument (SonicSF) +if(!f||!window.SonicSF)return;try{const buf=await readLocalFileBuffer(f);if(!buf)return;if(selectTokenRef.current!==(token||selectTokenRef.current))return;const midiResult=(typeof parseMidiFile==='function'?parseMidiFile:window.parseMidiFile)(buf);if(!midiResult||!midiResult.length)return;const ctx=getAudioContext();const bpmVal=parseInt(synthInst&&synthInst.bpm)||120;const secondsPerBeat=60.0/bpmVal;const startWallTime=ctx.currentTime+0.05;const program=synthInst?synthInst.program:undefined;const sfId=synthInst?synthInst.sfId:undefined;const bank=synthInst?synthInst.bank:0;if(synthInst&&window.SonicSF.selectInstrument){try{await window.SonicSF.selectInstrument(0,bank,program,sfId);}catch(e2){}}const prog=synthInst&&synthInst.program!==undefined?synthInst.program:undefined;const eng=synthInst?{soundfont_id:sfId,soundfont_bank:bank,soundfont_program:prog!==undefined?prog:0}:undefined;midiResult.forEach(track=>{(track.notes||[]).forEach(note=>{const startSec=(note.start_beat||0)*secondsPerBeat;const durMs=Math.max(80,(note.duration_beats||1)*secondsPerBeat*1000);window.SonicSF.playNote(note.pitch||60,note.velocity||0.8,durMs,startWallTime+startSec,prog,null,0,eng);});});// Keep a fake clock so canvas playhead animates; loop uses playStateRef +playStateRef.current={source:null,ctx,startedAt:startWallTime,fakeStart:startWallTime,midiTotal:midiResult[0].duration||4};setMidiNotes(midiResult[0].notes||[]);setMidiTotal(midiResult[0].duration||4);setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('MIDI preview failed',e);}};const stopMediaPlayback=React.useCallback(()=>{if(playStateRef.current){try{if(playStateRef.current.source)playStateRef.current.source.stop();}catch(e){}try{if(playStateRef.current.source)playStateRef.current.source.disconnect();}catch(e){}playStateRef.current=null;}if(rafRef.current)cancelAnimationFrame(rafRef.current);rafRef.current=null;setIsPlaying(false);setIsPaused(false);},[]);const playSelected=async(f,token)=>{if(!f)return;stopMediaPlayback();if(isMidiFile(f)){if(f.handle||f.path||f.file_id||f.fileId){// Real MIDI file: play through selected synth instrument +await playMidiPreview(f,token);return;}playStateRef.current={source:null,ctx:null,fakeStart:performance.now()/1000};setIsPlaying(true);setIsPaused(false);startCanvasClock();return;}const fid=f.file_id||f.fileId;const buf=await readLocalFileBuffer(f);if(selectTokenRef.current!==(token||selectTokenRef.current))return;if(!buf)return;try{const ctx=getAudioContext();const decoded=await ctx.decodeAudioData(buf);if(selectTokenRef.current!==(token||selectTokenRef.current))return;setAudioBuffer(decoded);const src=ctx.createBufferSource();src.buffer=decoded;src.loop=isLooping;src.playbackRate.value=rate;const gain=ctx.createGain();const linear=volumeDb<=-50?0:Math.pow(10,volumeDb/20);gain.gain.value=linear;src.connect(gain);gain.connect(ctx.destination);src.start();const startedAt=ctx.currentTime;playStateRef.current={source:src,ctx,startedAt};setIsPlaying(true);setIsPaused(false);startCanvasClock();}catch(e){console.error('Preview failed',e);}};const togglePause=()=>{const st=playStateRef.current;if(!st)return;if(isPaused){if(st.ctx)st.ctx.resume();setIsPaused(false);}else{if(st.ctx)st.ctx.suspend();setIsPaused(true);}};const startCanvasClock=()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);const tick=()=>{rafRef.current=requestAnimationFrame(tick);let t=currentTime;const st=playStateRef.current;if(st&&!isPaused){t=st.ctx?st.ctx.currentTime-st.startedAt:performance.now()/1000-st.fakeStart;}setCurrentTime(t);drawCanvas(t);};tick();};const drawCanvas=t=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const w=canvas.clientWidth,h=canvas.clientHeight;if(!w||!h)return;canvas.width=w*2;canvas.height=h*2;ctx.scale(2,2);ctx.clearRect(0,0,w,h);ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const f=selected;if(!f){ctx.fillStyle='#555';ctx.font='12px JetBrains Mono, monospace';ctx.fillText('No file selected',10,h/2);return;}const dur=fileDuration(f);if(isMidiFile(f)){ctx.strokeStyle='#333';for(let y=0;y{const x=n.start_beat/beats*w;const nw=Math.max(3,n.duration_beats/beats*w);const y=h-14-8-(Math.min(pitchMax,Math.max(pitchMin,n.pitch))-pitchMin)/pitchRange*(h-30);ctx.fillRect(x,y,nw,5);});}else{const events=f.events||76;for(let i=0;i0?peaks:null;if(pk){const mid=h/2;ctx.fillStyle='#22c55e';for(let i=0;i0){const px=t/dur*w;ctx.fillStyle='#ef4444';ctx.fillRect(px,0,2,h-14);}};React.useEffect(()=>{drawCanvas(currentTime);},[peaks,audioBuffer,selected,folder,isPlaying]);React.useEffect(()=>()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);},[]);const handleSelect=f=>{if(!f||f.is_dir)return;selectTokenRef.current++;const token=selectTokenRef.current;setSelected(f);setCurrentTime(0);setPeaks(null);setAudioBuffer(null);stopMediaPlayback();if(f.kind==='other')return;if(autoPlay){playSelected(f,token);}if(!isMidiFile(f)&&(f.path||f.file_id||f.fileId))loadWaveform(f);};const renderComputerNode=(entry,depth,isRoot)=>{const nodePath=entry.path;const node=computerTree[nodePath];const expanded=node&&node.expanded;const dirs=node?node.dirs:[];const pad=12+depth*12;return/*#__PURE__*/React.createElement(React.Fragment,{key:nodePath},/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath===nodePath?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,style:{paddingLeft:pad},onClick:()=>browseComputerDir(entry),onDoubleClick:e=>{e.stopPropagation();toggleComputerDir(entry);}},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${expanded?'fa-minus':'fa-plus'} text-[9px] text-slate-500 w-3 text-center shrink-0`,onClick:e=>{e.stopPropagation();toggleComputerDir(entry);}}),/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isRoot?'fa-hard-drive text-[#6ea8dc]':'fa-folder text-[#d9a752]'} shrink-0`}),/*#__PURE__*/React.createElement("span",{className:"truncate"},entry.name)),expanded&&dirs.map(d=>renderComputerNode(d,depth+1,false)));};const toggleLoop=()=>{setIsLooping(prev=>{const next=!prev;if(playStateRef.current&&playStateRef.current.source)playStateRef.current.source.loop=next;return next;});};const toggleRate=dir=>setRate(prev=>Math.max(0.25,Math.min(4.0,Math.round((prev+dir*0.1)*100)/100)));const togglePitch=dir=>setPitch(prev=>Math.max(-24,Math.min(24,prev+dir*0.5)));const selIsMidi=isMidiFile(selected);const selDur=fileDuration(selected);const selBpm=selected&&(selected.bpm||120);return/*#__PURE__*/React.createElement("div",{className:"flex flex-col w-full h-full text-slate-900 overflow-hidden select-none",style:{fontFamily:"'Inter', sans-serif"}},/*#__PURE__*/React.createElement("div",{className:"h-8 bg-[#d4d0c8] border-b border-[#808080] px-2 flex items-center justify-between text-xs shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 flex-1 mr-2"},/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Back",onClick:()=>folder==='computer'?goComputerParent():setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-left"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Forward",onClick:()=>setFolder('uploads')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-right"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Up Directory",onClick:()=>folder==='computer'?goComputerParent():setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-up"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Refresh",onClick:()=>{if(folder==='computer'){const node=computerTree[computerPath];if(node&&node.handle)browseComputerDir({name:computerPath.split('/').pop()||computerPath,path:computerPath,is_dir:true,handle:node.handle});else if(computerPath)browseComputerDir({name:computerPath.split(/[\\/]/).pop()||computerPath,path:computerPath,is_dir:true});}else if(window.SonicAPI&&window.SonicAPI.listMyFiles)window.SonicAPI.listMyFiles([]).then(d=>setUserFiles(d||[]));}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-rotate-right"})),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex items-center bg-white border border-[#808080] h-5 px-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752] text-[11px] mr-1.5"}),/*#__PURE__*/React.createElement("span",{className:"flex-1 text-xs text-slate-800 truncate"},folder==='computer'?computerPath||'My Computer':folder==='library'?'Media Library':folder==='uploads'?'Uploads':'Processed'),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center bg-white border border-[#808080] h-5 px-1 w-40"},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Filter/Search...",value:filterText,onChange:e=>setFilterText(e.target.value),className:"w-full text-xs outline-none bg-transparent font-sans text-slate-800"}),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"})),/*#__PURE__*/React.createElement("button",{className:"px-2 py-0.5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[11px] flex items-center gap-1 font-semibold",onClick:()=>setViewMode(viewMode==='details'?'list':'details')},/*#__PURE__*/React.createElement("span",null,viewMode==='details'?'Details':'List')," ",/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-[9px]"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex bg-white border border-[#808080] m-1 mr-0 text-xs select-none shrink-0 overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"overflow-y-auto p-1",style:{width:treeWidth+'px',minWidth:treeWidth+'px',maxWidth:treeWidth+'px'}},/*#__PURE__*/React.createElement("div",{className:"space-y-0.5 font-sans"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("span",{className:"w-3"})," "),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("span",{className:"w-3"})," "),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder==='computer'?'bg-slate-300 text-slate-900 font-semibold':'hover:bg-slate-200 text-slate-800'}`,onClick:openMyComputer},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-computer text-[11px] text-slate-600"})," My Computer"),folder==='computer'&&computerRoots&&/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},computerRoots.map(root=>renderComputerNode(root,0,true))),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer font-semibold rounded-sm ${folder==='library'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752]"})," Media Library"),/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder==='uploads'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('uploads')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752]"})," Uploads"),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder==='processed'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('processed')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752]"})," Processed")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-plus text-[9px] text-slate-500"})," Desktop"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-plus text-[9px] text-slate-500"})," My Documents"))),/*#__PURE__*/React.createElement("div",{className:"w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0",onMouseDown:startTreeResize,title:"Kéo để thay đổi chiều rộng"})),/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-white border border-[#808080] m-1 overflow-y-auto relative"},/*#__PURE__*/React.createElement("table",{className:"w-full text-xs text-left border-collapse"},/*#__PURE__*/React.createElement("thead",{className:"sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10"},/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0]"},"File"),viewMode==='details'&&/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0]"},"Size"))),/*#__PURE__*/React.createElement("tbody",{className:"font-sans text-slate-800"},visibleFiles.map((f,i)=>{const isSel=selected&&(selected.name||selected.file_id)===(f.name||f.file_id);const isMidi=isMidiFile(f);const icon=f.is_dir?'fa-folder text-[#d9a752]':isMidi?'fa-music text-purple-600':f.kind==='audio'?'fa-file-audio text-emerald-600':'fa-file text-zinc-500';return/*#__PURE__*/React.createElement("tr",{key:(f.path||f.file_id||f.name)+i,className:`cursor-pointer hover:bg-blue-100 ${isSel?'file-row-selected':''}`,onClick:()=>f.is_dir?browseComputerDir(f):handleSelect(f),onDoubleClick:()=>f.is_dir&&browseComputerDir(f)},/*#__PURE__*/React.createElement("td",{className:"py-1 px-2"},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${icon} mr-2`}),f.name||f.original_name),viewMode==='details'&&/*#__PURE__*/React.createElement("td",{className:"py-1 px-2"},f.size_mb!=null?f.size_mb.toFixed(2)+' MB':isMidi?(f.tpqn||'MIDI')+' TPQN':'-'));}),visibleFiles.length===0&&/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("td",{className:"py-3 px-2 text-slate-400 italic",colSpan:viewMode==='details'?2:1},"No files")))))),/*#__PURE__*/React.createElement("div",{className:"h-[38%] min-h-[110px] bg-[#d4d0c8] border-t border-[#808080] p-1.5 flex flex-col justify-between text-xs shrink-0 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{id:"btnStop",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-slate-800",title:"Stop",onClick:stopMediaPlayback},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-square text-[10px]"})),/*#__PURE__*/React.createElement("button",{id:"btnPlay",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-emerald-700 font-bold",title:"Play",onClick:()=>isPlaying?togglePause():playSelected(selected)},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isPlaying&&!isPaused?'fa-play':'fa-play'} text-xs`})),/*#__PURE__*/React.createElement("button",{id:"btnPause",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-amber-700",title:"Pause",onClick:togglePause},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-pause text-xs"})),/*#__PURE__*/React.createElement("button",{id:"btnLoop",className:`w-6 h-6 border rounded-sm flex items-center justify-center text-xs ${isLooping?'bg-cyan-600 text-white border-cyan-700':'bg-[#e0e0e0] hover:bg-white text-slate-700 border-[#707070]'}`,title:"Loop / Repeat",onClick:toggleLoop},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-rotate-right"})),/*#__PURE__*/React.createElement("button",{id:"btnAutoPlay",className:`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${autoPlay?'bg-gradient-to-r from-cyan-600 to-emerald-600 text-white border-slate-700':'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`,onClick:()=>setAutoPlay(p=>!p)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-bolt text-[9px]"})," ",/*#__PURE__*/React.createElement("span",null,"Auto-Play"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 font-mono text-[11px]"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",null,"Pitch:"),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>togglePitch(-0.5)},"-"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-14"},/*#__PURE__*/React.createElement("input",{type:"number",value:pitch.toFixed(1),step:"0.5",onChange:e=>setPitch(parseFloat(e.target.value)||0),className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>togglePitch(0.5)},"+")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",null,"Rate:"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-12"},/*#__PURE__*/React.createElement("input",{type:"number",value:rate.toFixed(2),step:"0.1",onChange:e=>setRate(Math.max(0.25,Math.min(4,parseFloat(e.target.value)||1))),className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]",onClick:()=>toggleRate(-1)},"-"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]",onClick:()=>toggleRate(1)},"+"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"font-sans text-slate-700"},"Volume:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-60",max:"12",step:"0.5",value:volumeDb,onChange:e=>setVolumeDb(parseFloat(e.target.value)),className:"me-fader-slider w-24"}),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center justify-center w-14 font-mono text-[11px]"},volumeDb<=-50?'-inf':volumeDb.toFixed(1)," dB")),/*#__PURE__*/React.createElement("div",{className:`px-2 py-0.5 border font-mono font-bold text-[10px] rounded-sm ${selIsMidi?'bg-purple-950 text-purple-300 border-purple-800':'bg-emerald-950 text-emerald-300 border-emerald-800'}`},selIsMidi?'MIDI':'Audio')),/*#__PURE__*/React.createElement("div",{className:"flex items-stretch gap-2 my-1 flex-1 min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-[#181818] border border-[#3a3a3a] relative overflow-hidden"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-full block cursor-pointer"})),/*#__PURE__*/React.createElement("div",{className:"w-48 bg-[#181818] border border-[#3a3a3a] p-1.5 font-mono text-[10px] text-slate-300 leading-tight overflow-y-auto shrink-0"},selected?selIsMidi&&!selected.path?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,selected.events," MIDI events"),/*#__PURE__*/React.createElement("div",null,"Length: ",selected.lengthQn," quarter notes"),/*#__PURE__*/React.createElement("div",null,"Length: ",selected.time," (est)"),/*#__PURE__*/React.createElement("div",null,"Ticks per quarter note: ",selected.tpqn)):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Size: ",selected.size_mb!=null?selected.size_mb.toFixed(2)+' MB':'-'),/*#__PURE__*/React.createElement("div",null,"Duration: ",selDur.toFixed(2),"s"),/*#__PURE__*/React.createElement("div",null,"Sample Rate: 44100 Hz"),/*#__PURE__*/React.createElement("div",null,"Type: ",selected.path?selIsMidi?'Local MIDI':selected.kind==='other'?'Local File':'Local Audio':selected.type||'Audio')):/*#__PURE__*/React.createElement("div",null,"No file selected"))),/*#__PURE__*/React.createElement("div",{className:"h-5 bg-[#c0c0c0] border-t border-white flex items-center justify-between text-[11px] font-mono px-1 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1.5 text-slate-900 font-bold"},formatTime(currentTime)," / ",formatTime(selDur))),/*#__PURE__*/React.createElement("div",{className:"text-slate-800 font-bold truncate max-w-[40%]"},selected?selected.name||selected.original_name:'No file selected'),/*#__PURE__*/React.createElement("div",{className:"text-slate-700"},selBpm," bpm x",rate.toFixed(2)))));};const App=()=>{// ── State Definitions ── +if(isPlaying&&dur>0){const px=t/dur*w;ctx.fillStyle='#ef4444';ctx.fillRect(px,0,2,h-14);}};React.useEffect(()=>{drawCanvas(currentTime);},[peaks,audioBuffer,midiNotes,selected,folder,isPlaying]);React.useEffect(()=>()=>{if(rafRef.current)cancelAnimationFrame(rafRef.current);},[]);const handleSelect=f=>{if(!f||f.is_dir)return;selectTokenRef.current++;const token=selectTokenRef.current;setSelected(f);setCurrentTime(0);setPeaks(null);setAudioBuffer(null);setMidiNotes(null);stopMediaPlayback();if(f.kind==='other')return;if(autoPlay){playSelected(f,token);}if(!isMidiFile(f)&&(f.path||f.file_id||f.fileId))loadWaveform(f);};const renderComputerNode=(entry,depth,isRoot)=>{const nodePath=entry.path;const node=computerTree[nodePath];const expanded=node&&node.expanded;const dirs=node?node.dirs:[];const pad=12+depth*12;return/*#__PURE__*/React.createElement(React.Fragment,{key:nodePath},/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${computerPath===nodePath?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,style:{paddingLeft:pad},onClick:()=>browseComputerDir(entry),onDoubleClick:e=>{e.stopPropagation();toggleComputerDir(entry);}},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${expanded?'fa-minus':'fa-plus'} text-[9px] text-slate-500 w-3 text-center shrink-0`,onClick:e=>{e.stopPropagation();toggleComputerDir(entry);}}),/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isRoot?'fa-hard-drive text-[#6ea8dc]':'fa-folder text-[#d9a752]'} shrink-0`}),/*#__PURE__*/React.createElement("span",{className:"truncate"},entry.name)),expanded&&dirs.map(d=>renderComputerNode(d,depth+1,false)));};const toggleLoop=()=>{setIsLooping(prev=>{const next=!prev;if(playStateRef.current&&playStateRef.current.source)playStateRef.current.source.loop=next;return next;});};const toggleRate=dir=>setRate(prev=>Math.max(0.25,Math.min(4.0,Math.round((prev+dir*0.1)*100)/100)));const togglePitch=dir=>setPitch(prev=>Math.max(-24,Math.min(24,prev+dir*0.5)));const selIsMidi=isMidiFile(selected);const selDur=fileDuration(selected);const selBpm=selected&&(selected.bpm||120);return/*#__PURE__*/React.createElement("div",{className:"flex flex-col w-full h-full text-slate-900 overflow-hidden select-none",style:{fontFamily:"'Inter', sans-serif"}},/*#__PURE__*/React.createElement("div",{className:"h-8 bg-[#d4d0c8] border-b border-[#808080] px-2 flex items-center justify-between text-xs shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 flex-1 mr-2"},/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Back",onClick:()=>folder==='computer'?goComputerParent():setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-left"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Forward",onClick:()=>setFolder('uploads')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-right"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Up Directory",onClick:()=>folder==='computer'?goComputerParent():setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-arrow-up"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-[10px]",title:"Refresh",onClick:()=>{if(folder==='computer'){const node=computerTree[computerPath];if(node&&node.handle)browseComputerDir({name:computerPath.split('/').pop()||computerPath,path:computerPath,is_dir:true,handle:node.handle});else if(computerPath)browseComputerDir({name:computerPath.split(/[\\/]/).pop()||computerPath,path:computerPath,is_dir:true});}else if(window.SonicAPI&&window.SonicAPI.listMyFiles)window.SonicAPI.listMyFiles([]).then(d=>setUserFiles(d||[]));}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-rotate-right"})),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex items-center bg-white border border-[#808080] h-5 px-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752] text-[11px] mr-1.5"}),/*#__PURE__*/React.createElement("span",{className:"flex-1 text-xs text-slate-800 truncate"},folder==='computer'?computerPath||'My Computer':folder==='library'?'Media Library':folder==='uploads'?'Uploads':'Processed'),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center bg-white border border-[#808080] h-5 px-1 w-40"},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Filter/Search...",value:filterText,onChange:e=>setFilterText(e.target.value),className:"w-full text-xs outline-none bg-transparent font-sans text-slate-800"}),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-slate-600 text-[10px] ml-1"})),/*#__PURE__*/React.createElement("button",{className:"px-2 py-0.5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[11px] flex items-center gap-1 font-semibold",onClick:()=>setViewMode(viewMode==='details'?'list':'details')},/*#__PURE__*/React.createElement("span",null,viewMode==='details'?'Details':'List')," ",/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-[9px]"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex bg-white border border-[#808080] m-1 mr-0 text-xs select-none shrink-0 overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"overflow-y-auto p-1",style:{width:treeWidth+'px',minWidth:treeWidth+'px',maxWidth:treeWidth+'px'}},/*#__PURE__*/React.createElement("div",{className:"space-y-0.5 font-sans"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("span",{className:"w-3"})," "),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("span",{className:"w-3"})," "),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder==='computer'?'bg-slate-300 text-slate-900 font-semibold':'hover:bg-slate-200 text-slate-800'}`,onClick:openMyComputer},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-computer text-[11px] text-slate-600"})," My Computer"),folder==='computer'&&computerRoots&&/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},computerRoots.map(root=>renderComputerNode(root,0,true))),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer font-semibold rounded-sm ${folder==='library'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('library')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752]"})," Media Library"),/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder==='uploads'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('uploads')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752]"})," Uploads"),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer pl-4 rounded-sm ${folder==='processed'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>setFolder('processed')},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752]"})," Processed")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-plus text-[9px] text-slate-500"})," Desktop"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 px-1 py-0.5 text-slate-700"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-plus text-[9px] text-slate-500"})," My Documents"))),/*#__PURE__*/React.createElement("div",{className:"w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0",onMouseDown:startTreeResize,title:"Kéo để thay đổi chiều rộng"})),/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-white border border-[#808080] m-1 overflow-y-auto relative"},/*#__PURE__*/React.createElement("table",{className:"w-full text-xs text-left border-collapse"},/*#__PURE__*/React.createElement("thead",{className:"sticky top-0 bg-[#e0e0e0] border-b border-[#a0a0a0] text-slate-800 font-semibold select-none shadow-sm z-10"},/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0]"},"File"),viewMode==='details'&&/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0]"},"Size"))),/*#__PURE__*/React.createElement("tbody",{className:"font-sans text-slate-800"},visibleFiles.map((f,i)=>{const isSel=selected&&(selected.name||selected.file_id)===(f.name||f.file_id);const isMidi=isMidiFile(f);const icon=f.is_dir?'fa-folder text-[#d9a752]':isMidi?'fa-music text-purple-600':f.kind==='audio'?'fa-file-audio text-emerald-600':'fa-file text-zinc-500';return/*#__PURE__*/React.createElement("tr",{key:(f.path||f.file_id||f.name)+i,className:`cursor-pointer hover:bg-blue-100 ${isSel?'file-row-selected':''}`,onClick:()=>f.is_dir?browseComputerDir(f):handleSelect(f),onDoubleClick:()=>f.is_dir&&browseComputerDir(f)},/*#__PURE__*/React.createElement("td",{className:"py-1 px-2"},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${icon} mr-2`}),f.name||f.original_name),viewMode==='details'&&/*#__PURE__*/React.createElement("td",{className:"py-1 px-2"},f.size_mb!=null?f.size_mb.toFixed(2)+' MB':isMidi?(f.tpqn||'MIDI')+' TPQN':'-'));}),visibleFiles.length===0&&/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("td",{className:"py-3 px-2 text-slate-400 italic",colSpan:viewMode==='details'?2:1},"No files")))))),/*#__PURE__*/React.createElement("div",{className:"h-[38%] min-h-[110px] bg-[#d4d0c8] border-t border-[#808080] p-1.5 flex flex-col justify-between text-xs shrink-0 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{id:"btnStop",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-slate-800",title:"Stop",onClick:stopMediaPlayback},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-square text-[10px]"})),/*#__PURE__*/React.createElement("button",{id:"btnPlay",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-emerald-700 font-bold",title:"Play",onClick:()=>isPlaying?togglePause():playSelected(selected)},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isPlaying&&!isPaused?'fa-play':'fa-play'} text-xs`})),/*#__PURE__*/React.createElement("button",{id:"btnPause",className:"w-6 h-6 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm flex items-center justify-center text-amber-700",title:"Pause",onClick:togglePause},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-pause text-xs"})),/*#__PURE__*/React.createElement("button",{id:"btnLoop",className:`w-6 h-6 border rounded-sm flex items-center justify-center text-xs ${isLooping?'bg-cyan-600 text-white border-cyan-700':'bg-[#e0e0e0] hover:bg-white text-slate-700 border-[#707070]'}`,title:"Loop / Repeat",onClick:toggleLoop},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-rotate-right"})),/*#__PURE__*/React.createElement("button",{id:"btnAutoPlay",className:`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${autoPlay?'bg-gradient-to-r from-cyan-600 to-emerald-600 text-white border-slate-700':'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`,onClick:()=>setAutoPlay(p=>!p)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-bolt text-[9px]"})," ",/*#__PURE__*/React.createElement("span",null,"Auto-Play")),/*#__PURE__*/React.createElement("div",{className:"relative"},/*#__PURE__*/React.createElement("button",{id:"btnSynth",className:`h-6 px-2 border rounded-sm font-bold text-[10px] flex items-center gap-1 ${synthInst?'bg-gradient-to-r from-violet-600 to-fuchsia-600 text-white border-slate-700':'bg-[#e0e0e0] text-slate-600 border-[#707070]'}`,title:"Chọn instrument để preview MIDI",onClick:toggleSynthDropdown},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-music text-[9px]"})," ",/*#__PURE__*/React.createElement("span",null,"Synth",synthInst?': '+(synthInst.name||'?'):''),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-caret-down text-[8px]"})),synthOpen&&/*#__PURE__*/React.createElement("div",{className:"absolute bottom-8 left-0 z-40 w-64 bg-white border border-[#808080] shadow-xl rounded-sm text-xs text-slate-800 font-sans max-h-72 overflow-y-auto"},/*#__PURE__*/React.createElement("div",{className:"sticky top-0 bg-[#e0e0e0] px-2 py-1 font-bold border-b border-[#a0a0a0] flex items-center justify-between"},/*#__PURE__*/React.createElement("span",null,"Select Instrument"),/*#__PURE__*/React.createElement("button",{onClick:()=>setSynthOpen(false),className:"text-slate-500 hover:text-slate-900"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"}))),synthLoading&&/*#__PURE__*/React.createElement("div",{className:"px-2 py-2 text-slate-400 italic"},"Loading..."),!synthLoading&&(!synthList||synthList.length===0)&&/*#__PURE__*/React.createElement("div",{className:"px-2 py-2 text-slate-400 italic"},"Không có SoundFont nào"),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst?'bg-slate-200':''}`,onClick:()=>selectSynthInst(null)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-ban text-slate-400"})," None (mặc định)"),!synthLoading&&synthList&&synthList.map(group=>/*#__PURE__*/React.createElement("div",{key:group.sf.id||group.sf.name},/*#__PURE__*/React.createElement("div",{className:"px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate"},group.sf.display||group.sf.name||group.sf.id),(group.presets||[]).slice(0,200).map((p,pi)=>{const progId=p.id||p.name||'preset_'+pi;return/*#__PURE__*/React.createElement("div",{key:progId,className:`flex items-center gap-1 px-2 pl-4 py-0.5 cursor-pointer hover:bg-blue-100 truncate ${synthInst&&synthInst.program===p.program&&synthInst.sfId===(group.sf.id||group.sf.name)?'bg-slate-200':''}`,onClick:()=>selectSynthInst({sfId:group.sf.id,sfName:group.sf.display||group.sf.name||group.sf.id,bank:p.bank||0,program:p.program,name:p.name||'Program '+p.program})},p.bank===128?/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-drum text-slate-400"}):/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-music text-slate-400"})," ",p.name||'Program '+p.program);})))))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 font-mono text-[11px]"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",null,"Pitch:"),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>togglePitch(-0.5)},"-"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-14"},/*#__PURE__*/React.createElement("input",{type:"number",value:pitch.toFixed(1),step:"0.5",onChange:e=>setPitch(parseFloat(e.target.value)||0),className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>togglePitch(0.5)},"+")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",null,"Rate:"),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center w-12"},/*#__PURE__*/React.createElement("input",{type:"number",value:rate.toFixed(2),step:"0.1",onChange:e=>setRate(Math.max(0.25,Math.min(4,parseFloat(e.target.value)||1))),className:"w-full text-xs text-right outline-none bg-transparent"})),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]",onClick:()=>toggleRate(-1)},"-"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[9px]",onClick:()=>toggleRate(1)},"+"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"font-sans text-slate-700"},"Volume:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-60",max:"12",step:"0.5",value:volumeDb,onChange:e=>setVolumeDb(parseFloat(e.target.value)),className:"me-fader-slider w-24"}),/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1 h-5 flex items-center justify-center w-14 font-mono text-[11px]"},volumeDb<=-50?'-inf':volumeDb.toFixed(1)," dB")),/*#__PURE__*/React.createElement("div",{className:`px-2 py-0.5 border font-mono font-bold text-[10px] rounded-sm ${selIsMidi?'bg-purple-950 text-purple-300 border-purple-800':'bg-emerald-950 text-emerald-300 border-emerald-800'}`},selIsMidi?'MIDI':'Audio')),/*#__PURE__*/React.createElement("div",{className:"flex items-stretch gap-2 my-1 flex-1 min-h-0"},/*#__PURE__*/React.createElement("div",{className:"flex-1 bg-[#181818] border border-[#3a3a3a] relative overflow-hidden"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-full block cursor-pointer"})),/*#__PURE__*/React.createElement("div",{className:"w-48 bg-[#181818] border border-[#3a3a3a] p-1.5 font-mono text-[10px] text-slate-300 leading-tight overflow-y-auto shrink-0"},selected?selIsMidi&&!selected.path?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,selected.events," MIDI events"),/*#__PURE__*/React.createElement("div",null,"Length: ",selected.lengthQn," quarter notes"),/*#__PURE__*/React.createElement("div",null,"Length: ",selected.time," (est)"),/*#__PURE__*/React.createElement("div",null,"Ticks per quarter note: ",selected.tpqn)):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Size: ",selected.size_mb!=null?selected.size_mb.toFixed(2)+' MB':'-'),/*#__PURE__*/React.createElement("div",null,"Duration: ",selDur.toFixed(2),"s"),/*#__PURE__*/React.createElement("div",null,"Sample Rate: 44100 Hz"),/*#__PURE__*/React.createElement("div",null,"Type: ",selected.path?selIsMidi?'Local MIDI':selected.kind==='other'?'Local File':'Local Audio':selected.type||'Audio')):/*#__PURE__*/React.createElement("div",null,"No file selected"))),/*#__PURE__*/React.createElement("div",{className:"h-5 bg-[#c0c0c0] border-t border-white flex items-center justify-between text-[11px] font-mono px-1 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1.5 text-slate-900 font-bold"},formatTime(currentTime)," / ",formatTime(selDur))),/*#__PURE__*/React.createElement("div",{className:"text-slate-800 font-bold truncate max-w-[40%]"},selected?selected.name||selected.original_name:'No file selected'),/*#__PURE__*/React.createElement("div",{className:"text-slate-700"},selBpm," bpm x",rate.toFixed(2)))));};const App=()=>{// ── State Definitions ── const[tracks,setTracks]=useState([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);const[appWarningModal,setAppWarningModal]=useState(null);const[bpm,setBpm]=useState(localStorage.getItem('studio_bpm')||'120');const prevBpmRef=useRef(bpm);const[draggedClip,setDraggedClip]=useState(null);const[hoveredTrackId,setHoveredTrackId]=useState(null);// Recalculate item/section/selection durations when BPM changes useEffect(()=>{const oldSpb=prevBpmRef.current?60.0/parseFloat(prevBpmRef.current)*4:null;const bpmVal=parseFloat(bpm)||120;const secondsPerBar=60.0/bpmVal*4;// Recalculate range loop selection to maintain bar count (tempo mode only) if(oldSpb&&selectionFollowsTempo&&selectionStart!==null&&selectionEnd!==null&&selectionEnd>selectionStart){const startBar=selectionStart/oldSpb;const endBar=selectionEnd/oldSpb;if(endBar-startBar>0.01){setSelectionStart(startBar*secondsPerBar);setSelectionEnd(endBar*secondsPerBar);}}prevBpmRef.current=bpm;// Force canvas redraw @@ -428,7 +432,7 @@ const handleHandleDragStart=(e,side)=>{e.preventDefault();e.stopPropagation();co const toggleTrackSoloEvaluate=trackId=>{const wasPlaying=isPlaying;const playingSubTab=subTabs.find(s=>s.isPlaying&&s.type==='PIANO_ROLL');stopAllPlayback();setTracks(prev=>prev.map(t=>t.id===trackId?{...t,solo:!t.solo,muted:false}:t));setTimeout(()=>{const soloCtx=getAudioContext();if(wasPlaying){startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=currentTime;setIsPlaying(true);startTrackPlayback(currentTime);}if(playingSubTab){const prSt=subTabs.find(s=>s.id===playingSubTab.id);if(prSt){const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=soloCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);schedulePianoRollMidi(prSt,prOffset);startSubTabPlayback(prSt,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{isPlaying:true,currentTime:prOffset}):s));}}},60);setTimeout(()=>lucide.createIcons(),50);};const toggleTrackMute=trackId=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,muted:!t.muted}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'MUTE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});setTimeout(()=>lucide.createIcons(),50);};const updateTrackProp=(trackId,props)=>{setTracks(prev=>prev.map(t=>t.id===trackId?{...t,...props}:t));setTimeout(()=>lucide.createIcons(),50);};const toggleTrackDrum=trackId=>{var mt=tracks||[];var tidx=mt.findIndex(function(tr){return tr.id===trackId;});if(tidx<0)tidx=0;setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const becomingPercussion=!t.is_percussion;let newBank=becomingPercussion?128:t._saved_sf_bank!==undefined?t._saved_sf_bank:0;let newProgram=becomingPercussion?0:t._saved_sf_program!==undefined?t._saved_sf_program:0;let newCh=becomingPercussion?9:t._saved_sf_channel!==undefined?t._saved_sf_channel:tidx%16;return{...t,is_percussion:becomingPercussion,soundfont_bank:newBank,instrumentProgram:newProgram,midiChannel:newCh,_saved_sf_bank:becomingPercussion?t.soundfont_bank:t._saved_sf_bank,_saved_sf_program:becomingPercussion?t.instrumentProgram:t._saved_sf_program,_saved_sf_channel:becomingPercussion?t.midiChannel:t._saved_sf_channel,synth_engine:t.synth_engine?{...t.synth_engine,soundfont_bank:newBank,soundfont_program:newProgram}:t.synth_engine};}));setTimeout(()=>lucide.createIcons(),50);};const updateTrackVolumeDb=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,volumeDb:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'VOLUME_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback const nodes=activeTrackNodesRef.current[trackId];if(nodes){const volLinear=val<=-50?0:Math.pow(10,val/20);nodes.gainNode.gain.setValueAtTime(volLinear,getAudioContext().currentTime);}};const updateTrackPan=(trackId,val)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,pan:val}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'PAN_CHANGE',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});// Real-time update during playback const nodes=activeTrackNodesRef.current[trackId];if(nodes){nodes.pannerNode.pan.setValueAtTime(val/100,getAudioContext().currentTime);}};const updateTrackName=(trackId,name)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,name}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'RENAME',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});};const updateTrackColor=(trackId,color)=>{const beforeSnap=captureTrackSnapshot(trackId);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,color}:t));setUndoStack(prev=>{const next=[...prev,{action_type:'RECOLOR',track_id:trackId,timestamp:Date.now(),before_state:beforeSnap,after_state:captureTrackSnapshot(trackId)}];if(next.length>MAX_UNDO)next.shift();return next;});};const updateClipName=(trackId,clipId,newName)=>{setTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const clips=t.clips&&t.clips.length>0?t.clips:[];return{...t,clips:clips.map(c=>c.id===clipId?{...c,name:newName}:c)};}));};// ── MIDI .mid file parser ── -const parseMidiFile=arrayBuffer=>{const data=new Uint8Array(arrayBuffer);if(data.length<14)return null;var pos=0;var read32=function(){var v=data[pos]<<24|data[pos+1]<<16|data[pos+2]<<8|data[pos+3];pos+=4;return v;};var read16=function(){var v=data[pos]<<8|data[pos+1];pos+=2;return v;};var readVLQ=function(){var v=0,b;do{b=data[pos++];v=v<<7|b&0x7f;}while(b&0x80);return v;};var header=String.fromCharCode(data[0],data[1],data[2],data[3]);if(header!=='MThd')return null;pos=8;var fmt=read16();var numTracks=read16();var division=read16();var ticksPerBeat=division&0x8000?480:division||480;var bpm=120;var result=[];for(var t=0;tdata.length)break;var trkId=String.fromCharCode(data[pos],data[pos+1],data[pos+2],data[pos+3]);pos+=4;var trkLen=read32();var endPos=Math.min(pos+trkLen,data.length);if(trkId!=='MTrk'){pos=endPos;continue;}var absTicks=0;var runningStatus=0;var trackName='MIDI Track '+(t+1);var midiNotes=[];var pendingNotes={};var maxAbsTick=0;while(pos=0x80){if(status<0xf0){runningStatus=status;}pos++;}else{status=runningStatus;}var cmd=status>>4;if(cmd===0x9||cmd===0x8){var chan=status&0x0F;var pitch=data[pos++];var vel=pos0){pendingNotes[noteKey]={tick:absTicks,vel:vel};if(absTicks>maxAbsTick)maxAbsTick=absTicks;}else{var pn=pendingNotes[noteKey];if(pn){var durTicks=absTicks-pn.tick;if(durTicks<=0)durTicks=240;midiNotes.push({id:'mn_'+t+'_'+pitch+'_'+pn.tick,pitch:pitch,start_beat:pn.tick/ticksPerBeat,duration_beats:durTicks/ticksPerBeat,velocity:Math.min(1,pn.vel/127)});delete pendingNotes[noteKey];}}}else if(status>=0xc0&&status<0xe0){if(pos=0xe0&&status<0xf0){if(pos+2<=endPos)pos+=2;}else if(status>=0xa0&&status<0xc0){if(pos+2<=endPos)pos+=2;}else if(status>=0xf0&&status<0xf8){if(pos=endPos)break;var metaType=data[pos++];var metaLen=readVLQ();if(metaType===0x03){try{trackName=String.fromCharCode.apply(null,Array.from(data.subarray(pos,pos+metaLen)));}catch(e){}}else if(metaType===0x51&&pos+3<=endPos){bpm=Math.round(60000000/(data[pos]<<16|data[pos+1]<<8|data[pos+2]));}pos+=Math.min(metaLen,endPos-pos);}else{if(pos+2<=endPos)pos+=2;}}catch(e){pos=endPos;}}Object.keys(pendingNotes).forEach(function(k){var pn=pendingNotes[k];var parts=k.split('_');var p=parseInt(parts[1]);var dur=Math.max(240,maxAbsTick-pn.tick);midiNotes.push({id:'mn_'+t+'_'+p+'_'+pn.tick,pitch:p,start_beat:pn.tick/ticksPerBeat,duration_beats:dur/ticksPerBeat,velocity:Math.min(1,pn.vel/127)});});if(midiNotes.length>0){var lastEnd=0;midiNotes.forEach(function(n){var e=(n.start_beat+n.duration_beats)*60/bpm;if(e>lastEnd)lastEnd=e;});result.push({name:trackName,notes:midiNotes,duration:lastEnd||4,startTime:0,id:'midi_'+t+'_'+Date.now()});}pos=endPos;}return result.length>0?result:null;};// ── Load File on Track (with server upload) ── +const parseMidiFile=arrayBuffer=>{const data=new Uint8Array(arrayBuffer);if(data.length<14)return null;var pos=0;var read32=function(){var v=data[pos]<<24|data[pos+1]<<16|data[pos+2]<<8|data[pos+3];pos+=4;return v;};var read16=function(){var v=data[pos]<<8|data[pos+1];pos+=2;return v;};var readVLQ=function(){var v=0,b;do{b=data[pos++];v=v<<7|b&0x7f;}while(b&0x80);return v;};var header=String.fromCharCode(data[0],data[1],data[2],data[3]);if(header!=='MThd')return null;pos=8;var fmt=read16();var numTracks=read16();var division=read16();var ticksPerBeat=division&0x8000?480:division||480;var bpm=120;var result=[];for(var t=0;tdata.length)break;var trkId=String.fromCharCode(data[pos],data[pos+1],data[pos+2],data[pos+3]);pos+=4;var trkLen=read32();var endPos=Math.min(pos+trkLen,data.length);if(trkId!=='MTrk'){pos=endPos;continue;}var absTicks=0;var runningStatus=0;var trackName='MIDI Track '+(t+1);var midiNotes=[];var pendingNotes={};var maxAbsTick=0;while(pos=0x80){if(status<0xf0){runningStatus=status;}pos++;}else{status=runningStatus;}var cmd=status>>4;if(cmd===0x9||cmd===0x8){var chan=status&0x0F;var pitch=data[pos++];var vel=pos0){pendingNotes[noteKey]={tick:absTicks,vel:vel};if(absTicks>maxAbsTick)maxAbsTick=absTicks;}else{var pn=pendingNotes[noteKey];if(pn){var durTicks=absTicks-pn.tick;if(durTicks<=0)durTicks=240;midiNotes.push({id:'mn_'+t+'_'+pitch+'_'+pn.tick,pitch:pitch,start_beat:pn.tick/ticksPerBeat,duration_beats:durTicks/ticksPerBeat,velocity:Math.min(1,pn.vel/127)});delete pendingNotes[noteKey];}}}else if(status>=0xc0&&status<0xe0){if(pos=0xe0&&status<0xf0){if(pos+2<=endPos)pos+=2;}else if(status>=0xa0&&status<0xc0){if(pos+2<=endPos)pos+=2;}else if(status>=0xf0&&status<0xf8){if(pos=endPos)break;var metaType=data[pos++];var metaLen=readVLQ();if(metaType===0x03){try{trackName=String.fromCharCode.apply(null,Array.from(data.subarray(pos,pos+metaLen)));}catch(e){}}else if(metaType===0x51&&pos+3<=endPos){bpm=Math.round(60000000/(data[pos]<<16|data[pos+1]<<8|data[pos+2]));}pos+=Math.min(metaLen,endPos-pos);}else{if(pos+2<=endPos)pos+=2;}}catch(e){pos=endPos;}}Object.keys(pendingNotes).forEach(function(k){var pn=pendingNotes[k];var parts=k.split('_');var p=parseInt(parts[1]);var dur=Math.max(240,maxAbsTick-pn.tick);midiNotes.push({id:'mn_'+t+'_'+p+'_'+pn.tick,pitch:p,start_beat:pn.tick/ticksPerBeat,duration_beats:dur/ticksPerBeat,velocity:Math.min(1,pn.vel/127)});});if(midiNotes.length>0){var lastEnd=0;midiNotes.forEach(function(n){var e=(n.start_beat+n.duration_beats)*60/bpm;if(e>lastEnd)lastEnd=e;});result.push({name:trackName,notes:midiNotes,duration:lastEnd||4,startTime:0,id:'midi_'+t+'_'+Date.now()});}pos=endPos;}return result.length>0?result:null;};window.parseMidiFile=parseMidiFile;// ── Load File on Track (with server upload) ── const loadFileOnTrack=async(trackId,file)=>{if(!file)return;var fileName=file.name||'';var isMidi=/\.mid$|\.midi$/i.test(fileName);showToast(`Đang nạp file ${fileName}...`,'info');try{if(isMidi){var arrayBuffer=await file.arrayBuffer();var midiResult=parseMidiFile(arrayBuffer);if(!midiResult||midiResult.length===0){showToast('Không tìm thấy nốt nhạc trong file MIDI.','error');return;}if(midiResult.length===1){updateActiveTracks(prev=>prev.map(function(t){if(t.id!==trackId)return t;return{...t,name:fileName,midiItems:[midiResult[0]]};}));showToast('Đã tải MIDI: '+fileName,'success');}else{var colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309','#ca8a04','#dc2626','#0891b2'];var firstDone=false;midiResult.forEach(function(midiItem,idx){if(!firstDone){updateActiveTracks(prev=>prev.map(function(t){if(t.id!==trackId)return t;return{...t,name:midiItem.name||fileName,midiItems:[midiItem]};}));firstDone=true;}else{var newId='midi_track_'+Date.now()+'_'+idx;updateActiveTracks(function(prev){var curLen=prev.length;return prev.concat([{id:newId,name:midiItem.name||'MIDI Track '+(idx+1),buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:colors[idx%colors.length],markers:[],serverFileId:null,clips:[],sections:[],midiItems:[midiItem],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);});}});showToast('Đã tải MIDI: '+midiResult.length+' tracks from '+fileName,'success');}return;}// Upload to server uploadToServer(file,trackId);// Decode locally for playback + analyze channels (stereo/mono) const{audioBuffer:decodedBuffer,channelInfo}=await window.SonicAudio.decodeAudioFile(file);setTracks(prev=>prev.map(t=>t.id===trackId?{...t,name:fileName,buffer:decodedBuffer,channelInfo:channelInfo}:t));showToast(`Nạp file thành công: ${fileName} (${channelInfo.label})`,'success');}catch(err){showToast(isMidi?"Lỗi giải mã MIDI.":"Lỗi giải mã âm thanh. Định dạng file không tương thích.",'error');}};// ── Drop MIDI file → create new track(s) ── diff --git a/app/templates/index.html b/app/templates/index.html index 9b46a28..0be1204 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -23,7 +23,7 @@ - +