diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 10a0cc0..f2bd60a 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -1123,6 +1123,16 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str): # tới worklet kịp khi main-thread stall chặn cả 2 hướng (fade nằm trong # browser queue, tới sau khi worklet đã cạn buffer → click vẫn còn), và # fade-in sau stall tạo dip âm lượng định kỳ = vibrato/wow&flutter. + # V34 diag: pump gap log - bridge khong san xuat block (SHM rong) > 30ms + # => fxrt_pump.log (phan biet bridge stall vs Python pump stall). + _plog = os.environ.get("FXRT_PUMP_LOG") + if not _plog: + _ad = os.environ.get("APPDATA") + _plog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_pump.log") if _ad else "" + _pf = open(_plog, "a", buffering=1) if _plog else None + _pump_last = None + if _pf is not None: + _pf.write("%d pump_start\n" % int(_time.time() * 1000)) _last_lat = None try: while True: @@ -1147,6 +1157,13 @@ async def ws_fx_realtime(websocket: WebSocket, session_id: str): # time.sleep dùng high-res waitable timer -> poll đúng 2ms. await asyncio.to_thread(_time.sleep, 0.002) continue + if _pf is not None: + _now = _time.time() + if _pump_last is not None: + _gap = (_now - _pump_last) * 1000.0 + if _gap > 30.0: + _pf.write("%d pump_gap %.0f n=%d\n" % (int(_now * 1000), _gap, len(blks))) + _pump_last = _now for _blk in blks: await websocket.send_bytes(_blk) except Exception: diff --git a/app/core/fx_realtime.py b/app/core/fx_realtime.py index 9c50666..2564aae 100644 --- a/app/core/fx_realtime.py +++ b/app/core/fx_realtime.py @@ -178,7 +178,12 @@ class FxRealtimeSession: "--parent", str(os.getpid())] # FXRT_BRIDGE_LOG=: bat stderr bridge (FxRTPerf) vao file — chan # doan outFull/drop; mac dinh DEVNULL nhu cu. - _blog = os.environ.get("FXRT_BRIDGE_LOG") or "" + # V34 diag: mac dinh ghi FxRTPerf vao fxrt_bridge.log de phan tich + # stall (procMax=bridge/VST cham, outDepth=Python pump cham). + _blog = os.environ.get("FXRT_BRIDGE_LOG") + if not _blog: + _ad = os.environ.get("APPDATA") + _blog = os.path.join(_ad, "SonicForgeDAW", "logs", "fxrt_bridge.log") if _ad else "" self.proc = subprocess.Popen( cmd, stdout=subprocess.DEVNULL, diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index f2169c2..f5ca30e 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -240,6 +240,8 @@ let _cc11Epoch = 0; let _pendingFadeSession = null; const restorePendingFadeSession = () => { if (!_pendingFadeSession) return; + let _bgBefore = -1; + try { if (_pendingFadeSession.bridgeGainNode) _bgBefore = _pendingFadeSession.bridgeGainNode.gain.value; } catch (e) {} try { const t = getAudioContext().currentTime; _pendingFadeSession.fadedGains.forEach(g => { @@ -249,8 +251,64 @@ const restorePendingFadeSession = () => { try { _pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t); _pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0, t); } catch (e) {} } } catch (e) {} + _dbgLog('restore', 'gains=' + _pendingFadeSession.fadedGains.length + ' bg=' + _bgBefore.toFixed(4) + '->1.0'); _pendingFadeSession = null; }; +// V28: timing diagnostics -> %APPDATA%/SonicForgeDAW/logs/dbg.log (Rust dbg_log). +// Fire-and-forget invoke — khong chan main thread; tag+extra ghi kem unix_ms. +const _dbgLog = (tag, extra) => { + try { + if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) { + window.__TAURI__.core.invoke('dbg_log', { tag: tag, extra: extra === undefined ? '' : String(extra) }).catch(function () {}); + } + } catch (e) {} +}; +// V29: expose dbg hook cho BridgeAudioNode (worklet state) + helpers. +window.__dbgLog = _dbgLog; +if (window.PerformanceObserver) { try { new PerformanceObserver((list) => { for (const e of list.getEntries()) _dbgLog('longtask', 'd=' + Math.round(e.duration) + ' s=' + Math.round(e.startTime)); }).observe({ entryTypes: ['longtask'] }); } catch (e) {} } +const _dbgBridgeGain = () => { + try { + if (window.BridgeAudioNode && window.BridgeAudioNode.getOutputNode) { + const _g = window.BridgeAudioNode.getOutputNode(); + if (_g && _g.gain) return 'bg=' + _g.gain.value.toFixed(4); + return 'bg=none'; + } + } catch (e) {} + return 'bg=n/a'; +}; +const _dbgWorkletState = () => { + try { + if (window.BridgeAudioNode && window.BridgeAudioNode.getDiag) { + const _d = window.BridgeAudioNode.getDiag(); + return 'wl=' + (_d.useWorklet ? 'y' : 'n') + ' pre=' + _d.preBufferLen + ' ch=' + _d.chunksLen; + } + } catch (e) {} + return 'wl=n/a'; +}; +window.__dbgBridgeGain = _dbgBridgeGain; +window.__dbgWorkletState = _dbgWorkletState; +// V28: ding fix — midi bat ky (keyboard/pad/timeline) restore bridge gain sau +// stop (gain giu ~0.0001 — V27 restore 800ms khong du cho VSTi release tail dai). +window.__restorePendingFadeSession = restorePendingFadeSession; +// V31: live note (ARM keyboard) — xoa backlog outQueue master FX truoc khi +// note preview render (idle block ~341ms lam ARM lag ~0.3s). +window.__clearMasterFxQueue = function () { + _dbgLastClearMs = performance.now(); + try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: 'live' }); } catch (e) {} +}; +// V32: queueLen master FX worklet hien tai (midi_push log — ARM latency). +window.__fxRtQueueLen = function () { + try { return (fxRt && fxRt.stats) ? fxRt.stats.queueLen : 'n/a'; } catch (e) { return 'n/a'; } +}; +// V28: do jank — play_start timestamp; play_rAF / play_first_audio log delta. +let _dbgPlayStartMs = 0; +let _dbgLastClearMs = 0; +let _dbgFirstAudioPending = false; +let _dbgFirstAudiblePending = false; +// V40: real PDC — freeze playhead theo latency đo được (fx_play_first self-calib). +let _fxRtPlayheadSnapPending = false; +let _fxRtFreezeTimer = null; +let _fxRtFreezeTries = 0; const _nativeTrackUrls = {}; const _nativeTrackPending = {}; // ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─ @@ -1710,13 +1768,14 @@ function encodeFloatWavBlob(pcm, sampleRate, channels) { // worklet → analyser (sau master fader, trước metering/destination — khớp thứ // tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường // chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out. -const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, latencyTotal: 0, fadedIn: false }; +const fxRt = { active: false, starting: false, startToken: 0, sessionId: null, ws: null, node: null, inQueue: [], pumpTimer: null, lastInSent: 0, chainKey: null, latencyTotal: 0, fadedIn: false, pdcCalibMs: 0 }; // Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không // teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop // (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user // stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down / // new-project / export-end → teardown bình thường. let fxRtKeepForRestart = false; +let fxRtWrapRestart = false; // V38: loop wrap restart -- clear_queue mode 'loop' (always-wipe + RESUME_FILL) // V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX // ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown // giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu). @@ -1848,7 +1907,7 @@ async function fxRtStart() { fxRt.sessionId = st.session_id; fxRtGuiPushShm(); try { - await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608231800`); + await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608272300`); if (token !== fxRt.startToken) { fxRt.starting = false; return; } fxRt.node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] }); const u = new URL(API_BASE_URL); @@ -1897,8 +1956,8 @@ async function fxRtStart() { if (fxRt.flushing) return; // V24: output stale trong luc flush — drop const f = new Float32Array(ev.data); const half = f.length / 2; - const l = new Float32Array(half); - const r = new Float32Array(half); + let l = fxRt.outL, r = fxRt.outR; + if (!l || l.length !== half) { l = fxRt.outL = new Float32Array(half); r = fxRt.outR = new Float32Array(half); } for (let i = 0; i < half; i++) { l[i] = f[i * 2]; r[i] = f[i * 2 + 1]; } fxRt.node.port.postMessage({ type: 'out', l, r }); }; @@ -1909,12 +1968,13 @@ async function fxRtStart() { // mẫu (~3.5-4.75ms/block) → đủ 187.5/s (take=1: 5.5ms > budget 5.33ms → // drop → crackle). KHÔNG dùng f.subarray().buffer — nó gửi cả backing // buffer 8192B; slice() copy đúng 2048B. + const fxRtChunks = [0, 1, 2, 3].map(() => new Float32Array(512)); const fxRtSendFrames = (d) => { if (!fxRt.ws || fxRt.ws.readyState !== 1) return; try { const f = new Float32Array(d.data); if (f.length > 512) { - for (let i = 0; i < f.length; i += 512) fxRt.ws.send(f.slice(i, i + 512).buffer); + for (let i = 0; i < f.length; i += 512) { const c = fxRtChunks[i / 512]; c.set(f.subarray(i, i + 512)); fxRt.ws.send(c.buffer); } } else { fxRt.ws.send(d.data); } @@ -1978,6 +2038,28 @@ async function fxRtStart() { // Telemetry worklet (mỗi ~2s): chẩn đoán crackle session thật. fxRt.stats = d; console.debug('[FX RT stats]', d); + // V33: log dbg.log — diff giua 2 dong = so su kien trong ~2s (u=underrun, + // h=hold, f=fade, s=silence, cap=capDrop, fi=fadeIn, q=queueLen). + try { _dbgLog('fx_stats', 'u=' + d.underruns + ' h=' + d.holds + ' f=' + d.fades + ' s=' + d.silences + ' cap=' + d.capDrops + ' fi=' + d.fadeIns + ' q=' + d.queueLen); } catch (e) {} + return; + } + if (d.type === 'dbg') { + // V32: hop dau sau clear_queue (in/out/play) — do latency master FX path. + const _now = performance.now(); + // V40: self-calib PDC — fx_play_first = block đầu tiên thực sự phát ra + // (đã qua FILL) → latency path thực đo được; snap playhead về đúng + // startOffset ngay mẫu đầu tiên nghe được (cả 2 chiều: hết chạy trước + // lẫn hết lag). Native-bridge path dùng _dbgFirstAudiblePending riêng — + // không đụng flag này. + // V41: guard ≥50ms — tại loop wrap (clear_queue mode 'loop' giữ queue) + // worklet phát lại OLD TAIL ~5ms → dbgPlayFirst tiêu thụ → trước đây snap + // playhead SỚM + calib nhiễu ~50ms → display chạy trước → wrap sớm → vấp + // beat 0:0. Pass mới thật ≥ ~80ms → snap/calib chỉ trên event hợp lệ. + if (d.tag === 'fx_play_first' && (_now - _dbgPlayStartMs) >= 50) { + fxRt.pdcCalibMs = Math.max(50, Math.min(3000, _now - _dbgPlayStartMs)); + try { if (window.__fxRtSnapPlayhead) window.__fxRtSnapPlayhead(); } catch (e2) {} + } + _dbgLog('fx_hop_' + d.tag, 'sincePlay=' + Math.round(_now - _dbgPlayStartMs) + ' sinceClear=' + Math.round(_now - _dbgLastClearMs) + ' wlT=' + d.t + ' q=' + (d.q !== undefined ? d.q : '')); return; } if (d.type !== 'in') return; @@ -2068,10 +2150,12 @@ function fxRtStop(silent) { // qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng // để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật // (tag seq trên block) khi cần sample-accurate. -// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 24 block -// (6144) — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 31 -// block ≈ 7936. (FILL 16 trước đây = 5888; giữ đồng bộ với sf-fx-realtime.js) -const FX_RT_ROUNDTRIP_SAMPLES = 7936; +// V40: FILL 24→8 (sf-fx-realtime.js) → RTT = batching 4 block (~7 block @48k +// ≈ 1792) + FILL buffer 8 block (2048) = 15 block ≈ 3840 (~80ms @48k). Giữ +// đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi +// chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s. +const FX_RT_ROUNDTRIP_SAMPLES = 3840; +const FX_RT_START_EST_MS = 450; function fxRtApplyPdc() { const ctx = getAudioContext(); const bus = masterBus; @@ -2228,28 +2312,49 @@ function getAudioContext() { var st = await window.NativeBridgeService.queryStatus(); var connected = !!(st && st.connected); window.SonicMidiRouter.setBridgeConnected(connected); + _dbgLog('bootstrap', 'bridge_sr=' + (st && st.sample_rate) + ' ctx_sr=' + audioCtx.sampleRate + ' connected=' + connected); console.log('[Bridge] bootstrap connected=', connected); if (connected) { - try { - console.log('[Bridge] Requesting restart to match sampleRate:', audioCtx.sampleRate); - await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate', { sampleRate: audioCtx.sampleRate }); - // Bridge restart → mất hết instrument → reset map để reconnect reload. - window.__bridgeLoadedChannels = {}; - window.__bridgeLoadedPaths = {}; - for (var retry = 0; retry < 10; retry++) { - await new Promise(r => setTimeout(r, 100)); - var st2 = await window.NativeBridgeService.queryStatus(); - if (st2 && st2.connected) { - console.log('[Bridge] Reconnected at sampleRate:', audioCtx.sampleRate); - break; + // V27: bridge_status trả sample_rate thật (BridgeSampleRate state) — + // nếu đã khớp audioCtx (48k mặc định) thì KHÔNG restart: trước đây + // luôn kill+respawn → first play sau launch im lặng ~0.5s (chờ bridge + // respawn + restore instrument từ bridge_state.json). + if (st.sample_rate && st.sample_rate !== audioCtx.sampleRate) { + try { + console.log('[Bridge] Requesting restart to match sampleRate:', audioCtx.sampleRate, '(bridge was', st.sample_rate + ')'); + await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate', { sampleRate: audioCtx.sampleRate }); + // Bridge restart → mất hết instrument → reset map để reconnect reload. + window.__bridgeLoadedChannels = {}; + window.__bridgeLoadedPaths = {}; + for (var retry = 0; retry < 10; retry++) { + await new Promise(r => setTimeout(r, 100)); + var st2 = await window.NativeBridgeService.queryStatus(); + if (st2 && st2.connected) { + console.log('[Bridge] Reconnected at sampleRate:', audioCtx.sampleRate); + break; + } } + } catch (err) { + console.warn('[Bridge] restart error:', err); } - } catch (err) { - console.warn('[Bridge] restart error:', err); + } else { + console.log('[Bridge] sampleRate match — skip restart:', audioCtx.sampleRate); } window.BridgeAudioNode.init(audioCtx); window.NativeBridgeService.onAudio(function (l, r) { try { + if (_dbgFirstAudioPending) { + _dbgFirstAudioPending = false; + _dbgLog('play_first_audio', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs)); + } + if (_dbgFirstAudiblePending && l && r) { + var _nz = false; + for (var _i = 0; _i < l.length; _i++) { if (l[_i] !== 0 || r[_i] !== 0) { _nz = true; break; } } + if (_nz) { + _dbgFirstAudiblePending = false; + _dbgLog('play_first_audible', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs)); + } + } window.BridgeAudioNode.onAudio(l, r); } catch (e) { console.warn('[Bridge] audio callback error:', e && e.message || e); @@ -14185,11 +14290,13 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat window.addEventListener('resize', resizeAll); const fftData = new Uint8Array(1024); + let _peakScratch = new Float32Array(2048); function getPeakLevel(analyser) { if (!analyser) return 0; const bufferLength = analyser.fftSize; - const dataArray = new Float32Array(bufferLength); + if (_peakScratch.length < bufferLength) _peakScratch = new Float32Array(bufferLength); + const dataArray = _peakScratch; analyser.getFloatTimeDomainData(dataArray); let maxVal = 0; @@ -14353,15 +14460,17 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat // enough to gauge perceived loudness of the processed master). const outAn = masterBus && masterBus.analyser; if (outAn) { - const outBuf = new Float32Array(outAn.fftSize); + const outLen = outAn.fftSize; + if (_peakScratch.length < outLen) _peakScratch = new Float32Array(outLen); + const outBuf = _peakScratch; outAn.getFloatTimeDomainData(outBuf); let sq = 0; - for (let i = 0; i < outBuf.length; i++) sq += outBuf[i] * outBuf[i]; - const rmsDb = 20 * Math.log10(Math.sqrt(sq / outBuf.length) + 1e-12); + for (let i = 0; i < outLen; i++) sq += outBuf[i] * outBuf[i]; + const rmsDb = 20 * Math.log10(Math.sqrt(sq / outLen) + 1e-12); const rmsEl = document.getElementById('outRmsText'); if (rmsEl) rmsEl.innerText = rmsDb < -90 ? '-inf' : rmsDb.toFixed(1) + ' dB'; if (!lufsAccRef.current) lufsAccRef.current = []; - lufsAccRef.current.push(sq / outBuf.length); + lufsAccRef.current.push(sq / outLen); const accLen = 24; // ~0.4s at 60fps rAF if (lufsAccRef.current.length >= accLen) { const blocks = lufsAccRef.current.splice(0, accLen); @@ -20722,9 +20831,51 @@ const App = () => { const rulerRef = useRef(null); const activeSourcesRef = useRef([]); const activeTrackNodesRef = useRef({}); // { [trackId]: { gainNode, pannerNode } } + const nativeLoopSrcsRef = useRef(new Map()); // V42A: { [trackId+'::'+clipId]: { src, loopStart, loopEnd } } — native WebAudio loop qua wrap const startOffsetTimeRef = useRef(0); const startBufferOffsetRef = useRef(0); const startAudioTimeRef = useRef(0); + // V40: real PDC — freeze playhead theo latency FX path (thay V39 freeze giả + // định 0.31s → under-estimate → playhead chạy trước ~0.66s). __fxRtPlayheadFreeze: + // gọi ở mọi play start (allowCold=true ở startTrackPlayback che cold-start VST + // pre-roll; section play không khởi động FX session → false). __fxRtSnapPlayhead: + // worklet fx_play_first → bỏ freeze đúng mẫu đầu tiên nghe được. Fail-safe: + // session không active trong 1500ms → hủy freeze (đừng đóng băng playhead vô hạn). + window.__fxRtPlayheadFreeze = function (allowCold) { + try { if (_fxRtFreezeTimer) { clearTimeout(_fxRtFreezeTimer); _fxRtFreezeTimer = null; } } catch (e3) {} + // V41: gate theo CHAIN (không chỉ active) — session FX warm (stop giữ warm, + // bypass toggle gọi rebuildMasteringGraph KHÔNG gọi fxRtStop) → fxRt.active vẫn + // true khi bypass → trước đây freeze nhầm → playhead chạy trước → wrap sớm → vấp. + const _fxWillRun = !!(fxRt && fxRtChain().length > 0 && (fxRt.active || allowCold)); + if (!_fxWillRun) return; + const _estMs = (fxRt.pdcCalibMs || FX_RT_START_EST_MS); + const _wasActiveAtFreeze = !!(fxRt && fxRt.active); + startAudioTimeRef.current = getAudioContext().currentTime + _estMs / 1000; + _fxRtPlayheadSnapPending = true; + _fxRtFreezeTries = 0; + // V41: snap xác định bằng timer tại est — loop wrap warm không post fx_play_first + // cho pass mới (old-tail tiêu thụ dbgPlayFirst) nên không có event để snap. Cold + // start (fxRt.active=false) KHÔNG snap sớm: chờ fx_play_first thật (VST load 4s). + _fxRtFreezeTimer = setTimeout(function freezeSnapRetry() { + _fxRtFreezeTimer = null; + if (_wasActiveAtFreeze && _fxRtPlayheadSnapPending) { + _fxRtPlayheadSnapPending = false; + startAudioTimeRef.current = getAudioContext().currentTime; + return; + } + if (fxRt && fxRt.starting) { + if (_fxRtFreezeTries < 20) { _fxRtFreezeTries++; _fxRtFreezeTimer = setTimeout(freezeSnapRetry, 500); } + return; + } + if (!fxRt || !fxRt.active) { startAudioTimeRef.current = getAudioContext().currentTime; } // session fail → bỏ freeze + }, _estMs); + }; + window.__fxRtSnapPlayhead = function () { + if (fxRt && fxRt.active && _fxRtPlayheadSnapPending) { + _fxRtPlayheadSnapPending = false; + startAudioTimeRef.current = getAudioContext().currentTime; + } + }; const animationFrameIdRef = useRef(null); const toastTimeoutRef = useRef(null); const rulerDragStartRef = useRef(null); @@ -23701,6 +23852,13 @@ const App = () => { // để master FX bị teardown giữa lúc section đang phát). if (fxRtWarmIdleTimer) { clearTimeout(fxRtWarmIdleTimer); fxRtWarmIdleTimer = null; } restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain + // V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc + // khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu). + try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {} + // V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong + // cho phia sau; flush ring bridge (V30) khong chaym queue nay. + const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38 + try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {} if (!st.buffer) { st.buffer = context.createBuffer(1, 128, context.sampleRate); } @@ -23805,6 +23963,9 @@ const App = () => { startOffsetTimeRef.current = offsetWallTime; startBufferOffsetRef.current = offsetBuffer; startAudioTimeRef.current = context.currentTime; + // V40: real PDC — freeze playhead theo latency FX path (đo được, không ước + // như V39). Section play không khởi động FX session → allowCold=false. + try { if (window.__fxRtPlayheadFreeze) window.__fxRtPlayheadFreeze(false); } catch (e) { } }; const playMetronomeClick = (time, isDownbeat = false) => { try { @@ -24139,6 +24300,7 @@ const App = () => { const start = Math.min(st.selectionStart, st.selectionEnd); const end = Math.max(st.selectionStart, st.selectionEnd); if (bufferPos >= end) { + fxRtWrapRestart = true; // V38: loop wrap restart fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? { @@ -24166,6 +24328,7 @@ const App = () => { return Math.max(maxEnd * beatSec, 16 * beatSec * 4) + 1.0; })() : st.buffer.duration; if (bufferPos >= effectiveDuration) { + fxRtWrapRestart = true; // V38: loop wrap restart fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); if (st.isLooping) { @@ -24206,6 +24369,7 @@ const App = () => { if (!selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null) { if (selRight > selLeft && updatedTime >= selRight) { if (hasAnySolo || selectionMode === 'local') { + fxRtWrapRestart = true; // V38: loop wrap restart fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); startOffsetTimeRef.current = selLeft; @@ -24219,6 +24383,7 @@ const App = () => { setCurrentTime(selLeft); setIsPlaying(true); } else { + fxRtWrapRestart = true; // V38: loop wrap restart fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); startOffsetTimeRef.current = selLeft; @@ -24241,6 +24406,7 @@ const App = () => { return; } if (isLoopingSelection) { + fxRtWrapRestart = true; // V38: loop wrap restart fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); startOffsetTimeRef.current = 0; @@ -24502,7 +24668,7 @@ const App = () => { fxRtTracks[key] = entry; fxRtGuiPushShm(); try { - await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608231800`); + await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608272300`); const node = new AudioWorkletNode(ctx, 'sf-fx-realtime', { numberOfInputs: 1, numberOfOutputs: 1, outputChannelCount: [2] }); entry.node = node; const u = new URL(API_BASE_URL); @@ -25125,8 +25291,23 @@ const App = () => { const context = getAudioContext(); _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain + // V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc + // khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu). + try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {} + // V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong + // cho phia sau; flush ring bridge (V30) khong chaym queue nay. + const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38 + try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {} + _dbgPlayStartMs = performance.now(); + _dbgFirstAudioPending = true; + _dbgFirstAudiblePending = true; + _dbgLog('play_start', 'offset=' + offsetTime + ' epoch=' + _nativePlayEpoch + ' ' + _dbgBridgeGain() + ' ' + _dbgWorkletState() + ' fxrt=' + (fxRt.active ? 1 : 0) + ' node=' + (fxRt.node ? 1 : 0) + ' q=' + (fxRt.stats ? fxRt.stats.queueLen : 'n/a')); // Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active. try { fxRtStart(); } catch (e) { console.warn('[FX RT] start error:', e); } + // V40: real PDC — freeze playhead theo latency FX path; allowCold=true che + // cold-start VST pre-roll (playhead không chạy trước khi có tiếng). Bỏ V39 + // freeze giả định 0.31s (under-estimate → playhead chạy trước ~0.66s). + try { if (window.__fxRtPlayheadFreeze) window.__fxRtPlayheadFreeze(true); } catch (e) { } // D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng // bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này). if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) { @@ -25138,6 +25319,7 @@ const App = () => { bridgeStopRetryTimerRef.current = null; } try { window.NativeBridgeService.transport('play'); } catch (e) {} + _dbgLog('play_transport', 'offset=' + offsetTime); } // V12: guardPlay (scheduleMidiNoteDispatch) đọc isPlayingRef.current — nếu // để React render set (setIsPlaying(true) sau) thì note-on delay=0 của @@ -25152,9 +25334,19 @@ const App = () => { const allPlayTracks = activeTracksRef.current && activeTracksRef.current.length ? activeTracksRef.current : activeTracks; // Capture items signature at schedule time — updatePlayhead so sánh với // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). - scheduledItemsSigRef.current = buildItemsSignature(allPlayTracks, sessionTabs); + // V28: defer bookkeeping signature — tính đồng bộ chặn main thread trước + // first-audio (góp vào delay onset); signature chỉ so sánh ~10fps trong + // updatePlayhead → trễ 0ms là đủ (re-schedule phát hiện muộn không sao). + setTimeout(function () { try { scheduledItemsSigRef.current = buildItemsSignature(allPlayTracks, sessionTabs); } catch (e) {} }, 0); console.log('[Play] offset=' + offsetTime + ' tracks=' + allPlayTracks.length + ' masterBus=' + !!masterBus + ' dest=' + (context.destination ? 'ok' : 'MISSING') + ' projectEnd=' + (projectEndRef.current || 0).toFixed(2)); const hasSolo = allPlayTracks.some(t => t.solo); + // V42A: native loop region — loop selection hợp lệ → [selLeft, selRight]; + // isLoopingSelection không selection → [0, projectEnd] (project-end wrap 24407). + // Không loop → _loopActive=false → fallback dip V42. + const _loopSel = !selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null && selRight > selLeft; + const _loopLs = _loopSel ? selLeft : 0; + const _loopLe = _loopSel ? selRight : projectEndRef.current; + const _loopActive = isLoopingSelection && _loopLe > _loopLs; allPlayTracks.forEach(track => { const isPlayable = hasSolo ? track.solo : !track.muted; if (!isPlayable) return; @@ -25178,9 +25370,7 @@ const App = () => { const rb = routeBypassFor(track); playNode.route.routeGain.gain.value = rb ? 0 : 1; playNode.route.dryGain.gain.value = rb ? 1 : 0; - console.log('[Bypass] reapply track', track.id, 'contentType=' + (routeBypassFor._lastType ? 'midi' : 'audio'), 'bypass=' + rb, '→ routeGain=' + playNode.route.routeGain.gain.value + '/dryGain=' + playNode.route.dryGain.gain.value); } - console.log('[Play] track', track.id, track.name, 'node ok:', !!(gainNode && pannerNode), 'muted:', track.muted); const clips = track.clips && track.clips.length > 0 ? track.clips : track.buffer ? [{ id: 'default', buffer: track.buffer, @@ -25190,13 +25380,34 @@ const App = () => { }] : []; clips.forEach(clip => { if (!clip.buffer) return; + const clipStart = clip.startTime || 0; + const clipDuration = clip.buffer.duration / (clip.speed || 1.0); + const clipEnd = clipStart + clipDuration; + // V42A: native WebAudio loop — wrap mẫu-chính-xác tại loopEnd, không + // restart → không click (thay dip V42). Chỉ khi loop active + track 1 + // clip + speed=1 + clip phủ đủ vùng loop. Source giữ nguyên qua wrap + // (doHardStop skip khi fxRtWrapRestart) → seam-free. + const _loopable = _loopActive && clips.length === 1 && Math.abs((clip.speed || 1.0) - 1) < 0.0001 && clipStart <= _loopLs + 0.001 && clipEnd >= _loopLe - 0.001; + const _key = track.id + '::' + (clip.id || 'default'); + const _kept = nativeLoopSrcsRef.current.get(_key); + if (_loopable && _kept && _kept.src && _kept.src.playbackState !== 'ended') { + if (Math.abs(_kept.loopStart - _loopLs) < 0.001 && Math.abs(_kept.loopEnd - _loopLe) < 0.001) { + activeSourcesRef.current.push(_kept.src); // giữ source loop đang chạy qua wrap + return; + } + try { _kept.src.stop(); } catch (e) {} + nativeLoopSrcsRef.current.delete(_key); + } const source = context.createBufferSource(); source.buffer = clip.buffer; source.playbackRate.value = clip.speed || 1.0; source.connect(gainNode); - const clipStart = clip.startTime || 0; - const clipDuration = clip.buffer.duration / (clip.speed || 1.0); - const clipEnd = clipStart + clipDuration; + if (_loopable) { + source.loop = true; + source.loopStart = (_loopLs - clipStart) / (clip.speed || 1.0); + source.loopEnd = (_loopLe - clipStart) / (clip.speed || 1.0); + nativeLoopSrcsRef.current.set(_key, { src: source, loopStart: _loopLs, loopEnd: _loopLe }); + } if (offsetTime < clipStart) { const delay = clipStart - offsetTime; source.start(context.currentTime + delay, 0); @@ -25447,15 +25658,34 @@ const App = () => { }); } }); + // V42A: sweep native-loop sources không được schedule pass này (track muted/ + // solo-off) → dừng + xóa — tránh source loop chơi âm thầm mãi. + try { + nativeLoopSrcsRef.current.forEach((v, k) => { + if (!v || !v.src || v.src.playbackState === 'ended') { nativeLoopSrcsRef.current.delete(k); return; } + if (activeSourcesRef.current.indexOf(v.src) === -1) { + try { v.src.stop(); } catch (e) {} + nativeLoopSrcsRef.current.delete(k); + } + }); + } catch (e) {} updateSfRouting(); }; // Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) const startLocalTrackPlayback = (trackId, offsetTime) => { const context = getAudioContext(); + // V42A: local/solo path khong dung native loop — dung + xoa src loop con lai + // tu main play (wrap local giu chung qua doHardStop -> double audio neu khong). + try { + nativeLoopSrcsRef.current.forEach(v => { try { if (v && v.src) v.src.stop(); } catch (e) {} }); + nativeLoopSrcsRef.current.clear(); + } catch (e) {} // V24: solo/local path — gọi fxRtStart (idempotent) để âm solo qua master // chain (trước đây path này bỏ qua → solo không qua master FX). try { fxRtStart(); } catch (e) { console.warn('[FX RT] start error:', e); } + const _wrapMode = fxRtWrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38 + try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: _wrapMode }); } catch (e) {} // V12: parity với startTrackPlayback — loop restart (solo/local) sau // stopAllPlayback(): hủy STOP#2 + báo PLAY (nếu không C++ transportStopped // giữ true → mọi note-on bị drop → solo/local loop pass 2+ câm) + set @@ -25569,6 +25799,12 @@ const App = () => { const context = getAudioContext(); _nativePlayEpoch++; // new play — vô hiệu doHardStop đang fade restorePendingFadeSession(); // V17: huy fade dang treo — replay khong bi dip gain + // V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc + // khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu). + try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {} + // V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong + // cho phia sau; flush ring bridge (V30) khong chaym queue nay. + try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: 'play' }); } catch (e) {} // V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 → // bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại. // Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục). @@ -25713,6 +25949,10 @@ const App = () => { startAudioTimeRef.current = context.currentTime; startTrackPlayback(currentTime); setIsPlaying(true); + // V28: rAF kế tiếp sau setState → đo jank main thread (chênh với play_start). + requestAnimationFrame(function () { + _dbgLog('play_rAF', 'delta_ms=' + Math.round(performance.now() - _dbgPlayStartMs)); + }); } }; handlePlayPauseRef.current = handlePlayPause; @@ -25764,6 +26004,19 @@ const App = () => { }; const stopAllPlayback = (fade) => { try { + if (fxRtWrapRestart && fxRtChain().length === 0 && nativeLoopSrcsRef.current.size === 0 && masterBus && masterBus.muteGain && masterBus.muteGain.gain) { + // V41: dip CHỈ khi bypass (chain rỗng). FX active → seam thật xảy ra sau + // latency (~80ms) tại _now+latency; dip tại _now chỉ tạo dropout 14ms giữa + // đuôi pass cũ (audible) mà không che được seam click. + // V42: rút ngắn dip 14ms → 5ms (2ms xuống + 3ms lên) — đủ che click seam + // nhưng dropout quá ngắn để nghe thấy (V39 14ms tạo dropout rõ). + const _actx = getAudioContext(); const _now = _actx.currentTime; + masterBus.muteGain.gain.cancelScheduledValues(_now); + masterBus.muteGain.gain.setValueAtTime(masterBus.muteGain.gain.value || 1, _now); + masterBus.muteGain.gain.linearRampToValueAtTime(0.0001, _now + 0.002); + masterBus.muteGain.gain.linearRampToValueAtTime(1, _now + 0.005); + } // V39: dip input truoc hard-stop + _dbgLog('stop_start', 'fade=' + !!fade + ' ' + _dbgBridgeGain()); isPlayingRef.current = false; // Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng). try { if (masterBus && masterBus.output && masterBus.output.gain) { const _actx = getAudioContext(); masterBus.output.gain.cancelScheduledValues(_actx.currentTime); masterBus.output.gain.setValueAtTime(1, _actx.currentTime); } } catch (e) { } @@ -25807,7 +26060,9 @@ const App = () => { const capturedSources = activeSourcesRef.current.slice(); const capturedNodes = Object.values(activeTrackNodesRef.current); activeSourcesRef.current = []; - activeTrackNodesRef.current = {}; + // V42A: wrap (fade=false) GIỮ track nodes — source native-loop kept vẫn + // routed (node cũ). User stop (fade=true) mới xóa nodes. + if (fade) activeTrackNodesRef.current = {}; const fadedGains = []; // V16: stop NGAY clip source CHƯA phát (playbackState='scheduled') — nếu // chờ tới doHardStop (sau FADE_MS) chúng đã bắt đầu phát trong lúc fade → @@ -25848,8 +26103,12 @@ const App = () => { const doHardStop = () => { try { const stale = _nativePlayEpoch !== stopEpoch; // play mới bắt đầu trong lúc fade + // V42A: wrap (fxRtWrapRestart, fade=false) — giữ source native-loop đang + // chạy qua restart (seam-free tại loopEnd); stop/seek thật → dừng + xóa. + const _wrapKeepLoop = !stale && fxRtWrapRestart && nativeLoopSrcsRef.current.size > 0; + const _loopSrcs = _wrapKeepLoop ? new Set(Array.from(nativeLoopSrcsRef.current.values(), v => v.src)) : null; capturedSources.forEach(src => { - try { src.stop(); } catch (e) { } + try { if (_loopSrcs && _loopSrcs.has(src)) return; src.stop(); } catch (e) { } }); capturedNodes.forEach(n => { if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} } @@ -25857,16 +26116,42 @@ const App = () => { // V17: restore gain LUON — play moi trong cua so fade (stale) truoc day bo qua // restore -> track/bridge gain ket 0.0001 -> moi am sau bi fade-out dai dang. // Chi skip khi co fade session MOI hon (session nay het pending — session moi tu restore). - if (_pendingFadeSession && _pendingFadeSession.stopEpoch === stopEpoch) { - restorePendingFadeSession(); + if (stale) { + if (_pendingFadeSession && _pendingFadeSession.stopEpoch === stopEpoch) { + restorePendingFadeSession(); + } + return; // play mới đang chạy — không dừng bridge/SF của nó } - if (stale) return; // play mới đang chạy — không dừng bridge/SF của nó + if (!_wrapKeepLoop) nativeLoopSrcsRef.current.clear(); // V42A: stop/seek thật — quên native loop + // V26: user stop (fade) — dừng master FX tail: xóa outQueue worklet + flush + // engine ring (audio đã xử lí còn trong pipeline phát tiếp ~0.3-0.5s sau stop + // = "ding"). Worklet tự fade-out 21ms (hết click) rồi báo main flush engine; + // session giữ warm (play kế tiếp FX ngay, không reload VST). + try { + if (fade && fxRt && fxRt.node && fxRt.active) { + fxRt.node.port.postMessage({ type: 'flush_for_stop' }); + } + } catch (e) { console.warn('[Stop] fx flush error:', e); } stopMidiCapture(); stopAllNativeSfNotes(); stopAllNativeVstNotes(); // D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết // âm ngân tức thì; A13 C++ flush note-off). if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) { try { window.NativeBridgeService.transport('stop'); } catch (e) {} + _dbgLog('stop_transport', 'epoch=' + stopEpoch); + // V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay. + try { + if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive && window.SonicMidiRouter.isBridgeActive()) { + Object.keys(trackMidiChannelsRef.current || {}).forEach(function (_tid) { + var _ch = trackMidiChannelsRef.current[_tid]; + window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 123, 0, 0, 0, 0, false); + window.NativeBridgeService.dispatchMidiEvent('CC', _ch, 120, 0, 0, 0, 0, false); + }); + } + } catch (e) { console.warn('[Stop] midi panic error:', e); } + // V27: xóa audio còn buffer trong pipeline (preBuffer/chunks + worklet + // ring) — nếu không, âm trước stop còn trong ring phát tiếp sau fade. + try { if (window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) { console.warn('[Stop] bridge flush error:', e); } // V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync // qua React effect (cham) van bay toi bridge sau STOP → retrigger am // treo. V10: timer lưu ref — startTrackPlayback (loop restart) hủy @@ -25882,6 +26167,12 @@ const App = () => { // Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck) if (window.SonicSF.panic) { try { window.SonicSF.panic(); } catch (e) { console.warn('[Stop] panic error:', e); } } } + // V28: KHÔNG auto-restore gain sau stop — VSTi release tail có thể dài + // hơn 800ms (V27 vẫn còn "ding"). Gain giữ ~0.0001 vô thời hạn; restore + // khi: play mới (startTrackPlayback entry) hoặc midi bất kỳ (pushEvent → + // __restorePendingFadeSession — keyboard/pad preview). Tail vẫn render + // nhưng im lặng qua bridge gain. + _dbgLog('stop_hard', 'epoch=' + stopEpoch + ' ' + _dbgBridgeGain()); } catch (e) { console.warn('[Stop] stopAllPlayback error:', e); } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 4477b9a..0746018 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -60,7 +60,17 @@ let _nativePlayEpoch=0;// CC11 timers epoch rieng: chi tang o stopAllPlayback (c // KHONG co CC11 realtime (main session co vi startTrackPlayback chi ++ 1 lan). let _cc11Epoch=0;// V17: fade session dang treo (gain ramp FADE_MS) — restore khi play moi / // stop moi / doHardStop de gain KHONG bi ket ~0.0001 (root cause "am bi fade out"). -let _pendingFadeSession=null;const restorePendingFadeSession=()=>{if(!_pendingFadeSession)return;try{const t=getAudioContext().currentTime;_pendingFadeSession.fadedGains.forEach(g=>{try{g.node.gain.cancelScheduledValues(t);g.node.gain.setValueAtTime(g.prev,t);}catch(e){}});if(_pendingFadeSession.bridgeGainNode){try{_pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t);_pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0,t);}catch(e){}}}catch(e){}_pendingFadeSession=null;};const _nativeTrackUrls={};const _nativeTrackPending={};// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─ +let _pendingFadeSession=null;const restorePendingFadeSession=()=>{if(!_pendingFadeSession)return;let _bgBefore=-1;try{if(_pendingFadeSession.bridgeGainNode)_bgBefore=_pendingFadeSession.bridgeGainNode.gain.value;}catch(e){}try{const t=getAudioContext().currentTime;_pendingFadeSession.fadedGains.forEach(g=>{try{g.node.gain.cancelScheduledValues(t);g.node.gain.setValueAtTime(g.prev,t);}catch(e){}});if(_pendingFadeSession.bridgeGainNode){try{_pendingFadeSession.bridgeGainNode.gain.cancelScheduledValues(t);_pendingFadeSession.bridgeGainNode.gain.setValueAtTime(1.0,t);}catch(e){}}}catch(e){}_dbgLog('restore','gains='+_pendingFadeSession.fadedGains.length+' bg='+_bgBefore.toFixed(4)+'->1.0');_pendingFadeSession=null;};// V28: timing diagnostics -> %APPDATA%/SonicForgeDAW/logs/dbg.log (Rust dbg_log). +// Fire-and-forget invoke — khong chan main thread; tag+extra ghi kem unix_ms. +const _dbgLog=(tag,extra)=>{try{if(window.__TAURI__&&window.__TAURI__.core&&window.__TAURI__.core.invoke){window.__TAURI__.core.invoke('dbg_log',{tag:tag,extra:extra===undefined?'':String(extra)}).catch(function(){});}}catch(e){}};// V29: expose dbg hook cho BridgeAudioNode (worklet state) + helpers. +window.__dbgLog=_dbgLog;if(window.PerformanceObserver){try{new PerformanceObserver(list=>{for(const e of list.getEntries())_dbgLog('longtask','d='+Math.round(e.duration)+' s='+Math.round(e.startTime));}).observe({entryTypes:['longtask']});}catch(e){}}const _dbgBridgeGain=()=>{try{if(window.BridgeAudioNode&&window.BridgeAudioNode.getOutputNode){const _g=window.BridgeAudioNode.getOutputNode();if(_g&&_g.gain)return'bg='+_g.gain.value.toFixed(4);return'bg=none';}}catch(e){}return'bg=n/a';};const _dbgWorkletState=()=>{try{if(window.BridgeAudioNode&&window.BridgeAudioNode.getDiag){const _d=window.BridgeAudioNode.getDiag();return'wl='+(_d.useWorklet?'y':'n')+' pre='+_d.preBufferLen+' ch='+_d.chunksLen;}}catch(e){}return'wl=n/a';};window.__dbgBridgeGain=_dbgBridgeGain;window.__dbgWorkletState=_dbgWorkletState;// V28: ding fix — midi bat ky (keyboard/pad/timeline) restore bridge gain sau +// stop (gain giu ~0.0001 — V27 restore 800ms khong du cho VSTi release tail dai). +window.__restorePendingFadeSession=restorePendingFadeSession;// V31: live note (ARM keyboard) — xoa backlog outQueue master FX truoc khi +// note preview render (idle block ~341ms lam ARM lag ~0.3s). +window.__clearMasterFxQueue=function(){_dbgLastClearMs=performance.now();try{if(fxRt&&fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'clear_queue',mode:'live'});}catch(e){}};// V32: queueLen master FX worklet hien tai (midi_push log — ARM latency). +window.__fxRtQueueLen=function(){try{return fxRt&&fxRt.stats?fxRt.stats.queueLen:'n/a';}catch(e){return'n/a';}};// V28: do jank — play_start timestamp; play_rAF / play_first_audio log delta. +let _dbgPlayStartMs=0;let _dbgLastClearMs=0;let _dbgFirstAudioPending=false;let _dbgFirstAudiblePending=false;// V40: real PDC — freeze playhead theo latency đo được (fx_play_first self-calib). +let _fxRtPlayheadSnapPending=false;let _fxRtFreezeTimer=null;let _fxRtFreezeTries=0;const _nativeTrackUrls={};const _nativeTrackPending={};// ── MIDI overlap cleanup + polyphony gain (spec midi_polyphony_loudness) ─ // Dedup: note cùng pitch + cùng start_beat → gộp 1, giữ velocity cao hơn // (tránh synth instantiate nhiều voice cùng pitch — spec §II.5). // Legato cleanup: note trước cùng pitch kéo dài qua start_beat note sau → cắt @@ -291,12 +301,13 @@ const len=Math.floor(pcm.length/channels);const bps=4;const dataBytes=len*bps*ch // worklet → analyser (sau master fader, trước metering/destination — khớp thứ // tự offline export). Track: mỗi track 1 session/worklet splice vào CẢ 2 đường // chain (audio-clips + MIDI/SF) — mọi luồng track qua bridge trước main out. -const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,latencyTotal:0,fadedIn:false};// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không +const fxRt={active:false,starting:false,startToken:0,sessionId:null,ws:null,node:null,inQueue:[],pumpTimer:null,lastInSent:0,chainKey:null,latencyTotal:0,fadedIn:false,pdcCalibMs:0};// Loop restart/seek/reschedule: stopAllPlayback giữ session FX realtime (không // teardown) → fxRtStart early-return → KHÔNG reload VST ~4s mỗi pass loop // (hết bypass rồi mới load lại). Chỉ set TRƯỚC nhánh restart/resume; user // stop thật (fade=true) giữ master FX warm + idle timeout (V21); bridge-down / // new-project / export-end → teardown bình thường. -let fxRtKeepForRestart=false;// V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX +let fxRtKeepForRestart=false;let fxRtWrapRestart=false;// V38: loop wrap restart -- clear_queue mode 'loop' (always-wipe + RESUME_FILL) +// V21: user stop (fade=true) giữ master FX session warm — play kế tiếp có FX // ngay, không chờ reload VST ~4s. Idle timeout: ngừng chơi lâu → teardown // giải phóng CPU/VST (chấp nhận ~4s delay ở play sau khi idle lâu). const FX_RT_WARM_IDLE_MS=120000;let fxRtWarmIdleTimer=null;// Native VSTi GUI open/reload đang chạy (C++ reloadForGUI mute 1-3s khi mở @@ -328,7 +339,7 @@ else if(fxRt.chainKey!==ck){fxRtSetChain(chain);fxRt.chainKey=ck;return;}else re // node → bypass vô hiệu. Mỗi fxRtStop() bump startToken → mọi checkpoint // dưới đây thấy mismatch → hủy session vừa tạo, không nối node. const token=++fxRt.startToken;fxRt.starting=true;const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet||!masterBus||!masterBus.output||!masterBus.analyser||!masterBus.masterFxGain||!masterBus.masterFxDryGain)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(window.currentMasteringSettings||{}).vstBypass}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT] start fail:',e);showToast('Không khởi động VST FX realtime: '+(e.message||e),'warning');fxRt.starting=false;return;}if(token!==fxRt.startToken){// Bypass/stop giữa lúc engine đang load chain — hủy session vừa tạo. -console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608231800`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong +console.warn('[FX RT] start cancelled mid-flight — session stopped');try{window.SonicAPI.fxRealtimeStop({session_id:st.session_id}).catch(()=>{});}catch(e2){}fxRt.starting=false;return;}fxRt.sessionId=st.session_id;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608272300`);if(token!==fxRt.startToken){fxRt.starting=false;return;}fxRt.node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms: Chromium thu ::1 truoc (Windows khong // co listener IPv6 -> SYN hang ~2s) roi moi fallback 127.0.0.1. Dung // thang 127.0.0.1 (loopback) -> WS open ~5ms. Server chap nhan loopback. const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const wsUrl=`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`;const ws=new WebSocket(wsUrl);fxRt.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{if(token!==fxRt.startToken){try{ws.close();}catch(e){}fxRt.starting=false;return;}fxRt.active=true;fxRt.flushing=false;fxRt.starting=false;fxRt.inQueue=[];// bỏ audio stale sản xuất trước khi WS open (~4s load VST) — hết lag khởi đầu @@ -339,12 +350,12 @@ if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='lat if(fxRt.fadedIn)fxRtApplyPdc();}else if(d&&d.cmd==='flush_done'){// V24: engine da flush ring xong — mo lai nhan output + bao worklet // (worklet drop block cho ack nay; mat ack -> fallback timer nha). fxRt.flushing=false;if(fxRt.flushTimer){clearTimeout(fxRt.flushTimer);fxRt.flushTimer=null;}if(fxRt.node)fxRt.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!fxRt.node)return;if(fxRt.flushing)return;// V24: output stale trong luc flush — drop -const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;ifxRtStop(true);ws.onerror=()=>{try{ws.close();}catch(e){}};// Worklet gom 4 block (1024 mẫu) vào 1 frame → tách 4 WS frame 512 floats +const f=new Float32Array(ev.data);const half=f.length/2;let l=fxRt.outL,r=fxRt.outR;if(!l||l.length!==half){l=fxRt.outL=new Float32Array(half);r=fxRt.outR=new Float32Array(half);}for(let i=0;ifxRtStop(true);ws.onerror=()=>{try{ws.close();}catch(e){}};// Worklet gom 4 block (1024 mẫu) vào 1 frame → tách 4 WS frame 512 floats // ở đây. Bridge VST3 nhận input dồn → take=4 → batch 1 call process 1024 // mẫu (~3.5-4.75ms/block) → đủ 187.5/s (take=1: 5.5ms > budget 5.33ms → // drop → crackle). KHÔNG dùng f.subarray().buffer — nó gửi cả backing // buffer 8192B; slice() copy đúng 2048B. -const fxRtSendFrames=d=>{if(!fxRt.ws||fxRt.ws.readyState!==1)return;try{const f=new Float32Array(d.data);if(f.length>512){for(let i=0;i{if(!fxRt.inQueue.length)return;// Không còn dùng (gửi ngay theo đồng hồ audio — xem onmessage 'in'). +const fxRtChunks=[0,1,2,3].map(()=>new Float32Array(512));const fxRtSendFrames=d=>{if(!fxRt.ws||fxRt.ws.readyState!==1)return;try{const f=new Float32Array(d.data);if(f.length>512){for(let i=0;i{if(!fxRt.inQueue.length)return;// Không còn dùng (gửi ngay theo đồng hồ audio — xem onmessage 'in'). // Giữ tham chiếu phòng khi track path còn dùng pattern cũ. const now=Date.now();while(fxRt.inQueue.length&&now-fxRt.lastInSent>=FXRT_BURST_MS){fxRt.lastInSent=now;fxRtSendFrames(fxRt.inQueue.shift());}if(fxRt.inQueue.length){if(fxRt.pumpTimer)clearTimeout(fxRt.pumpTimer);fxRt.pumpTimer=setTimeout(fxRtInPump,Math.max(1,FXRT_BURST_MS-(now-fxRt.lastInSent)));}else if(fxRt.pumpTimer){clearTimeout(fxRt.pumpTimer);fxRt.pumpTimer=null;}};fxRt.node.port.onmessage=ev=>{const d=ev.data;if(!d||!fxRt.ws)return;if(d.type==='started'){// Worklet bat dau phat (sau FILL) -> crossfade dry->fx muot. Token guard // chong race: neu stop/restart xay ra trong luc cho, bo qua. @@ -354,7 +365,19 @@ setTimeout(()=>fxRtApplyPdc(),250);},30);return;}if(d.type==='flush'){// V24: wo // stale) → het lag ~1s sau stall. Fallback timer phong ack mat (WS/ // engine loi) → tu nha sau 500ms, worklet nhan block moi binh thuong. if(fxRt.ws&&fxRt.ws.readyState===1){try{fxRt.ws.send(JSON.stringify({cmd:'flush'}));}catch(e){}}fxRt.flushing=true;if(fxRt.flushTimer)clearTimeout(fxRt.flushTimer);fxRt.flushTimer=setTimeout(()=>{fxRt.flushTimer=null;fxRt.flushing=false;if(fxRt.node)fxRt.node.port.postMessage({type:'flush_done'});},500);return;}if(d.type==='set_param'){try{fxRt.ws.send(JSON.stringify({cmd:'set_param',slot:d.slot,key:d.key,value:d.value}));}catch(e){}return;}if(d.type==='stats'){// Telemetry worklet (mỗi ~2s): chẩn đoán crackle session thật. -fxRt.stats=d;console.debug('[FX RT stats]',d);return;}if(d.type!=='in')return;// Gửi NGAY, không pace setTimeout: input pacing = đồng hồ audio worklet +fxRt.stats=d;console.debug('[FX RT stats]',d);// V33: log dbg.log — diff giua 2 dong = so su kien trong ~2s (u=underrun, +// h=hold, f=fade, s=silence, cap=capDrop, fi=fadeIn, q=queueLen). +try{_dbgLog('fx_stats','u='+d.underruns+' h='+d.holds+' f='+d.fades+' s='+d.silences+' cap='+d.capDrops+' fi='+d.fadeIns+' q='+d.queueLen);}catch(e){}return;}if(d.type==='dbg'){// V32: hop dau sau clear_queue (in/out/play) — do latency master FX path. +const _now=performance.now();// V40: self-calib PDC — fx_play_first = block đầu tiên thực sự phát ra +// (đã qua FILL) → latency path thực đo được; snap playhead về đúng +// startOffset ngay mẫu đầu tiên nghe được (cả 2 chiều: hết chạy trước +// lẫn hết lag). Native-bridge path dùng _dbgFirstAudiblePending riêng — +// không đụng flag này. +// V41: guard ≥50ms — tại loop wrap (clear_queue mode 'loop' giữ queue) +// worklet phát lại OLD TAIL ~5ms → dbgPlayFirst tiêu thụ → trước đây snap +// playhead SỚM + calib nhiễu ~50ms → display chạy trước → wrap sớm → vấp +// beat 0:0. Pass mới thật ≥ ~80ms → snap/calib chỉ trên event hợp lệ. +if(d.tag==='fx_play_first'&&_now-_dbgPlayStartMs>=50){fxRt.pdcCalibMs=Math.max(50,Math.min(3000,_now-_dbgPlayStartMs));try{if(window.__fxRtSnapPlayhead)window.__fxRtSnapPlayhead();}catch(e2){}}_dbgLog('fx_hop_'+d.tag,'sincePlay='+Math.round(_now-_dbgPlayStartMs)+' sinceClear='+Math.round(_now-_dbgLastClearMs)+' wlT='+d.t+' q='+(d.q!==undefined?d.q:''));return;}if(d.type!=='in')return;// Gửi NGAY, không pace setTimeout: input pacing = đồng hồ audio worklet // (1 frame/21.33ms), không phải timer main-thread. Timer bận/trễ → input // gửi trễ → engine/bridge chỉ sản xuất output khi có input → cushion cạn // dần → crackle sau 3-4s (đã xác nhận với chain RỖNG). Burst sau stall: @@ -385,10 +408,11 @@ if(!_fxStopNode)fxRtApplyPdc();if(sid){try{window.SonicAPI.fxRealtimeStop({sessi // qua worklet → lệch pha với mastered. fxRtApplyPdc trễ dry path đúng lượng // để align. FX_RT_ROUNDTRIP_SAMPLES là ước lượng — ponytail: đo RTT thật // (tag seq trên block) khi cần sample-accurate. -// RTT = batching 4 block (~7 block @48k ≈ 1792) + FILL buffer 24 block -// (6144) — worklet chỉ phát khi queue ≥ FILL → latency tĩnh thêm FILL → 31 -// block ≈ 7936. (FILL 16 trước đây = 5888; giữ đồng bộ với sf-fx-realtime.js) -const FX_RT_ROUNDTRIP_SAMPLES=7936;function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;const l=fxRt&&fxRt.active?FX_RT_ROUNDTRIP_SAMPLES+(fxRt.latencyTotal||0):0;try{bus.pdcDelay.delayTime.setTargetAtTime(l/(ctx.sampleRate||44100),ctx.currentTime,0.05);}catch(e){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi +// V40: FILL 24→8 (sf-fx-realtime.js) → RTT = batching 4 block (~7 block @48k +// ≈ 1792) + FILL buffer 8 block (2048) = 15 block ≈ 3840 (~80ms @48k). Giữ +// đồng bộ với sf-fx-realtime.js. FX_RT_START_EST_MS: ước lượng khởi đầu khi +// chưa có phép đo (fx_play_first) — dùng cho freeze playhead thay V39 0.31s. +const FX_RT_ROUNDTRIP_SAMPLES=3840;const FX_RT_START_EST_MS=450;function fxRtApplyPdc(){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.pdcDelay)return;const l=fxRt&&fxRt.active?FX_RT_ROUNDTRIP_SAMPLES+(fxRt.latencyTotal||0):0;try{bus.pdcDelay.delayTime.setTargetAtTime(l/(ctx.sampleRate||44100),ctx.currentTime,0.05);}catch(e){}}// Crossfade master-FX switch: ramp masterFxGain/masterFxDryGain thay vi // hard-rewire (click). Hai gain nay duoc tao trong initMasterBus. function fxRtCrossfade(toFx,token){const ctx=getAudioContext();const bus=masterBus;if(!ctx||!bus||!bus.masterFxGain||!bus.masterFxDryGain)return;if(token!==undefined&&token!==fxRt.startToken)return;const tc=0.025;try{bus.masterFxGain.gain.setTargetAtTime(toFx?1:0,ctx.currentTime,tc);bus.masterFxDryGain.gain.setTargetAtTime(toFx?0:1,ctx.currentTime,tc);}catch(e){}}// ── Realtime VST FX per-track (SHM bridge) — mọi luồng track qua bridge ───── // Mỗi track: session riêng (1 WS/SHM) cho TỪNG đường chain có âm — audio-clips @@ -422,8 +446,12 @@ try{masterBus.inputAnalyser.disconnect();masterBus.maximizerCompressor.disconnec // ("BiquadFilterNode: state is bad") and can leave the master routing broken // → global silence. The React effect applies it once per settings change. if(window.SonicSF&&window.SonicSF.init){window.SonicSF.init(audioCtx);}// C6: bootstrap native bridge 1 lần — query status, nối audio sink + router. -if(window.NativeBridgeService&&window.SonicMidiRouter&&!window.__bridgeBootstrapped){window.__bridgeBootstrapped=true;window.SonicMidiRouter.onFallback=function(cmd,ch,pitch,vel){if(!window.SonicSF)return;if(cmd==='PANIC'){if(window.SonicSF.stopAll)window.SonicSF.stopAll();return;}if(cmd==='NOTE_ON')window.SonicSF.playNote(pitch,vel,undefined,undefined,undefined,undefined,ch);else window.SonicSF.stopNote(ch,pitch);};(async function(){try{var st=await window.NativeBridgeService.queryStatus();var connected=!!(st&&st.connected);window.SonicMidiRouter.setBridgeConnected(connected);console.log('[Bridge] bootstrap connected=',connected);if(connected){try{console.log('[Bridge] Requesting restart to match sampleRate:',audioCtx.sampleRate);await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate',{sampleRate:audioCtx.sampleRate});// Bridge restart → mất hết instrument → reset map để reconnect reload. -window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){try{window.BridgeAudioNode.onAudio(l,r);}catch(e){console.warn('[Bridge] audio callback error:',e&&e.message||e);}});// A14: plugin MIDI output -> route notes to the selected track +if(window.NativeBridgeService&&window.SonicMidiRouter&&!window.__bridgeBootstrapped){window.__bridgeBootstrapped=true;window.SonicMidiRouter.onFallback=function(cmd,ch,pitch,vel){if(!window.SonicSF)return;if(cmd==='PANIC'){if(window.SonicSF.stopAll)window.SonicSF.stopAll();return;}if(cmd==='NOTE_ON')window.SonicSF.playNote(pitch,vel,undefined,undefined,undefined,undefined,ch);else window.SonicSF.stopNote(ch,pitch);};(async function(){try{var st=await window.NativeBridgeService.queryStatus();var connected=!!(st&&st.connected);window.SonicMidiRouter.setBridgeConnected(connected);_dbgLog('bootstrap','bridge_sr='+(st&&st.sample_rate)+' ctx_sr='+audioCtx.sampleRate+' connected='+connected);console.log('[Bridge] bootstrap connected=',connected);if(connected){// V27: bridge_status trả sample_rate thật (BridgeSampleRate state) — +// nếu đã khớp audioCtx (48k mặc định) thì KHÔNG restart: trước đây +// luôn kill+respawn → first play sau launch im lặng ~0.5s (chờ bridge +// respawn + restore instrument từ bridge_state.json). +if(st.sample_rate&&st.sample_rate!==audioCtx.sampleRate){try{console.log('[Bridge] Requesting restart to match sampleRate:',audioCtx.sampleRate,'(bridge was',st.sample_rate+')');await window.__TAURI__.core.invoke('restart_bridge_with_sample_rate',{sampleRate:audioCtx.sampleRate});// Bridge restart → mất hết instrument → reset map để reconnect reload. +window.__bridgeLoadedChannels={};window.__bridgeLoadedPaths={};for(var retry=0;retry<10;retry++){await new Promise(r=>setTimeout(r,100));var st2=await window.NativeBridgeService.queryStatus();if(st2&&st2.connected){console.log('[Bridge] Reconnected at sampleRate:',audioCtx.sampleRate);break;}}}catch(err){console.warn('[Bridge] restart error:',err);}}else{console.log('[Bridge] sampleRate match — skip restart:',audioCtx.sampleRate);}window.BridgeAudioNode.init(audioCtx);window.NativeBridgeService.onAudio(function(l,r){try{if(_dbgFirstAudioPending){_dbgFirstAudioPending=false;_dbgLog('play_first_audio','delta_ms='+Math.round(performance.now()-_dbgPlayStartMs));}if(_dbgFirstAudiblePending&&l&&r){var _nz=false;for(var _i=0;_i route notes to the selected track // (Scaler2/Instacomposer chords play the selected track's // instrument). Feedback guard: skip events whose source channel // is the selected track's own channel (would loop generator into @@ -450,7 +478,7 @@ if(window.__setTrackBypass)window.__setTrackBypass(track.id,'audio',next);else i const RefCompareModal=({open,onClose})=>{const[path,setPath]=React.useState('');const[refStats,setRefStats]=React.useState(null);const[masterStats,setMasterStats]=React.useState(null);const[busy,setBusy]=React.useState(false);const[ab,setAb]=React.useState('A');const audioRef=React.useRef(null);const fileInputRef=React.useRef(null);const stopRef=()=>{if(audioRef.current){try{audioRef.current.pause();}catch(e){}if(audioRef.current._objUrl){try{URL.revokeObjectURL(audioRef.current._objUrl);}catch(e){}}audioRef.current=null;}};React.useEffect(()=>()=>stopRef(),[]);React.useEffect(()=>{if(open){setMasterStats(window.__getLastMasterStats&&window.__getLastMasterStats()||null);setRefStats(null);setAb('A');stopRef();}// eslint-disable-next-line react-hooks/exhaustive-deps },[open]);const analyze=async src=>{setBusy(true);try{const res=await window.SonicAPI.referenceAnalyze(src);setRefStats(res);setMasterStats(window.__getLastMasterStats&&window.__getLastMasterStats()||null);}catch(e){window.showToast&&window.showToast('Analyze loi: '+(e.message||e),'error');}finally{setBusy(false);}};const onPickFile=f=>{if(!f)return;const fd=new FormData();fd.append('file',f);setBusy(true);const token=localStorage.getItem('sonic_token')||'';fetch((window.API_BASE_URL||'')+'/api/v1/audio/upload',{method:'POST',headers:token?{'Authorization':'Bearer '+token}:{},body:fd}).then(r=>r.json().then(d=>({ok:r.ok,d}))).then(({ok,d})=>{if(!ok)throw new Error(d.detail||'Upload loi');return analyze({file_id:d.file_id});}).catch(e=>{window.showToast&&window.showToast('Upload loi: '+(e.message||e),'error');setBusy(false);});};const playB=async()=>{if(!refStats)return;stopRef();try{// media/file va download deu can token -> fetch (Authorization header) // roi play qua blob URL (Audio element khong gui duoc header). -const token=localStorage.getItem('sonic_token')||'';const resp=await fetch((window.API_BASE_URL||'')+refStats.url,{headers:token?{'Authorization':'Bearer '+token}:{}});if(!resp.ok)throw new Error('HTTP '+resp.status);const blob=await resp.blob();const objUrl=URL.createObjectURL(blob);const au=new Audio(objUrl);au._objUrl=objUrl;audioRef.current=au;au.loop=true;au.play().then(()=>setAb('B')).catch(()=>{});}catch(e){window.showToast&&window.showToast('Play ref loi: '+(e.message||e),'error');}};const fmt=(v,suf)=>v===null||v===undefined?'--':v+suf;const dlt=(a,b)=>a===null||a===undefined||b===null||b===undefined?'--':(a-b>0?'+':'')+(a-b).toFixed(1);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[115] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-md bg-slate-900 border border-slate-700 rounded-2xl p-4 space-y-3 shadow-2xl",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-white tracking-wider"},"A/B REFERENCE"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-500 hover:text-slate-300"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},/*#__PURE__*/React.createElement("input",{value:path,onChange:e=>setPath(e.target.value),placeholder:"Local path (media/browse) hoac file_id",className:"flex-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300 focus:outline-none"}),/*#__PURE__*/React.createElement("button",{onClick:()=>path&&analyze({path}),disabled:busy,className:"px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-xs font-bold"},busy?'...':'Analyze')),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-500 text-center"},"- hoac -"),/*#__PURE__*/React.createElement("button",{onClick:()=>fileInputRef.current&&fileInputRef.current.click(),className:"w-full py-1.5 bg-slate-800 hover:bg-slate-700 text-zinc-200 rounded text-xs font-bold border border-slate-700"},"Upload reference file"),/*#__PURE__*/React.createElement("input",{ref:fileInputRef,type:"file",accept:"audio/*,.wav,.mp3,.flac,.ogg,.aiff,.m4a,.aac,.opus",className:"hidden",onChange:e=>{const f=e.target.files&&e.target.files[0];onPickFile(f);e.target.value='';}}),(refStats||masterStats)&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-1.5 text-[10px] text-center"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"METER"),/*#__PURE__*/React.createElement("div",{className:"text-emerald-400 font-bold"},"LUFS"),/*#__PURE__*/React.createElement("div",{className:"text-slate-300 font-bold"},"dBTP"),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},"Tone")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"MASTER"),/*#__PURE__*/React.createElement("div",{className:"text-cyan-300 font-bold"},fmt(masterStats&&masterStats.lufs,'')),/*#__PURE__*/React.createElement("div",{className:"text-cyan-300 font-bold"},fmt(masterStats&&masterStats.true_peak_db,'')),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},"-")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"REF"),/*#__PURE__*/React.createElement("div",{className:"text-fuchsia-300 font-bold"},fmt(refStats&&refStats.lufs,'')),/*#__PURE__*/React.createElement("div",{className:"text-fuchsia-300 font-bold"},fmt(refStats&&refStats.true_peak_db,'')),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},fmt(refStats&&refStats.centroid_hz,'Hz')))),refStats&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400 text-center"},"Delta: LUFS ",dlt(masterStats&&masterStats.lufs,refStats.lufs)," / dBTP ",dlt(masterStats&&masterStats.true_peak_db,refStats.true_peak_db)," \xA0(+ = master louder)",/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},refStats.filename," · ",refStats.duration,"s · ",refStats.sample_rate,"Hz · ",refStats.channels,"ch")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-1.5 pt-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{stopRef();setAb('A');},className:`py-1.5 rounded text-[10px] font-bold border ${ab==='A'?'bg-cyan-600 border-cyan-500 text-white':'bg-slate-800 text-slate-300 border-slate-700'}`},"A · Master"),/*#__PURE__*/React.createElement("button",{onClick:playB,disabled:!refStats,className:`py-1.5 rounded text-[10px] font-bold border ${ab==='B'?'bg-fuchsia-600 border-fuchsia-500 text-white':'bg-slate-800 text-slate-300 border-slate-700'}`},"B · Reference"))));};// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ── +const token=localStorage.getItem('sonic_token')||'';const resp=await fetch((window.API_BASE_URL||'')+refStats.url,{headers:token?{'Authorization':'Bearer '+token}:{}});if(!resp.ok)throw new Error('HTTP '+resp.status);const blob=await resp.blob();const objUrl=URL.createObjectURL(blob);const au=new Audio(objUrl);au._objUrl=objUrl;audioRef.current=au;au.loop=true;au.play().then(()=>setAb('B')).catch(()=>{});}catch(e){window.showToast&&window.showToast('Play ref loi: '+(e.message||e),'error');}};const fmt=(v,suf)=>v===null||v===undefined?'--':v+suf;const dlt=(a,b)=>a===null||a===undefined||b===null||b===undefined?'--':(a-b>0?'+':'')+(a-b).toFixed(1);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[115] bg-black/70 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-md bg-slate-900 border border-slate-700 rounded-2xl p-4 space-y-3 shadow-2xl",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-white tracking-wider"},"A/B REFERENCE"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-500 hover:text-slate-300"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},/*#__PURE__*/React.createElement("input",{value:path,onChange:e=>setPath(e.target.value),placeholder:"Local path (media/browse) hoac file_id",className:"flex-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1 text-xs text-zinc-300 focus:outline-none"}),/*#__PURE__*/React.createElement("button",{onClick:()=>path&&analyze({path}),disabled:busy,className:"px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-xs font-bold"},busy?'...':'Analyze')),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-500 text-center"},"- hoac -"),/*#__PURE__*/React.createElement("button",{onClick:()=>fileInputRef.current&&fileInputRef.current.click(),className:"w-full py-1.5 bg-slate-800 hover:bg-slate-700 text-zinc-200 rounded text-xs font-bold border border-slate-700"},"Upload reference file"),/*#__PURE__*/React.createElement("input",{ref:fileInputRef,type:"file",accept:"audio/*,.wav,.mp3,.flac,.ogg,.aiff,.m4a,.aac,.opus",className:"hidden",onChange:e=>{const f=e.target.files&&e.target.files[0];onPickFile(f);e.target.value='';}}),(refStats||masterStats)&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-1.5 text-[10px] text-center"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"METER"),/*#__PURE__*/React.createElement("div",{className:"text-emerald-400 font-bold"},"LUFS"),/*#__PURE__*/React.createElement("div",{className:"text-slate-300 font-bold"},"dBTP"),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},"Tone")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"MASTER"),/*#__PURE__*/React.createElement("div",{className:"text-cyan-300 font-bold"},fmt(masterStats&&masterStats.lufs,'')),/*#__PURE__*/React.createElement("div",{className:"text-cyan-300 font-bold"},fmt(masterStats&&masterStats.true_peak_db,'')),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},"-")),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded p-1.5"},/*#__PURE__*/React.createElement("div",{className:"text-slate-500 font-bold mb-1"},"REF"),/*#__PURE__*/React.createElement("div",{className:"text-fuchsia-300 font-bold"},fmt(refStats&&refStats.lufs,'')),/*#__PURE__*/React.createElement("div",{className:"text-fuchsia-300 font-bold"},fmt(refStats&&refStats.true_peak_db,'')),/*#__PURE__*/React.createElement("div",{className:"text-slate-400"},fmt(refStats&&refStats.centroid_hz,'Hz')))),refStats&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400 text-center"},"Delta: LUFS ",dlt(masterStats&&masterStats.lufs,refStats.lufs)," / dBTP ",dlt(masterStats&&masterStats.true_peak_db,refStats.true_peak_db)," \xA0(+ = master louder)",/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},refStats.filename," \xB7 ",refStats.duration,"s \xB7 ",refStats.sample_rate,"Hz \xB7 ",refStats.channels,"ch")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-1.5 pt-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{stopRef();setAb('A');},className:`py-1.5 rounded text-[10px] font-bold border ${ab==='A'?'bg-cyan-600 border-cyan-500 text-white':'bg-slate-800 text-slate-300 border-slate-700'}`},"A \xB7 Master"),/*#__PURE__*/React.createElement("button",{onClick:playB,disabled:!refStats,className:`py-1.5 rounded text-[10px] font-bold border ${ab==='B'?'bg-fuchsia-600 border-fuchsia-500 text-white':'bg-slate-800 text-slate-300 border-slate-700'}`},"B \xB7 Reference"))));};// ── Master Strip Console Component (from md/47_MASTER_STRIP_CONSOLE.md) ── const MasterStripConsole=({masterVolume,setMasterVolume,masterVolL,setMasterVolL,masterVolR,setMasterVolR,masterLink,setMasterLink,masterMuted,setMasterMuted,masterAutoVol,setMasterAutoVol,masterAutoOn,setMasterAutoOn,showMasteringModal,setShowMasteringModal,masteringSettings,setMasteringSettings,isPlaying})=>{const[isTestPlaying,setIsTestPlaying]=React.useState(false);const[isMono,setIsMono]=React.useState(false);const[pan,setPan]=React.useState(0.0);const[panText,setPanText]=React.useState('center');const vuCanvasRef=React.useRef(null);const animFrameRef=React.useRef(null);const rmsValRef=React.useRef(null);const peakLRef=React.useRef(null);const peakRRef=React.useRef(null);const panPointerRef=React.useRef(null);const isMasterActive=masteringSettings?masteringSettings.masterConnected:false;const panStartYRef=React.useRef(0);const startPanValRef=React.useRef(0);const[refOpen,setRefOpen]=React.useState(false);const[autoOpen,setAutoOpen]=React.useState(false);const addAutoPoint=()=>{const pts=(masterAutoVol||[]).slice();pts.push({time:Math.round(pts.reduce((m,p)=>Math.max(m,p.time||0),0)+1),db:0});setMasterAutoVol(pts);};const updAutoPoint=(i,key,val)=>{const pts=(masterAutoVol||[]).slice();pts[i]=Object.assign({},pts[i],{[key]:val});setMasterAutoVol(pts);};const delAutoPoint=i=>setMasterAutoVol((masterAutoVol||[]).filter((p,j)=>j!==i));const ensureAudio=()=>{getAudioContext();};const handleFaderChangeL=val=>{val=Math.max(-60,Math.min(12,val));ensureAudio();setMasterVolL(val);if(masterLink)setMasterVolR(Math.max(-60,Math.min(12,val+(masterVolR-masterVolL))));};const handleFaderChangeR=val=>{val=Math.max(-60,Math.min(12,val));ensureAudio();setMasterVolR(val);if(masterLink)setMasterVolL(Math.max(-60,Math.min(12,val-(masterVolR-masterVolL))));};// Double-click reset: ca 2 ve 0 const handleFaderChange=val=>{handleFaderChangeL(val);handleFaderChangeR(val);};React.useEffect(()=>{applyMasterVolumesTo(masterBus,masterVolL,masterVolR,masterMuted);},[masterVolL,masterVolR,masterMuted]);const masterVolDisplay=masterLink?masterVolL<=-50?'-inf':(masterVolL>0?'+':'')+masterVolL.toFixed(1):'L'+(masterVolL<=-50?'-inf':masterVolL.toFixed(1))+' R'+(masterVolR<=-50?'-inf':masterVolR.toFixed(1));const handlePanPointerDown=e=>{isPanDraggingRef.current=true;panStartYRef.current=e.clientY;startPanValRef.current=pan;e.currentTarget.setPointerCapture(e.pointerId);};const handlePanPointerMove=e=>{if(!isPanDraggingRef.current)return;const deltaY=panStartYRef.current-e.clientY;let newPan=startPanValRef.current+deltaY/80;newPan=Math.min(1.0,Math.max(-1.0,newPan));setPan(newPan);const angle=newPan*120;if(panPointerRef.current)panPointerRef.current.style.transform=`rotate(${angle}deg)`;if(newPan===0)setPanText('center');else if(newPan<0)setPanText('L'+Math.abs(Math.round(newPan*100)));else setPanText('R'+Math.round(newPan*100));};const handlePanPointerUp=e=>{isPanDraggingRef.current=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}};React.useEffect(()=>{const canvas=vuCanvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');function render(){animFrameRef.current=requestAnimationFrame(render);canvas.width=canvas.clientWidth;canvas.height=canvas.clientHeight;const w=canvas.width;const h=canvas.height;ctx.clearRect(0,0,w,h);let levelL=0;let levelR=0;if(masterBus&&masterBus.leftAnalyser&&masterBus.rightAnalyser){const leftData=new Uint8Array(256);const rightData=new Uint8Array(256);masterBus.leftAnalyser.getByteTimeDomainData(leftData);masterBus.rightAnalyser.getByteTimeDomainData(rightData);let peakL=0;let peakR=0;for(let i=0;ipeakL)peakL=v;}for(let i=0;ipeakR)peakR=v;}levelL=peakL;levelR=peakR;}const padding=4;const gap=4;const barW=Math.max(4,(w-padding*2-gap)/2);const grad=ctx.createLinearGradient(0,h,0,0);grad.addColorStop(0,'#10b981');grad.addColorStop(0.7,'#f59e0b');grad.addColorStop(0.95,'#ef4444');ctx.fillStyle=grad;ctx.fillRect(padding,h-levelL*h,barW,levelL*h);ctx.fillRect(padding+barW+gap,h-levelR*h,barW,levelR*h);const maxLevel=Math.max(levelL,levelR);if(rmsValRef.current){rmsValRef.current.innerText=maxLevel>0?(20*Math.log10(maxLevel)-3.2).toFixed(1)+' dB':'-inf';}if(peakLRef.current){peakLRef.current.innerText=levelL>0?(20*Math.log10(levelL)).toFixed(1)+'dB':'-inf';}if(peakRRef.current){peakRRef.current.innerText=levelR>0?(20*Math.log10(levelR)).toFixed(1)+'dB':'-inf';}}render();return()=>{if(animFrameRef.current)cancelAnimationFrame(animFrameRef.current);};},[masterVolL,masterVolR,masterMuted,isMono]);return React.createElement(React.Fragment,null,React.createElement("div",{className:"flex flex-col items-stretch w-[300px] shrink-0 strip-bg rounded-lg p-2 text-slate-300 select-none shadow-2xl relative overflow-hidden"},React.createElement("div",{className:"flex flex-col items-center my-0.5"},React.createElement("span",{className:"text-[9px] text-slate-400 font-mono"},panText||'center'),React.createElement("div",{className:"flex items-center gap-1"},React.createElement("span",{className:"text-[8px] font-mono text-slate-500 w-5 text-right"},pan<0?'L'+Math.abs(Math.round(pan*100)):''),React.createElement("div",{id:"panDial",className:"w-6 h-6 rounded-full bg-slate-800 border-2 border-slate-600 relative flex items-center justify-center shadow-inner cursor-pointer",title:"Kéo chuột để chỉnh Pan (Left/Right)",onPointerDown:handlePanPointerDown,onPointerMove:handlePanPointerMove,onPointerUp:handlePanPointerUp,onWheel:function(e){e.preventDefault();var step=e.deltaY>0?-0.05:0.05;var newPan=Math.max(-1,Math.min(1,pan+step));newPan=Math.round(newPan*100)/100;setPan(newPan);if(panPointerRef.current)panPointerRef.current.style.transform='rotate('+newPan*120+'deg)';if(newPan===0)setPanText('center');else if(newPan<0)setPanText('L'+Math.abs(Math.round(newPan*100)));else setPanText('R'+Math.round(newPan*100));},onDoubleClick:function(){setPan(0);setPanText('center');if(panPointerRef.current)panPointerRef.current.style.transform='rotate(0deg)';}},React.createElement("div",{ref:panPointerRef,id:"panPointer",className:"w-0.5 h-2 bg-slate-200 rounded absolute top-0.5 transition-transform",style:{transform:'rotate('+pan*120+'deg)'}})),React.createElement("span",{className:"text-[8px] font-mono text-slate-500 w-5 text-left"},pan>0?'R'+Math.round(pan*100):''),React.createElement("span",{ref:React.createRef?null:null,className:"text-[10px] font-bold font-mono text-slate-200 ml-8",onDoubleClick:function(){handleFaderChange(0);}},masterVolDisplay))),React.createElement("div",{className:"flex gap-1 my-1 justify-between items-stretch min-h-0",style:{flex:'1 1 0%'}},React.createElement("div",{className:"flex-1 flex flex-col bg-black/80 p-1.5 rounded border border-slate-800/90 relative shadow-inner"},React.createElement("div",{className:"flex-1 flex items-stretch justify-between min-h-0"},React.createElement("div",{className:"flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pr-1 text-right border-r border-slate-800/60 overflow-hidden"},React.createElement("span",{className:"flex-1 flex items-center justify-end"},"+12"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"+6"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"0"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-6"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-12"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-18"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-24"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-30"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-36"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-42"),React.createElement("span",{className:"flex-1 flex items-center justify-end"},"-54")),React.createElement("div",{className:"flex-1 relative bg-slate-950 rounded overflow-hidden mx-1.5 border border-slate-900"},React.createElement("canvas",{ref:vuCanvasRef,className:"w-[100px] h-full block"}),React.createElement("div",{className:"absolute bottom-0.5 inset-x-0 flex justify-around text-[7px] font-mono text-slate-500 font-bold pointer-events-none bg-black/60 py-0.5"},React.createElement("span",null,"L"),React.createElement("span",null,"R"))),React.createElement("div",{className:"flex flex-col h-full text-[8px] font-mono text-slate-400 select-none pl-1 text-left border-l border-slate-800/60 overflow-hidden"},React.createElement("span",{className:"flex-1 flex items-center justify-start"},"+12"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"+6"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"0"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-6"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-12"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-18"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-24"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-30"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-36"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-42"),React.createElement("span",{className:"flex-1 flex items-center justify-start"},"-54"))),React.createElement("div",{className:"flex justify-between text-[9px] font-mono text-slate-400 mt-0.5"},React.createElement("span",{ref:peakLRef},"-inf"),React.createElement("span",{ref:peakRRef},"-inf"))),React.createElement("div",{className:"w-36 flex items-stretch gap-0.5 bg-slate-900/60 p-1 rounded border border-slate-800"},React.createElement("div",{className:"flex-1 flex flex-col items-center justify-center relative fader-track rounded",onWheel:function(e){e.preventDefault();handleFaderChangeL(masterVolL+(e.deltaY>0?-0.5:0.5));}},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0"}),React.createElement("input",{id:"masterFaderL",type:"range",min:"-60",max:"12",step:"0.5",value:masterVolL,className:"fader-slider w-full z-10",orient:"vertical",onChange:function(e){handleFaderChangeL(parseFloat(e.target.value));},onDoubleClick:function(){handleFaderChangeL(0);}}),React.createElement("span",{className:"text-[7px] font-mono text-slate-500 leading-none mt-0.5"},"L")),React.createElement("div",{className:"relative w-5 text-[7px] font-mono text-slate-500 select-none overflow-hidden"},React.createElement("span",{className:"absolute",style:{top:'0%',right:'2px'}},"+12"),React.createElement("span",{className:"absolute",style:{top:'8.3%',right:'2px'}},"+6"),React.createElement("span",{className:"absolute",style:{top:'16.7%',right:'2px'}},"0"),React.createElement("span",{className:"absolute",style:{top:'25%',right:'2px'}},"-6"),React.createElement("span",{className:"absolute",style:{top:'33.3%',right:'2px'}},"-12"),React.createElement("span",{className:"absolute",style:{top:'50%',right:'2px'}},"-24"),React.createElement("span",{className:"absolute",style:{top:'66.7%',right:'2px'}},"-36"),React.createElement("span",{className:"absolute",style:{top:'91.7%',right:'2px'}},"-54")),React.createElement("div",{className:"flex-1 flex flex-col items-center justify-center relative fader-track rounded",onWheel:function(e){e.preventDefault();handleFaderChangeR(masterVolR+(e.deltaY>0?-0.5:0.5));}},React.createElement("div",{className:"w-0.5 h-full bg-slate-700 absolute left-1/2 -translate-x-1/2 top-0"}),React.createElement("input",{id:"masterFaderR",type:"range",min:"-60",max:"12",step:"0.5",value:masterVolR,className:"fader-slider w-full z-10",orient:"vertical",onChange:function(e){handleFaderChangeR(parseFloat(e.target.value));},onDoubleClick:function(){handleFaderChangeR(0);}}),React.createElement("span",{className:"text-[7px] font-mono text-slate-500 leading-none mt-0.5"},"R"))),React.createElement("div",{className:"w-8 flex flex-col justify-between text-[10px] font-bold"},React.createElement("button",{id:"monoBtn",onClick:function(){setIsMono(function(p){return!p;});},className:"btn-daw h-[18px] rounded flex flex-col items-center justify-center text-[8px]"+(isMono?" btn-mono-active":""),title:"Mono Switch"},React.createElement("i",{className:"fa-solid fa-circle-half-stroke text-[9px]"}),React.createElement("span",null,"MONO")),React.createElement("button",{id:"muteBtn",onClick:function(){setMasterMuted(function(p){return!p;});},className:"btn-daw h-[18px] rounded text-amber-500 font-bold hover:text-amber-400"+(masterMuted?" btn-mute-active":""),title:"Mute Master Output (that: zero output)"},"M"),React.createElement("button",{id:"linkBtn",onClick:function(){setMasterLink(function(p){return!p;});},className:"btn-daw h-[18px] rounded text-xs font-bold"+(masterLink?" btn-teal-active":""),title:"LINK: keo L/R cung luc (giu delta)"},"LK"),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 hover:text-slate-200",title:"Route Matrix"},React.createElement("i",{className:"fa-solid fa-diagram-project text-[9px]"})),React.createElement("button",{id:"fxBtn",className:"btn-daw h-[18px] rounded font-extrabold text-[10px] transition-all",onClick:function(){setShowMasteringModal(true);},title:"Mở MASTER FX CHAIN để chỉnh sửa"},"FX"),React.createElement("button",{id:"powerBtn",className:"btn-daw h-[18px] rounded text-xs transition-all"+(isMasterActive?" btn-teal-active":""),onClick:function(){if(setMasteringSettings){setMasteringSettings(function(prev){return Object.assign({},prev,{masterConnected:!prev.masterConnected,isBypassed:false});});}},title:"Bật/Tắt MASTER FX CHAIN Bypass"},[React.createElement("i",{className:"fa-solid fa-power-off"+(isMasterActive?" text-emerald-400":" text-slate-500"),key:"ico"}),React.createElement("span",{key:"lbl",className:"text-[7px] font-bold"+(isMasterActive?" text-emerald-300":" text-slate-400")},"PWR")]),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 text-[8px]",title:"Trim Envelope"},"TRIM"),React.createElement("button",{className:"btn-daw h-[18px] rounded text-slate-400 text-[9px]",title:"Session Info"},React.createElement("i",{className:"fa-solid fa-info"})))),React.createElement("div",{className:"flex flex-col gap-0.5 my-1 shrink-0"},React.createElement("div",{className:"flex items-center justify-between gap-1"},React.createElement("button",{id:"automBtn",onClick:function(){setAutoOpen(function(p){return!p;});},className:"btn-daw rounded px-1 py-0.5 text-[8px] font-bold"+(autoOpen?" btn-teal-active":""),title:"Master Volume Automation (Gap 7)"},"AUTOM"),React.createElement("button",{onClick:function(){setMasterAutoOn(function(p){return!p;});},className:"text-[8px] px-1 py-0.5 rounded font-mono"+(masterAutoOn?" text-emerald-400 bg-emerald-500/10":" text-slate-500"),title:"Bat/tat master volume automation"},masterAutoOn?"ON":"OFF")),autoOpen&&React.createElement("div",{className:"flex flex-col gap-0.5 bg-slate-950 border border-slate-800 rounded p-1 max-h-32 overflow-y-auto"},(masterAutoVol||[]).map(function(pt,i){return React.createElement("div",{key:i,className:"flex items-center gap-1"},React.createElement("input",{type:"number",step:"0.1",min:"0",value:pt.time,onChange:function(e){updAutoPoint(i,'time',parseFloat(e.target.value)||0);},className:"w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"}),React.createElement("span",{className:"text-[8px] text-slate-500"},"s"),React.createElement("input",{type:"number",step:"0.5",min:"-30",max:"3",value:pt.db,onChange:function(e){updAutoPoint(i,'db',parseFloat(e.target.value)||0);},className:"w-12 bg-slate-900 border border-slate-700 rounded px-0.5 text-[8px] font-mono text-slate-200"}),React.createElement("span",{className:"text-[8px] text-slate-500"},"dB"),React.createElement("button",{onClick:function(){delAutoPoint(i);},className:"text-[8px] text-red-400 hover:text-red-300 ml-auto"},"x"));}),React.createElement("button",{onClick:addAutoPoint,className:"text-[8px] text-cyan-400 hover:text-cyan-300 mt-0.5"},"+ ADD POINT"))),React.createElement("div",{className:"text-center text-[10px] font-bold font-mono text-slate-200 shrink-0"},masterVolDisplay),React.createElement("div",{className:"shrink-0 border-t border-slate-800 flex flex-col items-center"},React.createElement("div",{className:"flex justify-between w-full text-[9px] font-mono py-0.5"},React.createElement("span",{className:"text-emerald-400"},"RMS"),React.createElement("span",{ref:rmsValRef,className:"text-emerald-400 font-bold"},"-inf")),React.createElement("div",{className:"w-full text-center bg-black/80 py-0.5 rounded border border-slate-800 text-[10px] font-extrabold tracking-widest text-slate-100 uppercase"},React.createElement("span",null,"MAIN OUT")))),refOpen&&React.createElement(RefCompareModal,{open:refOpen,onClose:function(){setRefOpen(false);}}));};// ── Track Strip Console Component (from md/48_TRACK_STRIP_COMPONENT.md) ── const TrackStripConsole=({track,index,onUpdateTrack,trackVuRefs,style})=>{var vol=track.volumeDb!=null?track.volumeDb:0;var trackColor=track.color||'#06b6d4';var isMuted=track.muted;var isSoloed=track.solo;var isBypassed=!!(track.audioBypass??track.masteringBypass);var isArmed=track.isArmed;var trackName=track.name||'Track '+(index+1);var isMicActive=track.inputSource?.deviceType==='MICROPHONE';const[pan,setPan]=React.useState(0.0);const[panLabel,setPanLabel]=React.useState('center');const[isPhaseInverted,setIsPhaseInverted]=React.useState(false);const panPointerRef=React.useRef(null);const setVuCanvas=React.useCallback(function(el){if(el)trackVuRefs.current[track.id+'_mixer']=el;else delete trackVuRefs.current[track.id+'_mixer'];},[track.id,trackVuRefs]);// Track FX chain editor (mastering_expand.md §II.4): reuse the same module @@ -708,7 +736,7 @@ const[loadingBackups,setLoadingBackups]=useState({});// project_id -> bool const[backupMaxCount,setBackupMaxCount]=useState(()=>parseInt(localStorage.getItem('sonic_backup_max_count')||'10'));const[showBackupConfig,setShowBackupConfig]=useState(false);const handleDragStart=e=>{const r=dragRef.current;r.active=true;r.startX=e.clientX;r.startY=e.clientY;r.ofsX=dragOfs.x;r.ofsY=dragOfs.y;const onMove=ev=>{if(!r.active)return;setDragOfs({x:r.ofsX+ev.clientX-r.startX,y:r.ofsY+ev.clientY-r.startY});};const onUp=()=>{r.active=false;document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};useEffect(()=>{if(isOpen){fetchProfile();if(activeTab==='projects')fetchProjects();if(activeTab==='files')fetchFiles();}},[isOpen,activeTab]);const fetchProfile=async()=>{try{const data=await window.SonicAPI.getProfile();setProfile(data);}catch(e){setError(e.message||'Không thể tải thông tin profile');}};const fetchProjects=async()=>{setLoadingProjects(true);try{const data=await window.SonicAPI.listCloudProjects();setProjectsList(data||[]);}catch(e){showToast(e.message||'Không thể tải danh sách dự án','error');}finally{setLoadingProjects(false);}};const fetchFiles=async()=>{setLoadingFiles(true);try{const activeFileIds=tracks.map(t=>t.serverFileId).filter(Boolean);const data=await window.SonicAPI.listMyFiles(activeFileIds);setFilesList(data||[]);}catch(e){showToast(e.message||'Không thể tải danh sách tệp tin','error');}finally{setLoadingFiles(false);}};const fetchBackups=async projectId=>{setLoadingBackups(prev=>({...prev,[projectId]:true}));try{const data=await window.SonicAPI.listBackups(projectId);setBackupsMap(prev=>({...prev,[projectId]:data||[]}));}catch(e){showToast(e.message||'Không thể tải danh sách backup','error');}finally{setLoadingBackups(prev=>({...prev,[projectId]:false}));}};const handleDeleteBackup=async backupId=>{try{await window.SonicAPI.deleteBackup(backupId);setBackupsMap(prev=>{const next={...prev};Object.keys(next).forEach(pid=>{next[pid]=next[pid].filter(b=>b.id!==backupId);});return next;});fetchProjects();showToast('Đã xóa bản backup','info');}catch(e){showToast(e.message||'Lỗi xóa backup','error');}};const handleCleanupBackups=async()=>{try{const res=await window.SonicAPI.cleanupBackups(backupMaxCount);setBackupsMap({});fetchProjects();showToast(`Đã dọn dẹp ${res.deleted} bản backup cũ (giữ lại ${res.keep})`,'info');}catch(e){showToast(e.message||'Lỗi dọn dẹp backup','error');}};const handleOpenProject=async(projectId,projectName)=>{setAppWarningModal({title:"Mở dự án",message:"Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",isAlert:false,onConfirm:async()=>{try{const proj=await window.SonicAPI.getCloudProject(projectId);const parsed=JSON.parse(proj.data_json);let restoredTracks=[];let restoredBpm=bpm;let restoredSessionTabs=[];let restoredSubTabs=[];if(parsed.main_session){const result=deserializeProjectFromSchema(parsed);restoredTracks=result.tracks;restoredBpm=result.bpm;restoredSessionTabs=result.sessionTabs;restoredSubTabs=result.subTabs;if(result.masteringSettings){setMasteringSettings(result.masteringSettings);}else{setMasteringSettings({masterConnected:false,activeModule:null,eqActive:false,imagerActive:false,maximizerActive:false,eqLowGain:1.5,eqMid1Gain:-1.0,eqMid2Gain:2.0,eqHighGain:1.8,w1:0,w2:115,w3:135,w4:150,imagerScale:'v2',maxGain:5.4,maxUpward:2.0,maxSoftClip:15,maxTransient:25,ceiling:-0.1,isBypassed:false,chain:[],compActive:false,compThreshold:-16,compRatio:3,compMakeup:0,limActive:false,limThreshold:-1.0,limMode:0,limLookahead:2,excActive:false,excDrive:40,rebalActive:false,rebalMid:0,rebalSide:0});}}else{restoredTracks=(parsed.tracks||[]).map(t=>{const{height:_h,...rest}=t;return{...rest,buffer:null,channelInfo:t.channelInfo||null,clips:t.clips||[],serverFileId:t.serverFileId||null};});}setTracks(restoredTracks);loadAudioBuffersForTracks(restoredTracks).catch(function(err){console.warn('loadAudioBuffersForTracks error:',err);});trackMidiChannelsRef.current={};preloadTrackInstruments(restoredTracks).catch(function(err){console.warn('preloadTrackInstruments error:',err);});setBpm(restoredBpm.toString());setSelectedTrackId(restoredTracks[0]?.id||'1');setProjectName(proj.name);setCurrentProjectId(proj.id);if(restoredSessionTabs.length>0){setSessionTabs(restoredSessionTabs);}if(restoredSubTabs.length>0){setSubTabs(restoredSubTabs);}localStorage.setItem('sonic_project_name',proj.name);localStorage.setItem('sonic_project_id',proj.id);showToast(`Đã nạp dự án "${proj.name}" thành công!`,"success");}catch(e){showToast(e.message||"Lỗi khi nạp dự án","error");}}});};const handleDeleteProject=async(projectId,e)=>{e.stopPropagation();setConfirmModal({title:"Xóa dự án Cloud",message:"Bạn có chắc chắn muốn xóa dự án này khỏi Cloud? Hành động này không thể hoàn tác.",onConfirm:async()=>{try{await window.SonicAPI.deleteCloudProject(projectId);showToast("Đã xóa dự án thành công!","success");fetchProjects();fetchProfile();}catch(err){showToast(err.message||"Lỗi khi xóa dự án","error");}}});};const handleDeleteFile=async fileId=>{if(!confirm(`Bạn có chắc chắn muốn xóa tệp tin ${fileId}?`))return;try{await window.SonicAPI.deleteMyFile(fileId);showToast("Đã xóa tệp tin thành công!","success");fetchFiles();fetchProfile();}catch(err){showToast(err.message||"Lỗi khi xóa tệp tin","error");}};const handleCleanUnusedFiles=async()=>{const unusedFiles=filesList.filter(f=>!f.is_in_use);if(unusedFiles.length===0){showToast("Không có tập tin rác nào để dọn dẹp.","info");return;}if(!confirm(`Bạn có chắc chắn muốn xóa tất cả ${unusedFiles.length} tập tin rác để giải phóng dung lượng?`))return;let successCount=0;for(const file of unusedFiles){try{await window.SonicAPI.deleteMyFile(file.file_id);successCount++;}catch(e){console.error("Lỗi xóa file rác: ",file.file_id,e);}}showToast(`Đã dọn dẹp thành công ${successCount}/${unusedFiles.length} tập tin rác!`,"success");fetchFiles();fetchProfile();};const handleChangePassword=async e=>{e.preventDefault();setMsg('');setError('');setLoading(true);try{const res=await window.SonicAPI.changePassword(oldPassword,newPassword);setMsg(res.message||'Đổi mật khẩu thành công!');setOldPassword('');setNewPassword('');}catch(err){setError(err.message||'Lỗi khi đổi mật khẩu');}finally{setLoading(false);}};const modalStyle={left:`calc(50% + ${dragOfs.x}px)`,top:`calc(50% + ${dragOfs.y}px)`,transform:'translate(-50%, -50%)'};return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{key:"backdrop",className:"fixed inset-0 z-40 bg-black/70 backdrop-blur-sm",onClick:onClose}),confirmModal&&/*#__PURE__*/React.createElement("div",{key:"confirm-overlay",className:"fixed inset-0 z-[60] flex items-center justify-center bg-black/50"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-lg shadow-2xl p-5 max-w-sm w-full text-slate-200"},/*#__PURE__*/React.createElement("h4",{className:"text-sm font-bold text-rose-400 mb-2"},confirmModal.title),/*#__PURE__*/React.createElement("p",{className:"text-xs text-slate-300 mb-4"},confirmModal.message),/*#__PURE__*/React.createElement("div",{className:"flex justify-end gap-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>setConfirmModal(null),className:"px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold"},confirmModal.cancelText||"Hủy"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const fn=confirmModal.onConfirm;setConfirmModal(null);fn();},className:"px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold"},confirmModal.confirmText||"Xác nhận xóa")))),/*#__PURE__*/React.createElement("div",{key:"dialog",className:"fixed z-50 bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200 flex flex-col max-h-[85vh]",style:modalStyle},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838] shrink-0 cursor-grab active:cursor-grabbing select-none",onMouseDown:handleDragStart},/*#__PURE__*/React.createElement("h3",{className:"text-md font-bold text-teal-400 flex items-center gap-1.5"},"👤 Hồ Sơ Cá Nhân & Quản Lý Dự Án"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),/*#__PURE__*/React.createElement("div",{className:"flex gap-2 border-b border-[#383838] py-2 shrink-0 text-xs font-semibold"},[/*#__PURE__*/React.createElement("button",{key:"tab-acc",onClick:()=>setActiveTab('account'),className:`px-3 py-1.5 rounded transition ${activeTab==='account'?'bg-teal-950/60 text-teal-300 border border-teal-800':'text-slate-400 hover:text-slate-200'}`},"Tài Khoản"),/*#__PURE__*/React.createElement("button",{key:"tab-proj",onClick:()=>setActiveTab('projects'),className:`px-3 py-1.5 rounded transition ${activeTab==='projects'?'bg-teal-950/60 text-teal-300 border border-teal-800':'text-slate-400 hover:text-slate-200'}`},"Dự Án Cloud"),/*#__PURE__*/React.createElement("button",{key:"tab-files",onClick:()=>setActiveTab('files'),className:`px-3 py-1.5 rounded transition ${activeTab==='files'?'bg-teal-950/60 text-teal-300 border border-teal-800':'text-slate-400 hover:text-slate-200'}`},"Tập Tin Của Tôi")]),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto pt-4 space-y-4 pr-1 min-h-[300px]"},activeTab==='account'&&profile?[/*#__PURE__*/React.createElement("div",{key:"quota-info",className:"bg-[#1e1e1e] p-4 rounded-lg border border-[#333] grid grid-cols-2 gap-4 text-xs"},[/*#__PURE__*/React.createElement("div",{key:"username"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Tên người dùng"),/*#__PURE__*/React.createElement("span",{className:"font-bold text-teal-300 text-sm"},profile.username)]),/*#__PURE__*/React.createElement("div",{key:"role"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Vai trò"),/*#__PURE__*/React.createElement("span",{className:"uppercase font-semibold text-amber-400"},profile.role)]),/*#__PURE__*/React.createElement("div",{key:"email"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Email"),/*#__PURE__*/React.createElement("span",null,profile.email)]),/*#__PURE__*/React.createElement("div",{key:"quota"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-500 block"},"Dung lượng Quota"),/*#__PURE__*/React.createElement("span",{className:"font-semibold text-slate-200"},`${profile.quota.used_mb} MB / ${profile.quota.storage_limit_mb} MB`)])]),/*#__PURE__*/React.createElement("div",{key:"progress"},[/*#__PURE__*/React.createElement("div",{className:"flex justify-between text-xs mb-1"},[/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Tiến trình sử dụng bộ nhớ Server"),/*#__PURE__*/React.createElement("span",{className:"font-bold text-teal-400"},`${(profile.quota.used_mb/profile.quota.storage_limit_mb*100).toFixed(1)}%`)]),/*#__PURE__*/React.createElement("div",{className:"w-full h-2 bg-slate-800 rounded-full overflow-hidden"},[/*#__PURE__*/React.createElement("div",{className:"h-full bg-teal-500 rounded-full transition-all duration-300",style:{width:`${Math.min(100,profile.quota.used_mb/profile.quota.storage_limit_mb*100)}%`}})])]),/*#__PURE__*/React.createElement("form",{key:"pwd-form",onSubmit:handleChangePassword,className:"pt-4 border-t border-[#383838] space-y-3"},[/*#__PURE__*/React.createElement("h4",{className:"text-xs font-bold text-slate-300 uppercase"},"Thay Đổi Mật Khẩu"),msg&&/*#__PURE__*/React.createElement("div",{className:"p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"},msg),error&&/*#__PURE__*/React.createElement("div",{className:"p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"},error),/*#__PURE__*/React.createElement("div",{key:"old"},[/*#__PURE__*/React.createElement("label",{className:"block text-xs text-slate-400 mb-1"},"Mật khẩu cũ"),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:"current-password",required:true,value:oldPassword,onChange:e=>setOldPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"})]),/*#__PURE__*/React.createElement("div",{key:"new"},[/*#__PURE__*/React.createElement("label",{className:"block text-xs text-slate-400 mb-1"},"Mật khẩu mới"),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:"new-password",required:true,value:newPassword,onChange:e=>setNewPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"})]),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"w-full py-1.5 bg-teal-600 hover:bg-teal-500 text-white font-semibold text-xs rounded transition"},loading?'Đang cập nhật...':'Cập Nhật Mật Khẩu')])]:activeTab==='projects'?[loadingProjects?/*#__PURE__*/React.createElement("div",{key:"loading",className:"text-center py-8 text-xs text-zinc-500"},"Đang tải danh sách dự án..."):projectsList.length===0?/*#__PURE__*/React.createElement("div",{key:"empty",className:"text-center py-8 text-xs text-zinc-500"},"Bạn chưa có dự án nào lưu trên Cloud."):/*#__PURE__*/React.createElement(React.Fragment,{key:"list"},[/*#__PURE__*/React.createElement("div",{key:"backup-config-bar",className:"flex items-center justify-between bg-zinc-900/60 p-2 rounded border border-zinc-800 mb-2 text-xs"},[/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},[/*#__PURE__*/React.createElement("span",{className:"text-zinc-400 text-[10px]"},"⚙️ Tự động lưu 5 phút / Backup 30 phút"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setShowBackupConfig(!showBackupConfig);},className:"px-2 py-0.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-[10px] border border-zinc-700"},showBackupConfig?"ẨN":"CẤU HÌNH")]),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();handleCleanupBackups();},className:"px-2 py-0.5 bg-amber-800 hover:bg-amber-700 text-amber-200 rounded text-[10px] border border-amber-700"},"🧹 DỌN BACKUP")]),showBackupConfig&&/*#__PURE__*/React.createElement("div",{key:"backup-config-detail",className:"bg-[#18181b] border border-zinc-800 rounded p-3 mb-2 text-xs"},[/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-2"},[/*#__PURE__*/React.createElement("label",{className:"text-zinc-300 font-semibold"},"Số bản backup tối đa:"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},[/*#__PURE__*/React.createElement("input",{type:"range",min:5,max:20,value:backupMaxCount,onChange:e=>{const v=parseInt(e.target.value);setBackupMaxCount(v);localStorage.setItem('sonic_backup_max_count',v.toString());},className:"w-24 accent-amber-500"}),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-bold w-6 text-center"},backupMaxCount)])]),/*#__PURE__*/React.createElement("p",{className:"text-[10px] text-zinc-500"},"Mỗi dự án sẽ giữ tối đa số bản backup này. Backup cũ nhất sẽ tự động bị xóa khi vượt quá giới hạn.")]),/*#__PURE__*/React.createElement("div",{className:"space-y-1.5"},projectsList.map(proj=>/*#__PURE__*/React.createElement("div",{key:proj.id},[/*#__PURE__*/React.createElement("div",{onClick:()=>handleOpenProject(proj.id),className:"flex items-center justify-between p-3 bg-[#1e1e1e] hover:bg-zinc-800 rounded border border-zinc-800 transition cursor-pointer text-xs group"},[/*#__PURE__*/React.createElement("div",{key:"meta"},[/*#__PURE__*/React.createElement("div",{className:"font-bold text-slate-200 group-hover:text-teal-400"},proj.name),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 mt-0.5"},[`Dung lượng: ${proj.size_mb} MB | Cập nhật: ${new Date(proj.updated_at*1000).toLocaleString()}`,proj.backup_count>0&&` | Backup: ${proj.backup_count}`])]),/*#__PURE__*/React.createElement("div",{key:"actions",className:"flex items-center gap-1.5"},[/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();if(expandedBackupId===proj.id){setExpandedBackupId(null);}else{setExpandedBackupId(proj.id);fetchBackups(proj.id);}},className:`px-2 py-1 rounded font-semibold text-[10px] border ${expandedBackupId===proj.id?'bg-amber-800/80 text-amber-200 border-amber-700':'bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border-zinc-700'}`},`Backup (${proj.backup_count})`),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();handleOpenProject(proj.id);},className:"px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"},"MỞ"),/*#__PURE__*/React.createElement("button",{onClick:e=>handleDeleteProject(proj.id,e),className:"px-2 py-1 bg-rose-950/60 hover:bg-rose-900/80 text-rose-300 rounded font-semibold text-[10px]"},"XÓA")])]),expandedBackupId===proj.id&&/*#__PURE__*/React.createElement("div",{key:"backup-list",className:"ml-4 pl-3 border-l-2 border-amber-800/50 bg-[#161618] rounded-b p-2 mb-1"},[loadingBackups[proj.id]?/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 py-2 text-center"},"Đang tải..."):!backupsMap[proj.id]||backupsMap[proj.id].length===0?/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 py-2 text-center"},"Chưa có bản backup nào."):/*#__PURE__*/React.createElement("div",{className:"space-y-1 max-h-48 overflow-y-auto"},backupsMap[proj.id].map(b=>/*#__PURE__*/React.createElement("div",{key:b.id,className:"flex items-center justify-between py-1.5 px-2 bg-[#1e1e22] rounded border border-zinc-800"},[/*#__PURE__*/React.createElement("div",{key:"info",className:"flex-1 min-w-0"},[/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-300 truncate"},b.name),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-zinc-500 mt-0.5"},`${b.size_mb} MB | ${new Date(b.created_at*1000).toLocaleString()}`)]),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();handleDeleteBackup(b.id);},className:"px-1.5 py-0.5 bg-rose-950 hover:bg-rose-900 text-rose-300 rounded text-[9px] border border-rose-900 ml-2 shrink-0"},"XÓA")])))])])))])]:activeTab==='files'?[/*#__PURE__*/React.createElement("div",{key:"cleanup-header",className:"flex items-center justify-between bg-zinc-900/60 p-2.5 rounded border border-zinc-800 text-xs shrink-0 mb-3"},[/*#__PURE__*/React.createElement("span",{className:"text-zinc-400 text-[10px]"},"💡 Các tập tin không dùng trong dự án nào sẽ được đánh dấu 'Không dùng' và có thể xóa để giải phóng dung lượng."),/*#__PURE__*/React.createElement("button",{onClick:handleCleanUnusedFiles,className:"px-2.5 py-1 bg-rose-700 hover:bg-rose-600 text-white rounded font-bold text-[10px] uppercase flex items-center gap-1"},"🧹 Dọn dẹp tệp rác")]),loadingFiles?/*#__PURE__*/React.createElement("div",{key:"loading",className:"text-center py-8 text-xs text-zinc-500"},"Đang tải danh sách tập tin..."):filesList.length===0?/*#__PURE__*/React.createElement("div",{key:"empty",className:"text-center py-8 text-xs text-zinc-500"},"Chưa có tập tin nào tải lên hoặc tạo ra."):/*#__PURE__*/React.createElement("div",{key:"list",className:"space-y-1.5"},[filesList.map(file=>/*#__PURE__*/React.createElement("div",{key:file.file_id,className:"flex items-center justify-between p-3 bg-[#1e1e1e] rounded border border-zinc-800 text-xs"},[/*#__PURE__*/React.createElement("div",{key:"meta",className:"max-w-[70%]"},[/*#__PURE__*/React.createElement("div",{className:"font-semibold text-slate-300 truncate"},file.original_name||file.file_id),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 mt-0.5"},`Loại: ${file.type} | Dung lượng: ${file.size_mb} MB | Tạo lúc: ${new Date(file.created_at*1000).toLocaleString()}`)]),/*#__PURE__*/React.createElement("div",{key:"actions",className:"flex items-center gap-2"},[file.is_in_use?/*#__PURE__*/React.createElement("span",{className:"px-2 py-0.5 bg-emerald-950 text-emerald-400 border border-emerald-900 rounded text-[9px] uppercase font-bold font-mono"},"Đang dùng"):/*#__PURE__*/React.createElement("span",{className:"px-2 py-0.5 bg-zinc-850 text-zinc-400 border border-zinc-700 rounded text-[9px] uppercase font-bold font-mono"},"Không dùng"),!file.is_in_use&&/*#__PURE__*/React.createElement("button",{onClick:()=>handleDeleteFile(file.file_id),className:"px-2 py-0.5 bg-rose-950 hover:bg-rose-900 text-rose-300 rounded font-semibold text-[10px] border border-rose-900"},"Xóa")])]))])]:null)));};const SaveProjectModal=({isOpen,onClose,onSaveCloud,onSaveLocal,projectName})=>{if(!isOpen)return null;const[name,setName]=useState(projectName||'');const[saveType,setSaveType]=useState(localStorage.getItem('sonic_token')?'cloud':'local');const[cloudProjects,setCloudProjects]=useState([]);const[loading,setLoading]=useState(false);const[selectedExisting,setSelectedExisting]=useState(null);const[confirmOverwriteProject,setConfirmOverwriteProject]=useState(null);React.useEffect(function(){if(!isOpen)return;if(saveType==='cloud'&&window.SonicAPI&&window.SonicAPI.listCloudProjects){setLoading(true);window.SonicAPI.listCloudProjects().then(function(data){setCloudProjects(data||[]);}).catch(function(){setCloudProjects([]);}).finally(function(){setLoading(false);});}},[isOpen,saveType]);const handleSubmit=e=>{e.preventDefault();if(!name.trim())return;var matched=null;for(var i=0;i{if(!isOpen)return null;const[tab,setTab]=useState('cloud');const[projects,setProjects]=useState([]);const[loading,setLoading]=useState(false);React.useEffect(function(){if(!isOpen)return;if(tab==='cloud'){setLoading(true);var api=window.SonicAPI;if(api&&api.listCloudProjects){api.listCloudProjects().then(function(data){setProjects(data||[]);}).catch(function(){setProjects([]);}).finally(function(){setLoading(false);});}else{setLoading(false);}}},[isOpen,tab]);return React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"},React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-lg p-6 text-slate-200"},React.createElement("h3",{className:"text-sm font-bold text-teal-400 mb-4 uppercase"},"Mở dự án"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-4"},[React.createElement("button",{key:"cloud-tab",type:"button",onClick:function(){setTab('cloud');},className:"py-2 rounded border flex flex-col items-center gap-1 font-semibold transition "+(tab==='cloud'?'bg-teal-950/60 border-teal-500 text-teal-300':'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')},[React.createElement("span",{key:"t"},"☁️ Cloud"),React.createElement("span",{key:"d",className:"text-[9px] font-normal text-zinc-500"},"Dự án trên server")]),React.createElement("button",{key:"local-tab",type:"button",onClick:function(){setTab('local');},className:"py-2 rounded border flex flex-col items-center gap-1 font-semibold transition "+(tab==='local'?'bg-teal-950/60 border-teal-500 text-teal-300':'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')},[React.createElement("span",{key:"t"},"💾 Local"),React.createElement("span",{key:"d",className:"text-[9px] font-normal text-zinc-500"},"Tập tin .sfs trên máy")])]),tab==='cloud'?React.createElement("div",{className:"space-y-1.5 max-h-64 overflow-y-auto"},loading?[React.createElement("div",{key:"l",className:"text-center py-8 text-xs text-zinc-500"},"Đang tải danh sách dự án...")]:projects.length===0?[React.createElement("div",{key:"e",className:"text-center py-8 text-xs text-zinc-500"},"Bạn chưa có dự án nào trên Cloud.")]:projects.map(function(p){return React.createElement("div",{key:p.id,onClick:function(){onOpenCloud(p.id,p.name);},className:"flex items-center justify-between p-3 bg-[#1e1e1e] hover:bg-zinc-800 rounded border border-zinc-800 transition cursor-pointer text-xs group"},[React.createElement("div",{key:"meta"},[React.createElement("div",{className:"font-bold text-slate-200 group-hover:text-teal-400"},p.name),React.createElement("div",{className:"text-[10px] text-zinc-500 mt-0.5"},"Dung lượng: "+(p.size_mb||0)+" MB | Cập nhật: "+new Date((p.updated_at||0)*1000).toLocaleString())]),React.createElement("button",{onClick:function(e){e.stopPropagation();onOpenCloud(p.id,p.name);},className:"px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"},"MỞ")]);})):React.createElement("div",{className:"py-4 text-center text-xs text-zinc-400 space-y-3"},[React.createElement("div",{key:"d",className:"text-zinc-500"},"Chọn tệp .sfs để mở dự án từ Local."),React.createElement("button",{key:"b",onClick:function(){onOpenLocal();},className:"px-4 py-2 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold text-xs transition"},"Chọn tệp .sfs ...")]),React.createElement("div",{className:"flex justify-end gap-2 text-xs mt-4 pt-3 border-t border-zinc-800"},React.createElement("button",{type:"button",onClick:onClose,className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"},"Hủy"))));};const SaveAsModal=({isOpen,onClose,projectName,onSaveCloud,onSaveLocal})=>{if(!isOpen)return null;const[name,setName]=useState(projectName||'');const[saveType,setSaveType]=useState('cloud');const handleSubmit=e=>{e.preventDefault();if(!name.trim())return;if(saveType==='cloud'){onSaveCloud(name.trim());}else{onSaveLocal(name.trim());}onClose();};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-teal-400 mb-4 uppercase"},"Lưu dưới tên khác (Save As...)"),/*#__PURE__*/React.createElement("form",{onSubmit:handleSubmit,className:"space-y-4"},[/*#__PURE__*/React.createElement("div",{key:"name-block"},[/*#__PURE__*/React.createElement("label",{className:"block text-[10px] text-zinc-400 uppercase font-bold mb-1"},"Tên dự án mới"),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Nhập tên mới...",required:true,value:name,onChange:e=>setName(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold",autoFocus:true})]),/*#__PURE__*/React.createElement("div",{key:"type-block"},[/*#__PURE__*/React.createElement("label",{className:"block text-[10px] text-zinc-400 uppercase font-bold mb-1.5"},"Phương thức lưu trữ"),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs"},[/*#__PURE__*/React.createElement("button",{key:"btn-cloud",type:"button",onClick:()=>setSaveType('cloud'),className:`py-2 rounded border flex flex-col items-center gap-1 font-semibold transition ${saveType==='cloud'?'bg-teal-950/60 border-teal-500 text-teal-300':'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400'}`},[/*#__PURE__*/React.createElement("span",{key:"title"},"☁️ Lưu Cloud"),/*#__PURE__*/React.createElement("span",{key:"desc",className:"text-[9px] font-normal text-zinc-500"},"Lưu lên server cá nhân")]),/*#__PURE__*/React.createElement("button",{key:"btn-local",type:"button",onClick:()=>setSaveType('local'),className:`py-2 rounded border flex flex-col items-center gap-1 font-semibold transition ${saveType==='local'?'bg-teal-950/60 border-teal-500 text-teal-300':'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400'}`},[/*#__PURE__*/React.createElement("span",{key:"title"},"💾 Tải về máy (.sfs)"),/*#__PURE__*/React.createElement("span",{key:"desc",className:"text-[9px] font-normal text-zinc-500"},"Tải tệp JSON dự án về máy")])])]),/*#__PURE__*/React.createElement("div",{key:"actions",className:"flex justify-end gap-2 text-xs pt-2"},[/*#__PURE__*/React.createElement("button",{key:"cancel",type:"button",onClick:onClose,className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"},"Hủy"),/*#__PURE__*/React.createElement("button",{key:"save",type:"submit",className:"px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold"},"Thực hiện lưu")])])));};const SystemManagerModal=({isOpen,onClose})=>{if(!isOpen)return null;const[users,setUsers]=useState([]);const[loading,setLoading]=useState(true);const[msg,setMsg]=useState('');const[error,setError]=useState('');const[editingQuotaUser,setEditingQuotaUser]=useState(null);const[newQuotaMb,setNewQuotaMb]=useState(500);useEffect(()=>{if(isOpen)loadUsers();},[isOpen]);const loadUsers=async()=>{setLoading(true);setError('');try{const data=await window.SonicAPI.listUsers();setUsers(data);}catch(err){setError(err.message||'Không thể tải danh sách người dùng hệ thống');}finally{setLoading(false);}};const handleSaveQuota=async userId=>{try{await window.SonicAPI.updateUserQuota(userId,parseInt(newQuotaMb));setMsg('Đã cập nhật hạn mức Quota thành công!');setEditingQuotaUser(null);loadUsers();}catch(err){setError(err.message||'Lỗi cập nhật Quota');}};const handleToggleRole=async user=>{const nextRole=user.role==='admin'?'standard':'admin';try{await window.SonicAPI.updateUserRole(user.id,nextRole,user.is_active);setMsg(`Đã đổi vai trò người dùng ${user.username} thành ${nextRole}`);loadUsers();}catch(err){setError(err.message||'Lỗi cập nhật vai trò');}};const handleDeleteUser=async userId=>{if(!confirm('Bạn có chắc chắn muốn xóa người dùng này khỏi hệ thống?'))return;try{await window.SonicAPI.deleteUser(userId);setMsg('Đã xóa người dùng thành công');loadUsers();}catch(err){setError(err.message||'Lỗi khi xóa người dùng');}};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-6 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-amber-400"},"⚙️ Quản Lý Hệ Thống & Phân Quyền Admin"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),msg&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"},msg),error&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"},error),/*#__PURE__*/React.createElement("div",{className:"mt-4 overflow-x-auto max-h-96 no-scrollbar"},loading?/*#__PURE__*/React.createElement("div",{className:"py-8 text-center text-slate-400 text-xs"},"Đang tải thông tin hệ thống..."):/*#__PURE__*/React.createElement("table",{className:"w-full text-left text-xs border-collapse"},/*#__PURE__*/React.createElement("thead",null,/*#__PURE__*/React.createElement("tr",{className:"border-b border-[#383838] text-slate-400 bg-[#1e1e1e]"},/*#__PURE__*/React.createElement("th",{className:"p-3"},"Tên Người Dùng"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Email"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Vai Trò"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Dung Lượng Sử Dụng"),/*#__PURE__*/React.createElement("th",{className:"p-3"},"Hạn Mức Quota"),/*#__PURE__*/React.createElement("th",{className:"p-3 text-right"},"Thao Tác"))),/*#__PURE__*/React.createElement("tbody",{className:"divide-y divide-[#333]"},users.map(u=>/*#__PURE__*/React.createElement("tr",{key:u.id,className:"hover:bg-[#2e2e2e]"},/*#__PURE__*/React.createElement("td",{className:"p-3 font-semibold text-teal-300"},u.username,u.must_change_password&&/*#__PURE__*/React.createElement("span",{className:"ml-2 text-xs bg-amber-900/60 text-amber-300 px-1.5 py-0.5 rounded"},"Mật khẩu gốc")),/*#__PURE__*/React.createElement("td",{className:"p-3 text-slate-300"},u.email),/*#__PURE__*/React.createElement("td",{className:"p-3 uppercase font-bold text-amber-400"},u.role),/*#__PURE__*/React.createElement("td",{className:"p-3"},u.used_mb," MB"),/*#__PURE__*/React.createElement("td",{className:"p-3"},editingQuotaUser===u.id?/*#__PURE__*/React.createElement("div",{className:"flex items-center space-x-1"},/*#__PURE__*/React.createElement("input",{type:"number",value:newQuotaMb,onChange:e=>setNewQuotaMb(e.target.value),className:"w-16 bg-[#1e1e1e] border border-[#444] rounded px-1 py-0.5 text-xs text-slate-200"}),/*#__PURE__*/React.createElement("span",null,"MB"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSaveQuota(u.id),className:"px-2 py-0.5 bg-teal-600 rounded text-xs"},"Lưu")):/*#__PURE__*/React.createElement("span",{className:"font-semibold"},u.quota_mb," MB")),/*#__PURE__*/React.createElement("td",{className:"p-3 text-right space-x-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingQuotaUser(u.id);setNewQuotaMb(u.quota_mb);},className:"px-2 py-1 bg-slate-700 hover:bg-slate-600 rounded text-xs"},"Sửa Quota"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleToggleRole(u),className:"px-2 py-1 bg-amber-700/60 hover:bg-amber-600 rounded text-xs"},"Đổi Role"),u.role!=='admin'&&/*#__PURE__*/React.createElement("button",{onClick:()=>handleDeleteUser(u.id),className:"px-2 py-1 bg-red-700/60 hover:bg-red-600 rounded text-xs"},"Xóa")))))))));};/* ═══════════════════════════════════════════════════════════════════ ABOUT / HELP / PREFERENCES MODALS (menu Help + Tools → Preferences) ═══════════════════════════════════════════════════════════════════ */const APP_VERSION='1.0.0';// khớp src-tauri/tauri.conf.json -const AboutModal=({isOpen,onClose})=>{if(!isOpen)return null;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 mb-4"},/*#__PURE__*/React.createElement("img",{src:"/static/img/scorefx_app.png",alt:"ScoreFX Studio",className:"w-12 h-12 rounded-lg bg-zinc-900 border border-zinc-700 shadow-lg object-contain p-0.5"}),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("h2",{className:"text-lg font-bold text-white"},"ScoreFX Studio"),/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-400"},"Professional DAW — v",APP_VERSION))),/*#__PURE__*/React.createElement("div",{className:"space-y-2 text-sm"},/*#__PURE__*/React.createElement("p",{className:"text-zinc-300 leading-relaxed"},"Phần mềm sản xuất âm nhạc (DAW) — soạn nhạc, ghi âm, chỉnh sửa MIDI/Audio, SoundFont & VST, trộn và master."),/*#__PURE__*/React.createElement("div",{className:"pt-2 border-t border-zinc-800 space-y-1 text-xs"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Developer"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-semibold"},"Lộc Phạm")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Email"),/*#__PURE__*/React.createElement("a",{href:"mailto:tranloclqd@gmail.com",className:"text-cyan-400 hover:underline"},"tranloclqd@gmail.com")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Version"),/*#__PURE__*/React.createElement("span",{className:"text-zinc-300"},APP_VERSION)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Build"),/*#__PURE__*/React.createElement("span",{className:"text-zinc-300"},"Standalone (Tauri v2 + PyInstaller)")))),/*#__PURE__*/React.createElement("div",{className:"mt-5 flex justify-end"},/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition"},"Đóng"))));};const HelpModal=({isOpen,onClose,lang})=>{if(!isOpen)return null;const vi=lang!=='en';const sections=vi?[{title:'🚀 Bắt đầu nhanh',body:'Nhấn phím Cách (Space) để Play/Pause. Nhấn phím / để dừng. Dùng nút Record (●) để ghi âm/MIDI. Tạo track mới từ menu Track hoặc nút "+ Add Track".'},{title:'🎹 MIDI & ARM',body:'Bật nút ARM đỏ trên track để nhận phím từ MIDI keyboard. Vào menu Tools → MIDI Devices để chọn thiết bị. Khi ARM + bấm phím, âm preview phát và VU meter nhảy theo trường độ.'},{title:'🎼 Piano Roll',body:'Nhấp đúp vào MIDI item để mở Piano Roll. Dùng Ctrl+scroll hoặc nút +/− để zoom. Nhấp để vẽ note, kéo để di chuyển, Ctrl+kéo để copy. Phím tắt: C (vẽ), E (tẩy), Space (nghe).'},{title:'🎛️ FX & Master',body:'Chọn track → nút FX để mở FX Rack (thêm EQ, compressor...). Bấm nút PWR ở Master để mở Mastering Panel: EQ 4 băng, compressor, limiter. Mọi thay đổi áp dụng realtime.'},{title:'🧪 SoundFont & VST',body:'Vào Tools → Plugin Manager để quét SoundFont (.sf2/.sf3) và VSTi. Track MIDI dùng SoundFont làm nhạc cụ — chọn instrument từ nút Synth trên track.'},{title:'🤖 AI',body:'Tools → Config AI Providers để cấu hình API (OpenAI, Gemini, Ollama...). Dùng AI Prompt Generator (nút ✨) để sinh MIDI/ý tưởng; AI MIDI Preset Manager để lưu preset.'},{title:'💾 Lưu & Xuất',body:'Ctrl+S lưu project (cloud/temp), Ctrl+Shift+S Save As. Export WAV qua nút Export — chọn vùng, format, bitrate rồi Render.'},{title:'⌨️ Phím tắt',body:'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline'}]:[{title:'🚀 Quick start',body:'Press Space to Play/Pause. Press / to stop. Use Record (●) to capture audio/MIDI. Add tracks from the Track menu or the "+ Add Track" button.'},{title:'🎹 MIDI & ARM',body:'Enable the red ARM button on a track to receive keys from a MIDI keyboard. Go to Tools → MIDI Devices to pick your device. When ARM + key press, preview audio plays and the VU meter animates for the note length.'},{title:'🎼 Piano Roll',body:'Double-click a MIDI item to open the Piano Roll. Use Ctrl+scroll or +/− buttons to zoom. Click to draw notes, drag to move, Ctrl+drag to copy. Shortcuts: C (draw), E (erase), Space (listen).'},{title:'🎛️ FX & Master',body:'Select a track → FX button to open the FX Rack (EQ, compressor...). Press the PWR button on the Master to open the Mastering Panel: 4-band EQ, compressor, limiter — all realtime.'},{title:'🧪 SoundFont & VST',body:'Tools → Plugin Manager scans SoundFonts (.sf2/.sf3) and VSTi. MIDI tracks use SoundFonts as instruments — pick one from the Synth button on the track.'},{title:'🤖 AI',body:'Tools → Config AI Providers to set up APIs (OpenAI, Gemini, Ollama...). Use the AI Prompt Generator (✨) to create MIDI/ideas; AI MIDI Preset Manager stores presets.'},{title:'💾 Save & Export',body:'Ctrl+S saves the project (cloud/temp), Ctrl+Shift+S Save As. Export WAV via the Export button — pick range, format, bitrate, then Render.'},{title:'⌨️ Shortcuts',body:'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline'}];return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-5 py-3 border-b border-zinc-800 shrink-0"},/*#__PURE__*/React.createElement("h2",{className:"text-base font-bold text-white"},vi?'Hướng dẫn sử dụng ScoreFX Studio':'ScoreFX Studio User Guide'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm"},"✕")),/*#__PURE__*/React.createElement("div",{className:"overflow-y-auto px-5 py-4 space-y-3"},sections.map((s,i)=>/*#__PURE__*/React.createElement("div",{key:i,className:"bg-zinc-900/60 border border-zinc-800 rounded-lg p-3"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-cyan-400 mb-1"},s.title),/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-300 leading-relaxed"},s.body)))),/*#__PURE__*/React.createElement("div",{className:"px-5 py-3 border-t border-zinc-800 flex justify-end shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition"},vi?'Đóng':'Close'))));};const PreferencesModal=({isOpen,onClose,prefs,onPrefsChange})=>{if(!isOpen)return null;const vi=prefs.language!=='en';const themes=[{id:'dark',name:vi?'Tối (mặc định)':'Dark (default)',swatch:'from-zinc-700 to-zinc-900',ring:'ring-cyan-400'},{id:'midnight',name:vi?'Đêm xanh':'Midnight',swatch:'from-sky-800 to-slate-950',ring:'ring-sky-400'},{id:'forest',name:vi?'Rừng xanh':'Forest',swatch:'from-emerald-700 to-green-950',ring:'ring-emerald-400'},{id:'violet',name:vi?'Tím':'Violet',swatch:'from-violet-700 to-purple-950',ring:'ring-violet-400'},{id:'graphite',name:vi?'Than chì':'Graphite',swatch:'from-zinc-600 to-neutral-900',ring:'ring-zinc-300'}];const set=(k,v)=>onPrefsChange({...prefs,[k]:v});return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md max-h-[85vh] overflow-y-auto p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-4"},/*#__PURE__*/React.createElement("h2",{className:"text-base font-bold text-white"},vi?'Tùy chọn (Preferences)':'Preferences'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm"},"✕")),/*#__PURE__*/React.createElement("div",{className:"mb-5"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2"},vi?'🎨 Chủ đề màu':'🎨 Theme'),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 gap-1.5"},themes.map(th=>/*#__PURE__*/React.createElement("button",{key:th.id,onClick:()=>set('theme',th.id),className:`flex items-center gap-2.5 px-2.5 py-2 rounded-lg border text-xs text-left transition ${prefs.theme===th.id?'border-cyan-500 bg-zinc-800':'border-zinc-700 bg-zinc-900 hover:bg-zinc-800'}`},/*#__PURE__*/React.createElement("span",{className:`w-6 h-6 rounded-md bg-gradient-to-br ${th.swatch} ring-1 ring-black/40 shrink-0 ${prefs.theme===th.id?`ring-2 ${th.ring}`:''}`}),/*#__PURE__*/React.createElement("span",{className:"text-zinc-200"},th.name),prefs.theme===th.id&&/*#__PURE__*/React.createElement("span",{className:"ml-auto text-cyan-400 text-[10px] font-bold"},"✓"))))),/*#__PURE__*/React.createElement("div",{className:"mb-5"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2"},vi?'🌐 Ngôn ngữ':'🌐 Language'),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},[{id:'vi',label:'Tiếng Việt'},{id:'en',label:'English'}].map(l=>/*#__PURE__*/React.createElement("button",{key:l.id,onClick:()=>set('language',l.id),className:`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.language===l.id?'border-cyan-500 bg-cyan-900/40 text-cyan-300':'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`},l.label))),/*#__PURE__*/React.createElement("p",{className:"text-[10px] text-zinc-500 mt-1"},vi?'Áp dụng ngay cho menu & hướng dẫn.':'Applies immediately to menus & guide.')),/*#__PURE__*/React.createElement("div",{className:"mb-5"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2"},vi?'🔠 Cỡ chữ nút bấm':'🔠 Button font size'),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},[{id:'sm',label:vi?'Nhỏ':'Small'},{id:'md',label:vi?'Vừa':'Medium'},{id:'lg',label:vi?'Lớn':'Large'}].map(f=>/*#__PURE__*/React.createElement("button",{key:f.id,onClick:()=>set('buttonFontSize',f.id),className:`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.buttonFontSize===f.id?'border-cyan-500 bg-cyan-900/40 text-cyan-300':'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`},f.label)))),/*#__PURE__*/React.createElement("div",{className:"flex justify-end gap-2 pt-2 border-t border-zinc-800"},/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded text-xs font-bold text-white transition"},vi?'Hủy':'Cancel'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition"},vi?'Lưu & Đóng':'Save & Close'))));};const AIPresetModal=({isOpen,onClose})=>{if(!isOpen)return null;const mgrRef=React.useRef(null);if(!mgrRef.current)mgrRef.current=new window.PromptTemplateManager();const mgr=mgrRef.current;const[presets,setPresets]=React.useState(()=>[...mgr.getPresets()]);const[search,setSearch]=React.useState('');const[filterCategory,setFilterCategory]=React.useState('ALL');const[showFavoritesOnly,setShowFavoritesOnly]=React.useState(false);const[editingPreset,setEditingPreset]=React.useState(null);const[syncing,setSyncing]=React.useState(false);const[formName,setFormName]=React.useState('');const[formKeywords,setFormKeywords]=React.useState('');const[formCategory,setFormCategory]=React.useState('Orchestral / Film Score');const[formBars,setFormBars]=React.useState(8);const[formBpm,setFormBpm]=React.useState(120);const[formScale,setFormScale]=React.useState('C Minor');const[formTemplate,setFormTemplate]=React.useState('');const[showGeneratorModal,setShowGeneratorModal]=React.useState(false);const[selectedCategory,setSelectedCategory]=React.useState('Orchestral / Film Score');const[isAddingCategory,setIsAddingCategory]=React.useState(false);const[newCategoryValue,setNewCategoryValue]=React.useState('');const[categoriesVersion,setCategoriesVersion]=React.useState(0);const presetCategories=React.useMemo(()=>{const fromPresets=[...new Set(presets.map(p=>p.category).filter(Boolean))];let saved=[];try{const raw=localStorage.getItem('midi_prompt_categories');saved=raw?JSON.parse(raw):[];}catch(e){saved=[];}return[...new Set([...fromPresets,...saved])].sort();},[presets,categoriesVersion]);const addNewCategory=cat=>{const val=(cat||'').trim();if(!val)return;setFormCategory(val);setSelectedCategory(val);try{const raw=localStorage.getItem('midi_prompt_categories');const list=raw?JSON.parse(raw):[];if(!list.includes(val)){list.push(val);localStorage.setItem('midi_prompt_categories',JSON.stringify(list));setCategoriesVersion(v=>v+1);}}catch(e){}};const refreshPresets=()=>{setPresets([...mgr.getPresets()]);};// Sync from backend on mount — merge into local presets, never overwrite +const AboutModal=({isOpen,onClose})=>{if(!isOpen)return null;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3 mb-4"},/*#__PURE__*/React.createElement("img",{src:"/static/img/scorefx_app.png",alt:"ScoreFX Studio",className:"w-12 h-12 rounded-lg bg-zinc-900 border border-zinc-700 shadow-lg object-contain p-0.5"}),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("h2",{className:"text-lg font-bold text-white"},"ScoreFX Studio"),/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-400"},"Professional DAW \u2014 v",APP_VERSION))),/*#__PURE__*/React.createElement("div",{className:"space-y-2 text-sm"},/*#__PURE__*/React.createElement("p",{className:"text-zinc-300 leading-relaxed"},"Ph\u1EA7n m\u1EC1m s\u1EA3n xu\u1EA5t \xE2m nh\u1EA1c (DAW) \u2014 so\u1EA1n nh\u1EA1c, ghi \xE2m, ch\u1EC9nh s\u1EEDa MIDI/Audio, SoundFont & VST, tr\u1ED9n v\xE0 master."),/*#__PURE__*/React.createElement("div",{className:"pt-2 border-t border-zinc-800 space-y-1 text-xs"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Developer"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-semibold"},"L\u1ED9c Ph\u1EA1m")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Email"),/*#__PURE__*/React.createElement("a",{href:"mailto:tranloclqd@gmail.com",className:"text-cyan-400 hover:underline"},"tranloclqd@gmail.com")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Version"),/*#__PURE__*/React.createElement("span",{className:"text-zinc-300"},APP_VERSION)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 w-20 shrink-0"},"Build"),/*#__PURE__*/React.createElement("span",{className:"text-zinc-300"},"Standalone (Tauri v2 + PyInstaller)")))),/*#__PURE__*/React.createElement("div",{className:"mt-5 flex justify-end"},/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition"},"\u0110\xF3ng"))));};const HelpModal=({isOpen,onClose,lang})=>{if(!isOpen)return null;const vi=lang!=='en';const sections=vi?[{title:'🚀 Bắt đầu nhanh',body:'Nhấn phím Cách (Space) để Play/Pause. Nhấn phím / để dừng. Dùng nút Record (●) để ghi âm/MIDI. Tạo track mới từ menu Track hoặc nút "+ Add Track".'},{title:'🎹 MIDI & ARM',body:'Bật nút ARM đỏ trên track để nhận phím từ MIDI keyboard. Vào menu Tools → MIDI Devices để chọn thiết bị. Khi ARM + bấm phím, âm preview phát và VU meter nhảy theo trường độ.'},{title:'🎼 Piano Roll',body:'Nhấp đúp vào MIDI item để mở Piano Roll. Dùng Ctrl+scroll hoặc nút +/− để zoom. Nhấp để vẽ note, kéo để di chuyển, Ctrl+kéo để copy. Phím tắt: C (vẽ), E (tẩy), Space (nghe).'},{title:'🎛️ FX & Master',body:'Chọn track → nút FX để mở FX Rack (thêm EQ, compressor...). Bấm nút PWR ở Master để mở Mastering Panel: EQ 4 băng, compressor, limiter. Mọi thay đổi áp dụng realtime.'},{title:'🧪 SoundFont & VST',body:'Vào Tools → Plugin Manager để quét SoundFont (.sf2/.sf3) và VSTi. Track MIDI dùng SoundFont làm nhạc cụ — chọn instrument từ nút Synth trên track.'},{title:'🤖 AI',body:'Tools → Config AI Providers để cấu hình API (OpenAI, Gemini, Ollama...). Dùng AI Prompt Generator (nút ✨) để sinh MIDI/ý tưởng; AI MIDI Preset Manager để lưu preset.'},{title:'💾 Lưu & Xuất',body:'Ctrl+S lưu project (cloud/temp), Ctrl+Shift+S Save As. Export WAV qua nút Export — chọn vùng, format, bitrate rồi Render.'},{title:'⌨️ Phím tắt',body:'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline'}]:[{title:'🚀 Quick start',body:'Press Space to Play/Pause. Press / to stop. Use Record (●) to capture audio/MIDI. Add tracks from the Track menu or the "+ Add Track" button.'},{title:'🎹 MIDI & ARM',body:'Enable the red ARM button on a track to receive keys from a MIDI keyboard. Go to Tools → MIDI Devices to pick your device. When ARM + key press, preview audio plays and the VU meter animates for the note length.'},{title:'🎼 Piano Roll',body:'Double-click a MIDI item to open the Piano Roll. Use Ctrl+scroll or +/− buttons to zoom. Click to draw notes, drag to move, Ctrl+drag to copy. Shortcuts: C (draw), E (erase), Space (listen).'},{title:'🎛️ FX & Master',body:'Select a track → FX button to open the FX Rack (EQ, compressor...). Press the PWR button on the Master to open the Mastering Panel: 4-band EQ, compressor, limiter — all realtime.'},{title:'🧪 SoundFont & VST',body:'Tools → Plugin Manager scans SoundFonts (.sf2/.sf3) and VSTi. MIDI tracks use SoundFonts as instruments — pick one from the Synth button on the track.'},{title:'🤖 AI',body:'Tools → Config AI Providers to set up APIs (OpenAI, Gemini, Ollama...). Use the AI Prompt Generator (✨) to create MIDI/ideas; AI MIDI Preset Manager stores presets.'},{title:'💾 Save & Export',body:'Ctrl+S saves the project (cloud/temp), Ctrl+Shift+S Save As. Export WAV via the Export button — pick range, format, bitrate, then Render.'},{title:'⌨️ Shortcuts',body:'Space: Play/Pause · /: Stop · Ctrl+Z/Y: Undo/Redo · Ctrl+C/X/V: Copy/Cut/Paste · Ctrl+S: Save · Ctrl+N: New · S: Split · Ctrl+E: Edit in new tab · Ctrl+wheel: zoom timeline'}];return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-5 py-3 border-b border-zinc-800 shrink-0"},/*#__PURE__*/React.createElement("h2",{className:"text-base font-bold text-white"},vi?'Hướng dẫn sử dụng ScoreFX Studio':'ScoreFX Studio User Guide'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm"},"\u2715")),/*#__PURE__*/React.createElement("div",{className:"overflow-y-auto px-5 py-4 space-y-3"},sections.map((s,i)=>/*#__PURE__*/React.createElement("div",{key:i,className:"bg-zinc-900/60 border border-zinc-800 rounded-lg p-3"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-cyan-400 mb-1"},s.title),/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-300 leading-relaxed"},s.body)))),/*#__PURE__*/React.createElement("div",{className:"px-5 py-3 border-t border-zinc-800 flex justify-end shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition"},vi?'Đóng':'Close'))));};const PreferencesModal=({isOpen,onClose,prefs,onPrefsChange})=>{if(!isOpen)return null;const vi=prefs.language!=='en';const themes=[{id:'dark',name:vi?'Tối (mặc định)':'Dark (default)',swatch:'from-zinc-700 to-zinc-900',ring:'ring-cyan-400'},{id:'midnight',name:vi?'Đêm xanh':'Midnight',swatch:'from-sky-800 to-slate-950',ring:'ring-sky-400'},{id:'forest',name:vi?'Rừng xanh':'Forest',swatch:'from-emerald-700 to-green-950',ring:'ring-emerald-400'},{id:'violet',name:vi?'Tím':'Violet',swatch:'from-violet-700 to-purple-950',ring:'ring-violet-400'},{id:'graphite',name:vi?'Than chì':'Graphite',swatch:'from-zinc-600 to-neutral-900',ring:'ring-zinc-300'}];const set=(k,v)=>onPrefsChange({...prefs,[k]:v});return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9999] flex items-center justify-center bg-black/70 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#1e1e24] border border-zinc-700 rounded-xl shadow-2xl w-full max-w-md max-h-[85vh] overflow-y-auto p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-4"},/*#__PURE__*/React.createElement("h2",{className:"text-base font-bold text-white"},vi?'Tùy chọn (Preferences)':'Preferences'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-6 h-6 rounded hover:bg-zinc-700 text-zinc-400 hover:text-white text-sm"},"\u2715")),/*#__PURE__*/React.createElement("div",{className:"mb-5"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2"},vi?'🎨 Chủ đề màu':'🎨 Theme'),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 gap-1.5"},themes.map(th=>/*#__PURE__*/React.createElement("button",{key:th.id,onClick:()=>set('theme',th.id),className:`flex items-center gap-2.5 px-2.5 py-2 rounded-lg border text-xs text-left transition ${prefs.theme===th.id?'border-cyan-500 bg-zinc-800':'border-zinc-700 bg-zinc-900 hover:bg-zinc-800'}`},/*#__PURE__*/React.createElement("span",{className:`w-6 h-6 rounded-md bg-gradient-to-br ${th.swatch} ring-1 ring-black/40 shrink-0 ${prefs.theme===th.id?`ring-2 ${th.ring}`:''}`}),/*#__PURE__*/React.createElement("span",{className:"text-zinc-200"},th.name),prefs.theme===th.id&&/*#__PURE__*/React.createElement("span",{className:"ml-auto text-cyan-400 text-[10px] font-bold"},"\u2713"))))),/*#__PURE__*/React.createElement("div",{className:"mb-5"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2"},vi?'🌐 Ngôn ngữ':'🌐 Language'),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},[{id:'vi',label:'Tiếng Việt'},{id:'en',label:'English'}].map(l=>/*#__PURE__*/React.createElement("button",{key:l.id,onClick:()=>set('language',l.id),className:`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.language===l.id?'border-cyan-500 bg-cyan-900/40 text-cyan-300':'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`},l.label))),/*#__PURE__*/React.createElement("p",{className:"text-[10px] text-zinc-500 mt-1"},vi?'Áp dụng ngay cho menu & hướng dẫn.':'Applies immediately to menus & guide.')),/*#__PURE__*/React.createElement("div",{className:"mb-5"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-zinc-400 uppercase tracking-wider mb-2"},vi?'🔠 Cỡ chữ nút bấm':'🔠 Button font size'),/*#__PURE__*/React.createElement("div",{className:"flex gap-1.5"},[{id:'sm',label:vi?'Nhỏ':'Small'},{id:'md',label:vi?'Vừa':'Medium'},{id:'lg',label:vi?'Lớn':'Large'}].map(f=>/*#__PURE__*/React.createElement("button",{key:f.id,onClick:()=>set('buttonFontSize',f.id),className:`flex-1 px-2 py-1.5 rounded-lg border text-xs font-semibold transition ${prefs.buttonFontSize===f.id?'border-cyan-500 bg-cyan-900/40 text-cyan-300':'border-zinc-700 bg-zinc-900 text-zinc-400 hover:bg-zinc-800'}`},f.label)))),/*#__PURE__*/React.createElement("div",{className:"flex justify-end gap-2 pt-2 border-t border-zinc-800"},/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 rounded text-xs font-bold text-white transition"},vi?'Hủy':'Cancel'),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"px-4 py-1.5 bg-cyan-700 hover:bg-cyan-600 rounded text-xs font-bold text-white transition"},vi?'Lưu & Đóng':'Save & Close'))));};const AIPresetModal=({isOpen,onClose})=>{if(!isOpen)return null;const mgrRef=React.useRef(null);if(!mgrRef.current)mgrRef.current=new window.PromptTemplateManager();const mgr=mgrRef.current;const[presets,setPresets]=React.useState(()=>[...mgr.getPresets()]);const[search,setSearch]=React.useState('');const[filterCategory,setFilterCategory]=React.useState('ALL');const[showFavoritesOnly,setShowFavoritesOnly]=React.useState(false);const[editingPreset,setEditingPreset]=React.useState(null);const[syncing,setSyncing]=React.useState(false);const[formName,setFormName]=React.useState('');const[formKeywords,setFormKeywords]=React.useState('');const[formCategory,setFormCategory]=React.useState('Orchestral / Film Score');const[formBars,setFormBars]=React.useState(8);const[formBpm,setFormBpm]=React.useState(120);const[formScale,setFormScale]=React.useState('C Minor');const[formTemplate,setFormTemplate]=React.useState('');const[showGeneratorModal,setShowGeneratorModal]=React.useState(false);const[selectedCategory,setSelectedCategory]=React.useState('Orchestral / Film Score');const[isAddingCategory,setIsAddingCategory]=React.useState(false);const[newCategoryValue,setNewCategoryValue]=React.useState('');const[categoriesVersion,setCategoriesVersion]=React.useState(0);const presetCategories=React.useMemo(()=>{const fromPresets=[...new Set(presets.map(p=>p.category).filter(Boolean))];let saved=[];try{const raw=localStorage.getItem('midi_prompt_categories');saved=raw?JSON.parse(raw):[];}catch(e){saved=[];}return[...new Set([...fromPresets,...saved])].sort();},[presets,categoriesVersion]);const addNewCategory=cat=>{const val=(cat||'').trim();if(!val)return;setFormCategory(val);setSelectedCategory(val);try{const raw=localStorage.getItem('midi_prompt_categories');const list=raw?JSON.parse(raw):[];if(!list.includes(val)){list.push(val);localStorage.setItem('midi_prompt_categories',JSON.stringify(list));setCategoriesVersion(v=>v+1);}}catch(e){}};const refreshPresets=()=>{setPresets([...mgr.getPresets()]);};// Sync from backend on mount — merge into local presets, never overwrite React.useEffect(()=>{if(!window.SonicAPI)return;setSyncing(true);window.SonicAPI.getAIPresets().then(data=>{if(!data||!data.presets||data.presets.length===0)return;var existing=mgr.presets;var existingIds=new Set(existing.map(function(p){return p.id;}));var merged=existing.slice();data.presets.forEach(function(bp){if(!existingIds.has(bp.id)){merged.push(bp);existingIds.add(bp.id);}});mgr.presets=merged;setPresets(merged);}).catch(function(){}).finally(function(){setSyncing(false);});},[]);// Listen for GENERATOR_PRESET_DATA from the iframe generator modal React.useEffect(()=>{const handleMessage=event=>{if(event.data&&event.data.type==='GENERATOR_PRESET_DATA'){const d=event.data;setFormName(d.name||'');setFormCategory(d.category||'Orchestral / Film Score');setSelectedCategory(d.category||'Orchestral / Film Score');setFormKeywords(d.keywords||'');setFormBars(parseInt(d.default_bars)||8);setFormBpm(parseInt(d.default_bpm)||120);setFormScale(d.default_scale||'C Minor');setFormTemplate(d.template||'');setEditingPreset('new');setShowGeneratorModal(false);setIsAddingCategory(false);setNewCategoryValue('');refreshPresets();}};window.addEventListener('message',handleMessage);return()=>window.removeEventListener('message',handleMessage);},[]);const savePresets=newPresets=>{setPresets(newPresets);mgr.presets=newPresets;mgr.savePresets();// Sync to backend if available const userDefined=newPresets.filter(p=>p.is_user_defined);if(window.SonicAPI&&userDefined.length>0){userDefined.forEach(p=>{window.SonicAPI.saveAIPreset(p).catch(()=>{});});}};const handleEdit=p=>{setEditingPreset(p);setFormName(p.name);setFormKeywords(p.keywords.join(', '));setFormCategory(p.category);setSelectedCategory(p.category);setFormBars(p.default_bars);setFormBpm(p.default_bpm);setFormScale(p.default_scale);setFormTemplate(p.system_instruction_template);setIsAddingCategory(false);setNewCategoryValue('');};const handleNew=()=>{setEditingPreset('new');setFormName('');setFormKeywords('');setFormCategory('Orchestral / Film Score');setSelectedCategory('Orchestral / Film Score');setFormBars(8);setFormBpm(120);setFormScale('C Minor');setFormTemplate('');setIsAddingCategory(false);setNewCategoryValue('');};const handleNewStructured=()=>{refreshPresets();setShowGeneratorModal(true);};const handleToggleFav=id=>{mgr.toggleFavorite(id);setPresets([...mgr.getPresets()]);const p=mgr.presets.find(x=>x.id===id);if(p&&p.is_user_defined&&window.SonicAPI){window.SonicAPI.saveAIPreset(p).catch(()=>{});}};const handleDelete=id=>{const p=presets.find(x=>x.id===id);if(p&&p.is_user_defined&&window.SonicAPI){window.SonicAPI.deleteAIPreset(id).catch(()=>{});}mgr.deletePreset(id);setPresets([...mgr.getPresets()]);showToast('Đã xóa preset.','info');};const handleSaveForm=e=>{e.preventDefault();if(!formName.trim()||!formTemplate.trim()){showToast('Vui lòng điền đầy đủ tên và mẫu gợi ý.','warning');return;}const keywordsArray=formKeywords.split(',').map(k=>k.trim()).filter(Boolean);const presetObj={id:editingPreset==='new'?'preset_'+Date.now():editingPreset.id,name:formName.trim(),keywords:keywordsArray,category:formCategory,default_bars:parseInt(formBars)||8,default_bpm:parseInt(formBpm)||120,default_scale:formScale,system_instruction_template:formTemplate.trim(),is_user_defined:true,is_favorite:editingPreset==='new'?false:editingPreset.is_favorite||false,created_at:editingPreset==='new'?new Date().toISOString():editingPreset.created_at};mgr.saveUserPreset(presetObj);setPresets([...mgr.getPresets()]);setEditingPreset(null);if(window.SonicAPI){window.SonicAPI.saveAIPreset(presetObj).catch(()=>{});}showToast('Đã lưu preset thành công!','success');};const categories=['ALL','★ Yêu thích','Người dùng',...new Set(presets.map(p=>p.category))];const filtered=presets.filter(p=>{const matchesSearch=p.name.toLowerCase().includes(search.toLowerCase())||p.keywords.some(k=>k.toLowerCase().includes(search.toLowerCase()));let matchesCategory;if(filterCategory==='ALL'){matchesCategory=true;}else if(filterCategory==='★ Yêu thích'){matchesCategory=p.is_favorite;}else if(filterCategory==='Người dùng'){matchesCategory=p.is_user_defined;}else{matchesCategory=p.category===filterCategory;}if(showFavoritesOnly)matchesCategory=matchesCategory&&p.is_favorite;return matchesSearch&&matchesCategory;});return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4 animate-fade-in"},/*#__PURE__*/React.createElement("div",{className:"bg-[#18181b] border border-zinc-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden text-zinc-100"},/*#__PURE__*/React.createElement("div",{className:"p-4 border-b border-zinc-800 flex items-center justify-between shrink-0 bg-[#202024]"},/*#__PURE__*/React.createElement("h2",{className:"text-sm font-bold tracking-wider uppercase text-purple-400 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-4 h-4"}),"AI Prompt Preset Manager",syncing&&/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-zinc-500 ml-2"},"đang đồng bộ...")),/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingPreset(null);onClose();},className:"text-zinc-400 hover:text-zinc-200 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto p-4 flex gap-4 min-h-0"},!editingPreset?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col min-w-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 shrink-0"},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Tìm kiếm preset hoặc từ khóa...",value:search,onChange:e=>setSearch(e.target.value),className:"flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"}),/*#__PURE__*/React.createElement("select",{value:filterCategory,onChange:e=>setFilterCategory(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"},categories.map(c=>/*#__PURE__*/React.createElement("option",{key:c,value:c},c==='ALL'?'Tất cả danh mục':c))),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowFavoritesOnly(!showFavoritesOnly),className:`px-2.5 py-1 rounded text-xs font-bold transition shrink-0 ${showFavoritesOnly?'bg-yellow-700 text-yellow-300':'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'}`,title:"Chỉ hiện yêu thích"},/*#__PURE__*/React.createElement("i",{"data-lucide":"star",className:"w-3.5 h-3.5 inline-block mr-1"}),"★"),/*#__PURE__*/React.createElement("button",{onClick:handleNew,className:"px-3 py-1 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold flex items-center gap-1.5 shadow transition shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3.5 h-3.5"}),"Tạo mới")),/*#__PURE__*/React.createElement("div",{className:"flex-1 border border-zinc-800 rounded bg-[#0f0f12] overflow-y-auto"},filtered.length===0?/*#__PURE__*/React.createElement("div",{className:"p-8 text-center text-zinc-500 text-xs italic"},"Không tìm thấy preset nào."):/*#__PURE__*/React.createElement("table",{className:"w-full text-left text-xs border-collapse"},/*#__PURE__*/React.createElement("thead",{className:"bg-[#1f1f23] text-zinc-400 font-bold border-b border-zinc-800"},/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-8"},""),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/4"},"Tên Preset"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/4"},"Từ khóa kích hoạt"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6"},"Số Bar"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6"},"BPM"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6 text-right"},"Hành động"))),/*#__PURE__*/React.createElement("tbody",null,filtered.map(p=>/*#__PURE__*/React.createElement("tr",{key:p.id,className:"border-b border-zinc-800/50 hover:bg-zinc-850"},/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-center"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleToggleFav(p.id),className:`transition ${p.is_favorite?'text-yellow-400':'text-zinc-600 hover:text-zinc-400'}`,title:p.is_favorite?'Bỏ yêu thích':'Đánh dấu yêu thích'},p.is_favorite?"★":"☆")),/*#__PURE__*/React.createElement("td",{className:"p-2.5 font-semibold text-purple-300"},p.name),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400 font-mono text-[11px] truncate max-w-[150px]"},p.keywords.join(', ')),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400"},p.default_bars," Bars"),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400"},p.default_bpm," BPM"),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-right flex items-center justify-end gap-1.5 h-full"},/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>handleEdit(p),className:"px-2 py-0.5 bg-zinc-850 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[10px]"},"Sửa"),p.is_user_defined&&/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>handleDelete(p.id),className:"px-2 py-0.5 bg-red-950/40 hover:bg-red-800 text-red-400 rounded border border-red-900 text-[10px]"},"Xóa"))))))))):/*#__PURE__*/React.createElement("form",{onSubmit:handleSaveForm,className:"flex-1 flex flex-col gap-3 min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-zinc-400 shrink-0 border-b border-zinc-800 pb-1"},editingPreset==='new'?"TẠO PRESET MỚI":`SỬA PRESET: ${editingPreset.name}`),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Tên Preset"),/*#__PURE__*/React.createElement("input",{type:"text",value:formName,onChange:e=>setFormName(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Danh mục"),isAddingCategory?/*#__PURE__*/React.createElement("div",{className:"flex gap-2"},/*#__PURE__*/React.createElement("input",{type:"text",value:newCategoryValue,onChange:e=>setNewCategoryValue(e.target.value),onBlur:()=>{if(newCategoryValue.trim()){addNewCategory(newCategoryValue.trim());}setIsAddingCategory(false);},onKeyDown:e=>{if(e.key==='Enter'){e.preventDefault();if(newCategoryValue.trim()){addNewCategory(newCategoryValue.trim());}setIsAddingCategory(false);}else if(e.key==='Escape'){setIsAddingCategory(false);}},autoFocus:true,className:"flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200",placeholder:"Nhập danh mục mới..."})):/*#__PURE__*/React.createElement("select",{value:selectedCategory,onChange:e=>{if(e.target.value==='__add_new__'){setIsAddingCategory(true);setNewCategoryValue('');}else{setSelectedCategory(e.target.value);setFormCategory(e.target.value);}},className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"},presetCategories.map(c=>/*#__PURE__*/React.createElement("option",{key:c,value:c},c)),/*#__PURE__*/React.createElement("option",{value:'__add_new__'},"+ Nhập danh mục mới...")))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Từ khóa kích hoạt (ngăn cách bằng dấu phẩy)"),/*#__PURE__*/React.createElement("input",{type:"text",value:formKeywords,onChange:e=>setFormKeywords(e.target.value),placeholder:"Ví dụ: epic orchestra, hoành tráng, nhạc phim epic",className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-3"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Số Bars mặc định"),/*#__PURE__*/React.createElement("input",{type:"number",value:formBars,onChange:e=>setFormBars(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"BPM mặc định"),/*#__PURE__*/React.createElement("input",{type:"number",value:formBpm,onChange:e=>setFormBpm(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Âm giai (Scale) mặc định"),/*#__PURE__*/React.createElement("input",{type:"text",value:formScale,onChange:e=>setFormScale(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 flex-1 min-h-0"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"System Prompt Template / Luật soạn nhạc"),/*#__PURE__*/React.createElement("textarea",{value:formTemplate,onChange:e=>setFormTemplate(e.target.value),rows:6,className:"flex-1 bg-zinc-900 border border-zinc-700 rounded p-2.5 text-xs outline-none focus:border-purple-600 text-zinc-200 font-mono resize-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 shrink-0 pt-2 border-t border-zinc-800"},/*#__PURE__*/React.createElement("button",{type:"button",onClick:handleNewStructured,className:"px-3 py-1.5 bg-emerald-800 hover:bg-emerald-700 text-emerald-300 border border-emerald-700 rounded text-xs transition"},"Tạo preset với cấu trúc"),/*#__PURE__*/React.createElement("div",{className:"flex-1"}),/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>setEditingPreset(null),className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border border-zinc-700 rounded text-xs transition"},"Quay lại"),/*#__PURE__*/React.createElement("button",{type:"submit",className:"px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold shadow transition"},"Lưu Preset")))),/*#__PURE__*/React.createElement("div",{className:"p-4 border-t border-zinc-800 flex justify-end shrink-0 bg-[#202024]"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingPreset(null);onClose();},className:"px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs font-bold shadow transition"},"Đóng")))),showGeneratorModal?/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-black/80 p-4",onClick:e=>{if(e.target===e.currentTarget){refreshPresets();setShowGeneratorModal(false);}}},/*#__PURE__*/React.createElement("div",{className:"w-full h-full max-w-6xl max-h-[90vh] bg-[#13141a] border border-zinc-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between p-3 border-b border-zinc-800 shrink-0"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400"},"AI Prompt Generator"),/*#__PURE__*/React.createElement("button",{onClick:()=>{refreshPresets();setShowGeneratorModal(false);},className:"text-zinc-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("iframe",{src:"/ai-prompt-generator",className:"flex-1 w-full border-0 bg-white",title:"AI Prompt Generator"}))):null);};const PianoRollTabEditor=({st,zoom,bpm,viewportWidth,activeTracks,onClose,onUpdateNotes,onSaveNotes,setSubTabs,onPlayPause,onStop,isPlaying,playPreviewNote,showToast,midiDevices,recordingState,recTempMidiNotes,onRecord,selectedMidiInputId,onMidiInputSelect,activeMidiPitches,onInstrumentSelect,onRescheduleMidi,onSeekPlayhead,snapValue,onSnapChange,onRealtimePlay,onCopyNotes,clipboardNotes,onExportMidiAudio})=>{const[activeRollTool,setActiveRollTool]=React.useState('select');const[renderTick,setRenderTick]=React.useState(0);const[ccMode,setCcMode]=React.useState('velocity');const[rollZoom,setRollZoom]=React.useState(()=>{// Persist zoom piano roll qua phiên (localStorage) — user yêu cầu giữ kích @@ -917,10 +945,10 @@ const specMods=(spectrumModules?spectrumModules():null)||(getModule?[getModule() const applyAll=applyTo||(fn=>{if(typeof window!=='undefined'&&window.__getTrackFxModule){const a=window.__getTrackFxModule(track.id,'eqpro');if(a)fn(a);}if(typeof window!=='undefined'&&window.__getTrackSfFxModule){const b=window.__getTrackSfFxModule(track.id,'eqpro');if(b)fn(b);}});const onDown=e=>{const cv=canvasRef.current,w=cv.clientWidth,h=cv.clientHeight;const{x,y}=toLocal(e);const s=st.current;let hit=false;for(let i=0;it+1);};const onMove=e=>{const s=st.current;if(s.dragId===null)return;const cv=canvasRef.current,w=cv.clientWidth,h=cv.clientHeight;const{x,y}=toLocal(e);const mx=Math.max(0,Math.min(w,x)),my=Math.max(0,Math.min(h,y));const b=s.bands[s.dragId];if(s.mode==='center'){const f=eqproClamp(parseFloat(eqproXToFreq(mx,w).toFixed(1)),EQPRO_F_MIN,EQPRO_F_MAX);// Filters without a Gain control (highpass/lowpass/notch/bandpass) stay on // the 0 dB axis — only frequency is draggable for them. const g=eqproBandHasGain(b.type)?eqproClamp(parseFloat(eqproYToGain(my,h).toFixed(1)),-EQPRO_MAX_DB,EQPRO_MAX_DB):0;b.freq=f;b.gain=g;applyAll(mm=>mm.setBand(s.dragId,{freq:f,gain:g}));}else if(s.mode==='wing'){const q=eqproWingToQ(Math.abs(mx-eqproFreqToX(b.freq,w)));b.q=q;applyAll(mm=>mm.setBand(s.dragId,{q}));}setTick(t=>t+1);};const onUp=e=>{const s=st.current;if(s.dragId!==null)commit();s.dragId=null;s.mode=null;try{canvasRef.current.releasePointerCapture(e.pointerId);}catch(err){}};const onWheel=e=>{e.preventDefault();const s=st.current;if(s.selected===null)return;const b=s.bands[s.selected];b.q=eqproClamp(parseFloat((b.q+(e.deltaY<0?0.2:-0.2)).toFixed(1)),0.1,18);applyAll(mm=>mm.setBand(s.selected,{q:b.q}));setTick(t=>t+1);};const onDblClick=e=>{const cv=canvasRef.current,w=cv.clientWidth,h=cv.clientHeight;const{x,y}=toLocal(e);const s=st.current;const hitIdx=s.bands.findIndex(b=>Math.hypot(x-eqproFreqToX(b.freq,w),y-eqproGainToY(b.gain,h))<=10);if(hitIdx!==-1){s.bands.splice(hitIdx,1);if(s.selected===hitIdx)s.selected=null;else if(s.selected!==null&&s.selected>hitIdx)s.selected--;}else{if(s.bands.length>=EQPRO_MAX_BANDS)return;s.bands.push({type:'peaking',freq:eqproClamp(parseFloat(eqproXToFreq(x,w).toFixed(1)),EQPRO_F_MIN,EQPRO_F_MAX),gain:eqproClamp(parseFloat(eqproYToGain(y,h).toFixed(1)),-EQPRO_MAX_DB,EQPRO_MAX_DB),q:1.2,active:true});s.selected=s.bands.length-1;}applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);};const sel=st.current.selected!==null?st.current.bands[st.current.selected]:null;const selIdx=st.current.selected;const cvW=canvasRef.current?canvasRef.current.clientWidth:460;const cvH=240;const hudColor=selIdx!==null?EQPRO_BAND_COLORS[selIdx%EQPRO_BAND_COLORS.length]:EQPRO_BAND_COLORS[0];const hudXY=(()=>{if(st.current.hudPos){// Clamp so the HUD always stays inside the canvas area (buttons clickable). -return{hx:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientWidth:460)-268,st.current.hudPos.hx)),hy:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientHeight:240)-225,st.current.hudPos.hy))};}if(!sel)return null;const nx=eqproFreqToX(sel.freq,cvW),ny=eqproGainToY(sel.gain,cvH);let hx=nx-130,hy=ny-195;if(hx<8)hx=8;if(hx>cvW-268)hx=cvW-268;if(hy<8)hy=ny+24;return{hx,hy};})();return/*#__PURE__*/React.createElement("div",{className:"space-y-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[11px] font-mono"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Bands: ",/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold"},st.current.bands.length),"/8"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Amount:"),/*#__PURE__*/React.createElement("input",{type:"range",min:0,max:200,value:st.current.amount,onChange:e=>{st.current.amount=parseInt(e.target.value);applyAll(mm=>mm.setAmount(st.current.amount));commit();setTick(t=>t+1);},className:"w-20 h-1 cursor-pointer",style:{accentColor:'#2dd4bf'}}),/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold w-10"},st.current.amount,"%"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;s.bands=JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-slate-800 hover:bg-red-900/60 text-slate-300 border border-slate-700 rounded text-[10px]"},"Reset"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;if(s.bands.length>=EQPRO_MAX_BANDS)return;s.bands.push({type:'peaking',freq:1000,gain:0,q:1.0,active:true});s.selected=s.bands.length-1;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-teal-900/50 hover:bg-teal-800 text-teal-300 border border-teal-700 rounded text-[10px]"},"+ Band"))),/*#__PURE__*/React.createElement("div",{className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-60 block cursor-crosshair rounded-lg border border-slate-800 bg-slate-950",onPointerDown:onDown,onPointerMove:onMove,onPointerUp:onUp,onWheel:onWheel,onDoubleClick:onDblClick}),sel&&hudXY&&/*#__PURE__*/React.createElement("div",{className:"absolute z-10 w-64 rounded-xl p-3 text-[11px] font-mono space-y-2 pointer-events-auto cursor-move",style:{left:hudXY.hx,top:hudXY.hy,background:'rgba(15,23,42,0.94)',border:'1px solid rgba(45,212,191,0.3)',boxShadow:'0 10px 30px rgba(0,0,0,0.8)',backdropFilter:'blur(12px)'},onPointerDown:e=>{const s=st.current;s.dragHud=true;const r=e.currentTarget.getBoundingClientRect();s.hudOffsetX=e.clientX-r.left;s.hudOffsetY=e.clientY-r.top;e.currentTarget.setPointerCapture(e.pointerId);},onPointerMove:e=>{const s=st.current;if(!s.dragHud)return;const cv=canvasRef.current;const w=cv?cv.clientWidth:460;s.hudPos={hx:Math.max(0,Math.min(w-268,e.clientX-s.hudOffsetX-(cv?cv.getBoundingClientRect().left:0))),hy:Math.max(0,e.clientY-s.hudOffsetY-(cv?cv.getBoundingClientRect().top:0))};setTick(t=>t+1);},onPointerUp:e=>{st.current.dragHud=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-700/80 pb-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"w-4 h-4 rounded-full text-slate-950 font-bold flex items-center justify-center text-[10px]",style:{background:hudColor.badge}},selIdx+1),/*#__PURE__*/React.createElement("span",{className:"font-bold text-white uppercase tracking-wider"},"Band ",selIdx+1)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{title:"Bypass band",onClick:()=>{const s=st.current;s.bands[s.selected].active=!s.bands[s.selected].active;applyAll(mm=>mm.setBand(s.selected,{active:s.bands[s.selected].active}));commit();setTick(t=>t+1);},className:`w-5 h-5 rounded flex items-center justify-center text-[10px] ${sel.active?'bg-slate-800 text-slate-400':'bg-amber-600 text-white'}`},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-power-off"})),/*#__PURE__*/React.createElement("button",{title:"Delete band",onClick:()=>{const s=st.current;s.bands.splice(s.selected,1);s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"w-5 h-5 rounded bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white flex items-center justify-center text-[10px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"})))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[9px] text-slate-400 uppercase tracking-widest mb-1"},"Filter Shape"),/*#__PURE__*/React.createElement("select",{value:sel.type,onChange:ev=>{const s=st.current;s.bands[s.selected].type=ev.target.value;applyAll(mm=>mm.setBand(s.selected,{type:ev.target.value}));commit();setTick(t=>t+1);},className:"w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none"},['peaking','highpass','lowpass','lowshelf','highshelf','notch','bandpass'].map(t=>/*#__PURE__*/React.createElement("option",{key:t,value:t},t==='peaking'?'Bell / Peaking':t==='highpass'?'Low Cut / High Pass':t==='lowpass'?'High Cut / Low Pass':t==='lowshelf'?'Low Shelf':t==='highshelf'?'High Shelf':t==='notch'?'Notch / Band Stop':'Band Pass')))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 text-center bg-slate-950/80 p-2 rounded-lg border border-slate-800"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"FREQ"),/*#__PURE__*/React.createElement("div",{className:"text-teal-300 font-bold text-[11px]"},sel.freq>=1000?(sel.freq/1000).toFixed(2)+' kHz':Math.round(sel.freq)+' Hz')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"GAIN"),/*#__PURE__*/React.createElement("div",{className:"text-amber-400 font-bold text-[11px]"},eqproBandHasGain(sel.type)?(sel.gain>0?'+':'')+sel.gain.toFixed(1)+' dB':'0.0 dB')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"Q"),/*#__PURE__*/React.createElement("div",{className:"text-purple-400 font-bold text-[11px]"},sel.q.toFixed(1)))),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 text-center"},"Drag center: Freq & Gain · Wings/Wheel: Q · Dbl-click: add/delete"))));};const ExportModal=({open,onClose,exportSettings,setExportSettings,isExporting,exportProgress,onExport})=>{if(!open)return null;const pickDir=async()=>{try{const r=await fetch(`${API_BASE_URL}/api/v1/plugins/pick-dir`,{method:'POST'});if(r.ok){const d=await r.json();if(d&&d.path)setExportSettings(p=>({...p,exportDir:d.path}));}}catch(e){}};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3.5 h-3.5"})," EXPORT"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"p-4 max-h-[72vh] overflow-y-auto space-y-3"},/*#__PURE__*/React.createElement("div",{className:"bg-[#141414] border border-zinc-800 rounded px-2 py-1.5 space-y-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Tên file export"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.fileName,onChange:e=>setExportSettings(p=>({...p,fileName:e.target.value})),placeholder:"Để trống = dùng tên mặc định",className:"w-full bg-[#0d0d0d] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-end gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex-1 min-w-0"},/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Thư mục export"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 bg-[#0d0d0d] border border-zinc-800 rounded px-1.5 py-0.5 min-h-[22px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-cyan-400 text-[10px] shrink-0"}),exportSettings.exportDir?/*#__PURE__*/React.createElement("a",{className:"text-cyan-400 underline truncate cursor-pointer",title:exportSettings.exportDir,onClick:pickDir},exportSettings.exportDir):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 truncate"},"Chưa chọn — xuất sẽ mở Save-As"))),/*#__PURE__*/React.createElement("button",{onClick:pickDir,className:"shrink-0 px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-[10px] font-bold flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[10px]"})," Chọn thư mục")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.rememberExportDir,onChange:e=>setExportSettings(p=>({...p,rememberExportDir:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Lưu thư mục export làm mặc định"))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Nguồn"),/*#__PURE__*/React.createElement("select",{value:exportSettings.source,onChange:e=>setExportSettings(p=>({...p,source:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"project"},"Project (Mix)"),/*#__PURE__*/React.createElement("option",{value:"track_mix"},"Track Selection"),/*#__PURE__*/React.createElement("option",{value:"active_clip"},"Active Clip"),/*#__PURE__*/React.createElement("option",{value:"clip_selection"},"Clip Selection"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Định dạng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.format,onChange:e=>setExportSettings(p=>({...p,format:e.target.value,sampleRate:'44100',bitDepth:'16',quality:'44khz'})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"wav"},"WAV"),/*#__PURE__*/React.createElement("option",{value:"mp3"},"MP3"),/*#__PURE__*/React.createElement("option",{value:"ogg"},"OGG")))),exportSettings.format==='wav'?/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"SR (Hz)"),/*#__PURE__*/React.createElement("select",{value:exportSettings.sampleRate,onChange:e=>setExportSettings(p=>({...p,sampleRate:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44100"},"44100"),/*#__PURE__*/React.createElement("option",{value:"48000"},"48000"),/*#__PURE__*/React.createElement("option",{value:"96000"},"96000"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Bit"),/*#__PURE__*/React.createElement("select",{value:exportSettings.bitDepth,onChange:e=>setExportSettings(p=>({...p,bitDepth:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"16"},"16"),/*#__PURE__*/React.createElement("option",{value:"24"},"24"),/*#__PURE__*/React.createElement("option",{value:"float"},"32 float"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Kênh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))):/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Chất lượng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.quality,onChange:e=>setExportSettings(p=>({...p,quality:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44khz"},"44kHz"),/*#__PURE__*/React.createElement("option",{value:"lossless"},"Lossless"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Kênh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.dither,onChange:e=>setExportSettings(p=>({...p,dither:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Dither TPDF (PCM 16/24)")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.normalize,onChange:e=>setExportSettings(p=>({...p,normalize:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Normalize")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.monoCheck,onChange:e=>setExportSettings(p=>({...p,monoCheck:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Cảnh báo mono (side/mid)")),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Normalize target"),/*#__PURE__*/React.createElement("select",{value:exportSettings.normalizeTarget,onChange:e=>setExportSettings(p=>({...p,normalizeTarget:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"-1dbtp"},"-1 dBTP"),/*#__PURE__*/React.createElement("option",{value:"-14lufs"},"-14 LUFS")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.loop,onChange:e=>setExportSettings(p=>({...p,loop:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Xuất loop (không fade in / không fade out)")),exportSettings.loop&&/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1 ml-auto"},/*#__PURE__*/React.createElement("span",{className:"text-[7px] text-zinc-500 font-bold uppercase"},"Số ô nhịp"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",max:"256",value:exportSettings.loopBars,onChange:e=>setExportSettings(p=>({...p,loopBars:Math.max(1,parseInt(e.target.value)||1)})),className:"w-16 bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Title (BWF/INFO)"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaTitle,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaTitle:e.target.value})),placeholder:"Tên bài",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Artist"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaArtist,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaArtist:e.target.value})),placeholder:"Nghệ sĩ",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"ISRC"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaIsrc,maxLength:"12",onChange:e=>setExportSettings(p=>({...p,metaIsrc:e.target.value.toUpperCase()})),placeholder:"US-ABC-26-00001",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.realtime,onChange:e=>setExportSettings(p=>({...p,realtime:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Realtime (bounce — chạy lại project thật, giữ nguyên MIDI + FX Rack + Mastering Chain)")))),/*#__PURE__*/React.createElement("div",{className:"p-4 pt-3 border-t border-slate-700 space-y-2"},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("button",{onClick:onExport,disabled:isExporting,className:"w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3 h-3"})," ",isExporting?'...':'Export'),/*#__PURE__*/React.createElement("button",{onClick:onClose,disabled:isExporting,className:"w-full py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"Hủy")))));};// ── Export progress modal (RIÊNG, không dùng statusbar — vị trí đó cho tools) ── +return{hx:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientWidth:460)-268,st.current.hudPos.hx)),hy:Math.max(0,Math.min((canvasRef.current?canvasRef.current.clientHeight:240)-225,st.current.hudPos.hy))};}if(!sel)return null;const nx=eqproFreqToX(sel.freq,cvW),ny=eqproGainToY(sel.gain,cvH);let hx=nx-130,hy=ny-195;if(hx<8)hx=8;if(hx>cvW-268)hx=cvW-268;if(hy<8)hy=ny+24;return{hx,hy};})();return/*#__PURE__*/React.createElement("div",{className:"space-y-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[11px] font-mono"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-3"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Bands: ",/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold"},st.current.bands.length),"/8"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"Amount:"),/*#__PURE__*/React.createElement("input",{type:"range",min:0,max:200,value:st.current.amount,onChange:e=>{st.current.amount=parseInt(e.target.value);applyAll(mm=>mm.setAmount(st.current.amount));commit();setTick(t=>t+1);},className:"w-20 h-1 cursor-pointer",style:{accentColor:'#2dd4bf'}}),/*#__PURE__*/React.createElement("span",{className:"text-teal-300 font-bold w-10"},st.current.amount,"%"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;s.bands=JSON.parse(JSON.stringify(EQPRO_DEFAULT_BANDS));s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-slate-800 hover:bg-red-900/60 text-slate-300 border border-slate-700 rounded text-[10px]"},"Reset"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const s=st.current;if(s.bands.length>=EQPRO_MAX_BANDS)return;s.bands.push({type:'peaking',freq:1000,gain:0,q:1.0,active:true});s.selected=s.bands.length-1;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"px-2 py-1 bg-teal-900/50 hover:bg-teal-800 text-teal-300 border border-teal-700 rounded text-[10px]"},"+ Band"))),/*#__PURE__*/React.createElement("div",{className:"relative"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-60 block cursor-crosshair rounded-lg border border-slate-800 bg-slate-950",onPointerDown:onDown,onPointerMove:onMove,onPointerUp:onUp,onWheel:onWheel,onDoubleClick:onDblClick}),sel&&hudXY&&/*#__PURE__*/React.createElement("div",{className:"absolute z-10 w-64 rounded-xl p-3 text-[11px] font-mono space-y-2 pointer-events-auto cursor-move",style:{left:hudXY.hx,top:hudXY.hy,background:'rgba(15,23,42,0.94)',border:'1px solid rgba(45,212,191,0.3)',boxShadow:'0 10px 30px rgba(0,0,0,0.8)',backdropFilter:'blur(12px)'},onPointerDown:e=>{const s=st.current;s.dragHud=true;const r=e.currentTarget.getBoundingClientRect();s.hudOffsetX=e.clientX-r.left;s.hudOffsetY=e.clientY-r.top;e.currentTarget.setPointerCapture(e.pointerId);},onPointerMove:e=>{const s=st.current;if(!s.dragHud)return;const cv=canvasRef.current;const w=cv?cv.clientWidth:460;s.hudPos={hx:Math.max(0,Math.min(w-268,e.clientX-s.hudOffsetX-(cv?cv.getBoundingClientRect().left:0))),hy:Math.max(0,e.clientY-s.hudOffsetY-(cv?cv.getBoundingClientRect().top:0))};setTick(t=>t+1);},onPointerUp:e=>{st.current.dragHud=false;try{e.currentTarget.releasePointerCapture(e.pointerId);}catch(err){}}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-700/80 pb-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"w-4 h-4 rounded-full text-slate-950 font-bold flex items-center justify-center text-[10px]",style:{background:hudColor.badge}},selIdx+1),/*#__PURE__*/React.createElement("span",{className:"font-bold text-white uppercase tracking-wider"},"Band ",selIdx+1)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{title:"Bypass band",onClick:()=>{const s=st.current;s.bands[s.selected].active=!s.bands[s.selected].active;applyAll(mm=>mm.setBand(s.selected,{active:s.bands[s.selected].active}));commit();setTick(t=>t+1);},className:`w-5 h-5 rounded flex items-center justify-center text-[10px] ${sel.active?'bg-slate-800 text-slate-400':'bg-amber-600 text-white'}`},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-power-off"})),/*#__PURE__*/React.createElement("button",{title:"Delete band",onClick:()=>{const s=st.current;s.bands.splice(s.selected,1);s.selected=null;applyAll(mm=>mm.syncBands(s.bands));commit();setTick(t=>t+1);},className:"w-5 h-5 rounded bg-slate-800 hover:bg-red-600 text-slate-400 hover:text-white flex items-center justify-center text-[10px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"})))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[9px] text-slate-400 uppercase tracking-widest mb-1"},"Filter Shape"),/*#__PURE__*/React.createElement("select",{value:sel.type,onChange:ev=>{const s=st.current;s.bands[s.selected].type=ev.target.value;applyAll(mm=>mm.setBand(s.selected,{type:ev.target.value}));commit();setTick(t=>t+1);},className:"w-full bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none"},['peaking','highpass','lowpass','lowshelf','highshelf','notch','bandpass'].map(t=>/*#__PURE__*/React.createElement("option",{key:t,value:t},t==='peaking'?'Bell / Peaking':t==='highpass'?'Low Cut / High Pass':t==='lowpass'?'High Cut / Low Pass':t==='lowshelf'?'Low Shelf':t==='highshelf'?'High Shelf':t==='notch'?'Notch / Band Stop':'Band Pass')))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 text-center bg-slate-950/80 p-2 rounded-lg border border-slate-800"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"FREQ"),/*#__PURE__*/React.createElement("div",{className:"text-teal-300 font-bold text-[11px]"},sel.freq>=1000?(sel.freq/1000).toFixed(2)+' kHz':Math.round(sel.freq)+' Hz')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"GAIN"),/*#__PURE__*/React.createElement("div",{className:"text-amber-400 font-bold text-[11px]"},eqproBandHasGain(sel.type)?(sel.gain>0?'+':'')+sel.gain.toFixed(1)+' dB':'0.0 dB')),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500"},"Q"),/*#__PURE__*/React.createElement("div",{className:"text-purple-400 font-bold text-[11px]"},sel.q.toFixed(1)))),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 text-center"},"Drag center: Freq & Gain \xB7 Wings/Wheel: Q \xB7 Dbl-click: add/delete"))));};const ExportModal=({open,onClose,exportSettings,setExportSettings,isExporting,exportProgress,onExport})=>{if(!open)return null;const pickDir=async()=>{try{const r=await fetch(`${API_BASE_URL}/api/v1/plugins/pick-dir`,{method:'POST'});if(r.ok){const d=await r.json();if(d&&d.path)setExportSettings(p=>({...p,exportDir:d.path}));}}catch(e){}};return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[300] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3.5 h-3.5"})," EXPORT"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"p-4 max-h-[72vh] overflow-y-auto space-y-3"},/*#__PURE__*/React.createElement("div",{className:"bg-[#141414] border border-zinc-800 rounded px-2 py-1.5 space-y-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"T\xEAn file export"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.fileName,onChange:e=>setExportSettings(p=>({...p,fileName:e.target.value})),placeholder:"\u0110\u1EC3 tr\u1ED1ng = d\xF9ng t\xEAn m\u1EB7c \u0111\u1ECBnh",className:"w-full bg-[#0d0d0d] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-end gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex-1 min-w-0"},/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Th\u01B0 m\u1EE5c export"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 bg-[#0d0d0d] border border-zinc-800 rounded px-1.5 py-0.5 min-h-[22px]"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-cyan-400 text-[10px] shrink-0"}),exportSettings.exportDir?/*#__PURE__*/React.createElement("a",{className:"text-cyan-400 underline truncate cursor-pointer",title:exportSettings.exportDir,onClick:pickDir},exportSettings.exportDir):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 truncate"},"Ch\u01B0a ch\u1ECDn \u2014 xu\u1EA5t s\u1EBD m\u1EDF Save-As"))),/*#__PURE__*/React.createElement("button",{onClick:pickDir,className:"shrink-0 px-2 py-1 bg-cyan-700 hover:bg-cyan-600 text-white rounded text-[10px] font-bold flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[10px]"})," Ch\u1ECDn th\u01B0 m\u1EE5c")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.rememberExportDir,onChange:e=>setExportSettings(p=>({...p,rememberExportDir:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"L\u01B0u th\u01B0 m\u1EE5c export l\xE0m m\u1EB7c \u0111\u1ECBnh"))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Ngu\u1ED3n"),/*#__PURE__*/React.createElement("select",{value:exportSettings.source,onChange:e=>setExportSettings(p=>({...p,source:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"project"},"Project (Mix)"),/*#__PURE__*/React.createElement("option",{value:"track_mix"},"Track Selection"),/*#__PURE__*/React.createElement("option",{value:"active_clip"},"Active Clip"),/*#__PURE__*/React.createElement("option",{value:"clip_selection"},"Clip Selection"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"\u0110\u1ECBnh d\u1EA1ng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.format,onChange:e=>setExportSettings(p=>({...p,format:e.target.value,sampleRate:'44100',bitDepth:'16',quality:'44khz'})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"wav"},"WAV"),/*#__PURE__*/React.createElement("option",{value:"mp3"},"MP3"),/*#__PURE__*/React.createElement("option",{value:"ogg"},"OGG")))),exportSettings.format==='wav'?/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"SR (Hz)"),/*#__PURE__*/React.createElement("select",{value:exportSettings.sampleRate,onChange:e=>setExportSettings(p=>({...p,sampleRate:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44100"},"44100"),/*#__PURE__*/React.createElement("option",{value:"48000"},"48000"),/*#__PURE__*/React.createElement("option",{value:"96000"},"96000"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Bit"),/*#__PURE__*/React.createElement("select",{value:exportSettings.bitDepth,onChange:e=>setExportSettings(p=>({...p,bitDepth:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"16"},"16"),/*#__PURE__*/React.createElement("option",{value:"24"},"24"),/*#__PURE__*/React.createElement("option",{value:"float"},"32 float"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"K\xEAnh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))):/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Ch\u1EA5t l\u01B0\u1EE3ng"),/*#__PURE__*/React.createElement("select",{value:exportSettings.quality,onChange:e=>setExportSettings(p=>({...p,quality:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"44khz"},"44kHz"),/*#__PURE__*/React.createElement("option",{value:"lossless"},"Lossless"))),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"K\xEAnh"),/*#__PURE__*/React.createElement("select",{value:exportSettings.channels,onChange:e=>setExportSettings(p=>({...p,channels:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"mono"},"Mono"),/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo")))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.dither,onChange:e=>setExportSettings(p=>({...p,dither:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Dither TPDF (PCM 16/24)")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.normalize,onChange:e=>setExportSettings(p=>({...p,normalize:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Normalize")),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.monoCheck,onChange:e=>setExportSettings(p=>({...p,monoCheck:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"C\u1EA3nh b\xE1o mono (side/mid)")),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Normalize target"),/*#__PURE__*/React.createElement("select",{value:exportSettings.normalizeTarget,onChange:e=>setExportSettings(p=>({...p,normalizeTarget:e.target.value})),className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"},/*#__PURE__*/React.createElement("option",{value:"-1dbtp"},"-1 dBTP"),/*#__PURE__*/React.createElement("option",{value:"-14lufs"},"-14 LUFS")))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.loop,onChange:e=>setExportSettings(p=>({...p,loop:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Xu\u1EA5t loop (kh\xF4ng fade in / kh\xF4ng fade out)")),exportSettings.loop&&/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1 ml-auto"},/*#__PURE__*/React.createElement("span",{className:"text-[7px] text-zinc-500 font-bold uppercase"},"S\u1ED1 \xF4 nh\u1ECBp"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",max:"256",value:exportSettings.loopBars,onChange:e=>setExportSettings(p=>({...p,loopBars:Math.max(1,parseInt(e.target.value)||1)})),className:"w-16 bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-2 pt-1 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Title (BWF/INFO)"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaTitle,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaTitle:e.target.value})),placeholder:"T\xEAn b\xE0i",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"Artist"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaArtist,maxLength:"254",onChange:e=>setExportSettings(p=>({...p,metaArtist:e.target.value})),placeholder:"Ngh\u1EC7 s\u0129",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-[7px] text-zinc-500 font-bold uppercase mb-0.5"},"ISRC"),/*#__PURE__*/React.createElement("input",{type:"text",value:exportSettings.metaIsrc,maxLength:"12",onChange:e=>setExportSettings(p=>({...p,metaIsrc:e.target.value.toUpperCase()})),placeholder:"US-ABC-26-00001",className:"w-full bg-[#141414] border border-zinc-800 rounded px-1 py-0.5 text-xs text-zinc-300 focus:outline-none"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 bg-[#141414] border border-zinc-800 rounded px-2 py-1.5"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1.5 cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:!!exportSettings.realtime,onChange:e=>setExportSettings(p=>({...p,realtime:e.target.checked})),className:"accent-cyan-500 w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-300"},"Realtime (bounce \u2014 ch\u1EA1y l\u1EA1i project th\u1EADt, gi\u1EEF nguy\xEAn MIDI + FX Rack + Mastering Chain)")))),/*#__PURE__*/React.createElement("div",{className:"p-4 pt-3 border-t border-slate-700 space-y-2"},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-2"},/*#__PURE__*/React.createElement("button",{onClick:onExport,disabled:isExporting,className:"w-full py-1 bg-cyan-700 hover:bg-cyan-600 text-zinc-100 font-bold rounded text-xs flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download-cloud",className:"w-3 h-3"})," ",isExporting?'...':'Export'),/*#__PURE__*/React.createElement("button",{onClick:onClose,disabled:isExporting,className:"w-full py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"H\u1EE7y")))));};// ── Export progress modal (RIÊNG, không dùng statusbar — vị trí đó cho tools) ── // Settings modal đóng ngay khi bấm Export; modal này hiện tiến trình render + // encode + ghi file, tự đóng khi xong (success/error qua toast). -const ExportProgressModal=({open,exportProgress,isExporting,onClose})=>{if(!open)return null;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[310] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6"},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-md bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"w-3 h-3 border-2 border-cyan-300 border-t-transparent rounded-full animate-spin"}),"ĐANG XUẤT ÂM THANH")),/*#__PURE__*/React.createElement("div",{className:"p-5 space-y-3"},/*#__PURE__*/React.createElement("div",{className:"h-2.5 bg-zinc-800 rounded-full overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-full bg-cyan-500 transition-all duration-200",style:{width:(exportProgress>=0?exportProgress:25)+'%'}})),/*#__PURE__*/React.createElement("div",{className:"text-xs text-zinc-400 text-center"},exportProgress>=0?'Đang xuất... '+Math.round(exportProgress)+'%':'Đang xử lý...'),!isExporting&&/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-full py-1.5 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"Đóng"))));};// --- Embedded VST GUI (PLAN_MASTERBUS_VST_GUI_EMBED.md) -------------------- +const ExportProgressModal=({open,exportProgress,isExporting,onClose})=>{if(!open)return null;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[310] bg-black/70 backdrop-blur-sm flex items-center justify-center p-6"},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-md bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-9 bg-slate-800/80 border-b border-slate-700 flex items-center justify-between px-3 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-cyan-300 font-mono flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"w-3 h-3 border-2 border-cyan-300 border-t-transparent rounded-full animate-spin"}),"\u0110ANG XU\u1EA4T \xC2M THANH")),/*#__PURE__*/React.createElement("div",{className:"p-5 space-y-3"},/*#__PURE__*/React.createElement("div",{className:"h-2.5 bg-zinc-800 rounded-full overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-full bg-cyan-500 transition-all duration-200",style:{width:(exportProgress>=0?exportProgress:25)+'%'}})),/*#__PURE__*/React.createElement("div",{className:"text-xs text-zinc-400 text-center"},exportProgress>=0?'Đang xuất... '+Math.round(exportProgress)+'%':'Đang xử lý...'),!isExporting&&/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"w-full py-1.5 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 font-bold rounded text-xs"},"\u0110\xF3ng"))));};// --- Embedded VST GUI (PLAN_MASTERBUS_VST_GUI_EMBED.md) -------------------- // Frame-capture + input relay: hiển thị native GUI qua HTTP /frame (~10fps), // gửi pointer/key về bridge qua /input. Dùng chung MasteringModal + FXRackModal. // url = embed_url từ API fx-gui open (embed:true); onClose khi user đóng; @@ -938,10 +966,10 @@ React.useEffect(()=>{let alive=true;const maybeShow=()=>{const r=screenRectRef.c // re-anchors over the panel on scroll/drag/resize. Idempotent: the // bridge skips SetWindowPos/resize when nothing moved. try{fetch(url+'/show',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({rect:r})}).catch(()=>{});}catch(e){}}};const refresh=()=>{const box=boxRef.current;if(!box)return;const r=box.getBoundingClientRect();if(r.width<4||r.height<4)return;try{const tw=window.__TAURI__&&window.__TAURI__.window;if(tw&&tw.getCurrentWindow){Promise.all([tw.getCurrentWindow().innerPosition(),tw.getCurrentWindow().scaleFactor()]).then(([pos,sf])=>{if(!alive||!boxRef.current)return;const rb=boxRef.current.getBoundingClientRect();screenRectRef.current={x:Math.round(pos.x+rb.left*sf),y:Math.round(pos.y+rb.top*sf),w:Math.round(rb.width*sf),h:Math.round(rb.height*sf)};maybeShow();}).catch(()=>{if(!alive||!boxRef.current)return;const rb=boxRef.current.getBoundingClientRect();screenRectRef.current={x:Math.round((window.screenX||0)+rb.left),y:Math.round((window.screenY||0)+rb.top),w:Math.round(rb.width),h:Math.round(rb.height)};maybeShow();});}else{screenRectRef.current={x:Math.round((window.screenX||0)+r.left),y:Math.round((window.screenY||0)+r.top),w:Math.round(r.width),h:Math.round(r.height)};maybeShow();}}catch(e){}};refresh();const iv=setInterval(refresh,250);return()=>{alive=false;clearInterval(iv);};// eslint-disable-next-line react-hooks/exhaustive-deps -},[url]);const pt=e=>{const box=boxRef.current,img=imgRef.current;if(!box||!img||!img.naturalWidth||!img.naturalHeight)return null;const r=box.getBoundingClientRect();if(r.width===0||r.height===0)return null;return{x:Math.round((e.clientX-r.left)/r.width*img.naturalWidth),y:Math.round((e.clientY-r.top)/r.height*img.naturalHeight)};};const handle=e=>{const p=pt(e);if(!p)return;post({type:e.type==='mousedown'?'mousedown':e.type==='mouseup'?'mouseup':'mousemove',x:p.x,y:p.y,button:e.button,buttons:e.buttons,rect:screenRectRef.current||undefined});};const close=()=>{try{fetch(url+'/close',{method:'POST'}).catch(()=>{});}catch(e){}onClose&&onClose();};return/*#__PURE__*/React.createElement("div",{className:"relative w-full h-full flex flex-col"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-3 py-1.5 bg-slate-900/80 border border-slate-800 rounded-t-lg shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-violet-300 font-mono"},"VST GUI — nhúng (frame-capture · ~10fps)"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},dead&&/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-red-400 font-mono"},"bridge mất kết nối"),/*#__PURE__*/React.createElement("button",{onClick:close,className:"px-2 py-0.5 bg-red-900/60 hover:bg-red-800 text-red-100 text-[10px] font-semibold rounded transition",title:"Đóng GUI và trả về graph"},"✕ Đóng GUI"))),/*#__PURE__*/React.createElement("div",{ref:boxRef,tabIndex:0,className:"flex-1 relative bg-slate-950 border border-slate-800 rounded-b-lg overflow-hidden outline-none",onMouseDown:handle,onMouseUp:handle,onMouseMove:handle,onMouseLeave:()=>post({type:'mouseup',x:-1,y:-1,button:0}),onWheel:e=>{const p=pt(e);if(p)post({type:'wheel',x:p.x,y:p.y,deltaY:e.deltaY,rect:screenRectRef.current||undefined});},onKeyDown:e=>{e.preventDefault();post({type:'keydown',keyCode:e.keyCode||e.which});},onKeyUp:e=>{e.preventDefault();post({type:'keyup',keyCode:e.keyCode||e.which});}},!frameLoaded&&/*#__PURE__*/React.createElement("div",{className:"absolute inset-0 flex items-center justify-center bg-slate-950 z-10"},/*#__PURE__*/React.createElement("span",{className:"text-[11px] text-slate-500 font-mono animate-pulse"},"Đang tải plugin VST — GUI embed sẽ hiện khi plugin sẵn sàng...")),/*#__PURE__*/React.createElement("img",{ref:imgRef,alt:"VST GUI",className:"w-full h-full object-contain select-none pointer-events-none",draggable:false,onLoad:()=>setFrameLoaded(true)})));};const vstRow=(s,idx,toggleV,changeV,uploadV,moveV,removeV,fxl,chainLen,openGui,activeIdx=-1,onSelect=null)=>/*#__PURE__*/React.createElement("div",{key:idx,onClick:()=>onSelect&&onSelect(s,idx),className:`flex items-center gap-2 bg-slate-900/60 border rounded-lg px-2 py-1.5 flex-wrap cursor-pointer transition-all ${activeIdx===idx?'border-teal-400 bg-slate-800/80':'border-slate-800 hover:border-slate-600'}`},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleV(idx);},className:`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass?'bg-teal-500 text-slate-950':'bg-slate-700 text-slate-300'}`,title:s.bypass?'Bypass':'Active'},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-300 font-mono w-6 shrink-0"},idx+1),/*#__PURE__*/React.createElement("select",{value:s.path||'',onClick:e=>e.stopPropagation(),onChange:e=>changeV(idx,e.target.value),className:"flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500"},/*#__PURE__*/React.createElement("option",{value:""},"— Chọn VST FX —"),(fxl||[]).map(f=>/*#__PURE__*/React.createElement("option",{key:f.path,value:f.path},f.name||f.id))),/*#__PURE__*/React.createElement("span",{className:`text-[9px] font-mono ${s.preset_b64?'text-emerald-400':'text-slate-600'}`},s.preset_b64?'✓ preset':'no preset'),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();uploadV(idx);},className:"px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition",title:"Upload .vstpreset (Save preset)"},"Preset"),openGui&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();openGui(s,idx);},disabled:!s.path,className:"px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition",title:"Mở GUI của VST (chỉnh thông số trực tiếp)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"monitor",className:"w-3 h-3"})," GUI"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();moveV(idx,-1);},disabled:idx===0,className:"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30",title:"Lên"},/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-up",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();moveV(idx,1);},disabled:idx===chainLen-1,className:"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30",title:"Xuống"},/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-down",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeV(idx);},className:"text-slate-600 hover:text-red-400 text-xs px-0.5",title:"Xóa slot"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));// ── VST FX Plugin Picker (spec vst_fx_panel_integration.md §II) ────────── +},[url]);const pt=e=>{const box=boxRef.current,img=imgRef.current;if(!box||!img||!img.naturalWidth||!img.naturalHeight)return null;const r=box.getBoundingClientRect();if(r.width===0||r.height===0)return null;return{x:Math.round((e.clientX-r.left)/r.width*img.naturalWidth),y:Math.round((e.clientY-r.top)/r.height*img.naturalHeight)};};const handle=e=>{const p=pt(e);if(!p)return;post({type:e.type==='mousedown'?'mousedown':e.type==='mouseup'?'mouseup':'mousemove',x:p.x,y:p.y,button:e.button,buttons:e.buttons,rect:screenRectRef.current||undefined});};const close=()=>{try{fetch(url+'/close',{method:'POST'}).catch(()=>{});}catch(e){}onClose&&onClose();};return/*#__PURE__*/React.createElement("div",{className:"relative w-full h-full flex flex-col"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-3 py-1.5 bg-slate-900/80 border border-slate-800 rounded-t-lg shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-violet-300 font-mono"},"VST GUI \u2014 nh\xFAng (frame-capture \xB7 ~10fps)"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},dead&&/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-red-400 font-mono"},"bridge m\u1EA5t k\u1EBFt n\u1ED1i"),/*#__PURE__*/React.createElement("button",{onClick:close,className:"px-2 py-0.5 bg-red-900/60 hover:bg-red-800 text-red-100 text-[10px] font-semibold rounded transition",title:"\u0110\xF3ng GUI v\xE0 tr\u1EA3 v\u1EC1 graph"},"\u2715 \u0110\xF3ng GUI"))),/*#__PURE__*/React.createElement("div",{ref:boxRef,tabIndex:0,className:"flex-1 relative bg-slate-950 border border-slate-800 rounded-b-lg overflow-hidden outline-none",onMouseDown:handle,onMouseUp:handle,onMouseMove:handle,onMouseLeave:()=>post({type:'mouseup',x:-1,y:-1,button:0}),onWheel:e=>{const p=pt(e);if(p)post({type:'wheel',x:p.x,y:p.y,deltaY:e.deltaY,rect:screenRectRef.current||undefined});},onKeyDown:e=>{e.preventDefault();post({type:'keydown',keyCode:e.keyCode||e.which});},onKeyUp:e=>{e.preventDefault();post({type:'keyup',keyCode:e.keyCode||e.which});}},!frameLoaded&&/*#__PURE__*/React.createElement("div",{className:"absolute inset-0 flex items-center justify-center bg-slate-950 z-10"},/*#__PURE__*/React.createElement("span",{className:"text-[11px] text-slate-500 font-mono animate-pulse"},"\u0110ang t\u1EA3i plugin VST \u2014 GUI embed s\u1EBD hi\u1EC7n khi plugin s\u1EB5n s\xE0ng...")),/*#__PURE__*/React.createElement("img",{ref:imgRef,alt:"VST GUI",className:"w-full h-full object-contain select-none pointer-events-none",draggable:false,onLoad:()=>setFrameLoaded(true)})));};const vstRow=(s,idx,toggleV,changeV,uploadV,moveV,removeV,fxl,chainLen,openGui,activeIdx=-1,onSelect=null)=>/*#__PURE__*/React.createElement("div",{key:idx,onClick:()=>onSelect&&onSelect(s,idx),className:`flex items-center gap-2 bg-slate-900/60 border rounded-lg px-2 py-1.5 flex-wrap cursor-pointer transition-all ${activeIdx===idx?'border-teal-400 bg-slate-800/80':'border-slate-800 hover:border-slate-600'}`},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleV(idx);},className:`w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0 ${!s.bypass?'bg-teal-500 text-slate-950':'bg-slate-700 text-slate-300'}`,title:s.bypass?'Bypass':'Active'},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-300 font-mono w-6 shrink-0"},idx+1),/*#__PURE__*/React.createElement("select",{value:s.path||'',onClick:e=>e.stopPropagation(),onChange:e=>changeV(idx,e.target.value),className:"flex-1 min-w-[200px] bg-slate-950 border border-slate-700 text-teal-300 rounded px-2 py-1 text-[11px] outline-none focus:border-teal-500"},/*#__PURE__*/React.createElement("option",{value:""},"\u2014 Ch\u1ECDn VST FX \u2014"),(fxl||[]).map(f=>/*#__PURE__*/React.createElement("option",{key:f.path,value:f.path},f.name||f.id))),/*#__PURE__*/React.createElement("span",{className:`text-[9px] font-mono ${s.preset_b64?'text-emerald-400':'text-slate-600'}`},s.preset_b64?'✓ preset':'no preset'),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();uploadV(idx);},className:"px-2 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 text-[10px] font-semibold rounded transition",title:"Upload .vstpreset (Save preset)"},"Preset"),openGui&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();openGui(s,idx);},disabled:!s.path,className:"px-2 py-1 bg-violet-800 hover:bg-violet-700 disabled:opacity-30 text-violet-100 text-[10px] font-semibold rounded transition",title:"M\u1EDF GUI c\u1EE7a VST (ch\u1EC9nh th\xF4ng s\u1ED1 tr\u1EF1c ti\u1EBFp)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"monitor",className:"w-3 h-3"})," GUI"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();moveV(idx,-1);},disabled:idx===0,className:"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30",title:"L\xEAn"},/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-up",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();moveV(idx,1);},disabled:idx===chainLen-1,className:"w-6 h-6 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded text-xs disabled:opacity-30",title:"Xu\u1ED1ng"},/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-down",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeV(idx);},className:"text-slate-600 hover:text-red-400 text-xs px-0.5",title:"X\xF3a slot"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));// ── VST FX Plugin Picker (spec vst_fx_panel_integration.md §II) ────────── // Modal chọn VST FX theo category (heuristic từ backend list_fx.category). // Dùng chung FXRackModal + MasteringModal: [+] → picker → chèn VST card. -const VST_FX_CATEGORIES=[{id:'all',label:'Tất cả'},{id:'equalizer',label:'Equalizers / Dynamic EQ'},{id:'reverb_delay',label:'Reverbs & Delays'},{id:'comp_limiter',label:'Compressors & Limiters'},{id:'distortion',label:'Distortion / Saturation'},{id:'utility',label:'Utility'}];const VstPluginPicker=({open,onClose,plugins,onPick,loading})=>{const[cat,setCat]=React.useState('all');React.useEffect(()=>{if(open)setCat('all');},[open]);if(!open)return null;const list=(plugins||[]).filter(p=>cat==='all'||(p.category||'utility')===cat);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[125] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] flex flex-col",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider font-mono"},"CHỌN VST FX — PLUGIN PICKER"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-wrap gap-1.5 shrink-0"},VST_FX_CATEGORIES.map(c=>/*#__PURE__*/React.createElement("button",{key:c.id,onClick:()=>setCat(c.id),className:`px-2.5 py-1 rounded text-[10px] font-semibold transition ${cat===c.id?'bg-teal-700 text-white':'bg-slate-800 hover:bg-slate-700 text-slate-300'}`},c.label))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto oz-scrollbar grid grid-cols-2 gap-2"},loading&&/*#__PURE__*/React.createElement("div",{className:"col-span-2 text-[11px] text-amber-300 font-mono py-10 text-center animate-pulse"},"Đang quét VST FX..."),!loading&&list.length===0&&/*#__PURE__*/React.createElement("div",{className:"col-span-2 text-[11px] text-slate-500 font-mono py-10 text-center"},"Không có VST FX trong mục này."),!loading&&list.map(f=>/*#__PURE__*/React.createElement("button",{key:f.path,onClick:()=>{onPick(f);onClose();},className:"p-2 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-lg text-left space-y-0.5 transition-colors"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-teal-300 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plug",className:"w-3 h-3"})," ",f.name||f.id),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono truncate"},f.path))))));};// GUI embed state survives FXRackModal unmount so the native window +const VST_FX_CATEGORIES=[{id:'all',label:'Tất cả'},{id:'equalizer',label:'Equalizers / Dynamic EQ'},{id:'reverb_delay',label:'Reverbs & Delays'},{id:'comp_limiter',label:'Compressors & Limiters'},{id:'distortion',label:'Distortion / Saturation'},{id:'utility',label:'Utility'}];const VstPluginPicker=({open,onClose,plugins,onPick,loading})=>{const[cat,setCat]=React.useState('all');React.useEffect(()=>{if(open)setCat('all');},[open]);if(!open)return null;const list=(plugins||[]).filter(p=>cat==='all'||(p.category||'utility')===cat);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[125] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-2xl bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] flex flex-col",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider font-mono"},"CH\u1ECCN VST FX \u2014 PLUGIN PICKER"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-wrap gap-1.5 shrink-0"},VST_FX_CATEGORIES.map(c=>/*#__PURE__*/React.createElement("button",{key:c.id,onClick:()=>setCat(c.id),className:`px-2.5 py-1 rounded text-[10px] font-semibold transition ${cat===c.id?'bg-teal-700 text-white':'bg-slate-800 hover:bg-slate-700 text-slate-300'}`},c.label))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto oz-scrollbar grid grid-cols-2 gap-2"},loading&&/*#__PURE__*/React.createElement("div",{className:"col-span-2 text-[11px] text-amber-300 font-mono py-10 text-center animate-pulse"},"\u0110ang qu\xE9t VST FX..."),!loading&&list.length===0&&/*#__PURE__*/React.createElement("div",{className:"col-span-2 text-[11px] text-slate-500 font-mono py-10 text-center"},"Kh\xF4ng c\xF3 VST FX trong m\u1EE5c n\xE0y."),!loading&&list.map(f=>/*#__PURE__*/React.createElement("button",{key:f.path,onClick:()=>{onPick(f);onClose();},className:"p-2 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-lg text-left space-y-0.5 transition-colors"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-teal-300 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plug",className:"w-3 h-3"})," ",f.name||f.id),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono truncate"},f.path))))));};// GUI embed state survives FXRackModal unmount so the native window // re-shows automatically when the rack is reopened (close/reopen bug). let _persistFxGuiOpen=null;// Track id whose rack owns the currently-open embedded GUI (for save). let _persistFxGuiTrackId=null;// Latest preset_b64 captured from each plugin's embedded GUI (poll /preset) @@ -981,7 +1009,7 @@ ctx.strokeStyle='rgba(51, 65, 85, 0.35)';for(let i=1;i<5;i++){const y=i/5*h;ctx. const bands=[{type:'lowshelf',f0:100,gain:ap.g1||0,q:0.7},{type:'peaking',f0:800,gain:ap.g2||0,q:0.7},{type:'peaking',f0:3200,gain:ap.g3||0,q:1.2},{type:'highshelf',f0:10000,gain:ap.g4||0,q:0.7}];const pts=[];const N=120;for(let i=0;i<=N;i++){const f=20*Math.pow(20000/20,i/N);let db=0;bands.forEach(b=>{const lf=Math.log(f/b.f0);if(b.type==='peaking'){db+=b.gain/(1+Math.pow(lf*b.q,2));}else if(b.type==='highshelf'){db+=b.gain/2*(1+2/Math.PI*Math.atan(lf/(1/b.q)));}else{db+=b.gain/2*(1-2/Math.PI*Math.atan(lf/(1/b.q)));}});const x=i/N*w;const y=midY-db/12*(h/2);pts.push([x,y]);}// fill ctx.beginPath();pts.forEach(([x,y],i)=>i===0?ctx.moveTo(x,y):ctx.lineTo(x,y));ctx.lineTo(w,h);ctx.lineTo(0,h);ctx.closePath();ctx.fillStyle='rgba(34, 211, 238, 0.10)';ctx.fill();// curve ctx.beginPath();pts.forEach(([x,y],i)=>i===0?ctx.moveTo(x,y):ctx.lineTo(x,y));ctx.strokeStyle='#22d3ee';ctx.lineWidth=2;ctx.stroke();// dB labels -ctx.fillStyle='#475569';ctx.font='9px monospace';ctx.fillText('+12 dB',4,midY-h/2+10);ctx.fillText('0 dB',4,midY+3);ctx.fillText('-12 dB',4,midY+h/2-4);ctx.fillText('20Hz',4,h-2);ctx.fillText('20kHz',w-38,h-2);},[ap.g1,ap.g2,ap.g3,ap.g4,activeMod]);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[115] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-4xl bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl flex flex-col max-h-[90vh] text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("div",{className:"w-3 h-3 rounded-full bg-cyan-400 animate-pulse"}),/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider font-mono"},track.name," — FX CHAIN")),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-white text-base"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"h-16 bg-slate-950 border border-slate-800 rounded-xl px-3 flex items-center gap-2 overflow-x-auto shrink-0 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 font-mono shrink-0"},"CHAIN:"),unifiedChain.length===0&&/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-slate-500 font-mono"},"Chưa có FX — bấm [+] (DSP) hoặc [+VST] để thêm (signal đi thẳng: Source → Fader)"),unifiedChain.map((u,ui)=>{if(u.kind==='builtin'){const m=u.m;const meta=TRACK_FX_META[m.type]||{name:m.type,icon:'circle',color:'#94a3b8',sub:''};const on=m.active!==false;return/*#__PURE__*/React.createElement("div",{key:`b${u.i}`,draggable:true,onDragStart:e=>{dragChainIndexRef.current=ui;e.dataTransfer.effectAllowed='move';},onDragOver:e=>{e.preventDefault();e.dataTransfer.dropEffect='move';},onDrop:e=>{e.preventDefault();const from=dragChainIndexRef.current;if(from!==null&&from!==ui){const next=[...unifiedChain];const mv=next.splice(from,1)[0];next.splice(ui,0,mv);const nb=next.filter(x=>x.kind==='builtin').map(x=>x.m);const nv=next.filter(x=>x.kind==='vst').map(x=>x.m);setChain(nb);setVstChain(nv);if(mv.kind==='vst')setActiveVstIdx(nv.indexOf(mv.m));}dragChainIndexRef.current=null;},onClick:()=>{closeFxGui();setActiveType(m.type);},className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeType===m.type?'border-2 border-cyan-400 bg-slate-800':'bg-slate-900 border border-slate-800 hover:border-slate-600'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 min-w-0"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleMod(u.i);},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0",style:{backgroundColor:on?'#38bdf8':'#334155',color:on?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200 truncate"},meta.name),/*#__PURE__*/React.createElement("div",{className:"text-[9px] font-mono truncate",style:{color:meta.color}},u.i+1,". ",meta.sub))),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeMod(u.i);},className:"text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0",title:"Xóa module"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));}const s=u.m;return/*#__PURE__*/React.createElement("div",{key:`v${u.i}`,draggable:true,onDragStart:e=>{dragChainIndexRef.current=ui;e.dataTransfer.effectAllowed='move';},onDragOver:e=>{e.preventDefault();e.dataTransfer.dropEffect='move';},onDrop:e=>{e.preventDefault();const from=dragChainIndexRef.current;if(from!==null&&from!==ui){const next=[...unifiedChain];const mv=next.splice(from,1)[0];next.splice(ui,0,mv);const nb=next.filter(x=>x.kind==='builtin').map(x=>x.m);const nv=next.filter(x=>x.kind==='vst').map(x=>x.m);setChain(nb);setVstChain(nv);if(mv.kind==='vst')setActiveVstIdx(nv.indexOf(mv.m));}dragChainIndexRef.current=null;},onClick:()=>{closeFxGui();setActiveVstIdx(vstChain.indexOf(s));},className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeVstIdx===vstChain.indexOf(s)?'border-2 border-teal-400 bg-slate-800':'bg-slate-900 border border-teal-900/70 hover:border-teal-600'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 min-w-0"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleVst(vstChain.indexOf(s));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0",style:{backgroundColor:!s.bypass?'#2dd4bf':'#334155',color:!s.bypass?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-teal-200 truncate"},s.name||s.path),/*#__PURE__*/React.createElement("div",{className:"text-[9px] font-mono truncate text-teal-500/80"},u.i+1,". ",s.preset_b64?'✓ preset':'no preset'))),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeVst(vstChain.indexOf(s));},className:"text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0",title:"Xóa slot"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));}),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(true),className:"w-16 h-12 rounded-lg border border-dashed border-slate-700 hover:border-cyan-500 flex items-center justify-center text-slate-500 hover:text-cyan-400 cursor-pointer transition-all bg-slate-900/40 shrink-0",title:"Thêm module DSP builtin"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-4 h-4"})),/*#__PURE__*/React.createElement("button",{onClick:()=>setVstPickerOpen(true),className:"w-16 h-12 rounded-lg border border-dashed border-teal-800 hover:border-teal-500 flex items-center justify-center text-teal-500 hover:text-teal-300 cursor-pointer transition-all bg-slate-900/40 shrink-0",title:"Thêm VST FX (Plugin Picker theo category)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plug",className:"w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-[9px] font-bold font-mono ml-0.5"},"VST"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between flex-wrap gap-2 shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 font-mono truncate"},"VST3 — realtime qua bridge (SHM) khi play + áp dụng lúc Export/Offline render"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:previewFx,className:"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"play",className:"w-3 h-3"})," Nghe thử (render offline)"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-slate-400 font-mono cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:vstBypass,onChange:e=>{const val=e.target.checked;setVstBypass(val);if(onUpdateTrack)onUpdateTrack(track.id,{vstFxBypass:val});if(window.__rebuildTrackFxGraph)window.__rebuildTrackFxGraph(track.id,{vstFxBypass:val});},className:"w-3 h-3"})," Bypass"))),fxGuiOpen?/*#__PURE__*/React.createElement("div",{className:"flex-1 min-h-0"},/*#__PURE__*/React.createElement(VstGuiEmbed,{url:fxGuiOpen.url,onClose:closeFxGui,onDead:()=>{window.showToast&&window.showToast('Bridge VST GUI đã mất kết nối — trả về graph','error');closeFxGui();}})):/*#__PURE__*/React.createElement(React.Fragment,null,activeVstIdx>=0&&activeVstIdxapplyPreset(e.target.value),className:"bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-xs outline-none focus:border-cyan-500"},Object.keys(TRACK_EQ_PRESETS).map(k=>/*#__PURE__*/React.createElement("option",{key:k,value:k},TRACK_EQ_PRESETS[k].name))))),!activeMod&&/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-500 font-mono text-center py-10"},"Chưa có module. Bấm [+] để thêm EQ / Compressor / Limiter / Exciter / Rebalance."),activeMod&&activeMod.type==='eq'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"space-y-3"},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-4 gap-4"},[['g1','BAND 1 (LOW)','100 Hz','#22d3ee'],['g2','BAND 2 (MID LOW)','800 Hz','#fbbf24'],['g3','BAND 3 (MID HIGH)','3.2 kHz','#a855f7'],['g4','BAND 4 (HIGH)','10 kHz','#34d399']].map(([key,label,freq,color])=>/*#__PURE__*/React.createElement("div",{key:key,className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg space-y-2"},slider(label,ap[key]!==undefined?ap[key]:0,-12,12,0.1,color,v=>setParams(activeIdx,{[key]:v}),v=>`${v>0?'+':''}${v.toFixed(1)} dB`),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono text-center"},freq)))),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 rounded-lg p-2"},/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono mb-1 flex justify-between"},/*#__PURE__*/React.createElement("span",null,"VECTOR DISPLAY — EQ RESPONSE"),/*#__PURE__*/React.createElement("span",null,"20Hz – 20kHz")),/*#__PURE__*/React.createElement("canvas",{ref:eqCurveRef,className:"w-full h-[88px] block"}))),activeMod&&activeMod.type==='eqpro'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"space-y-3"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-teal-400 font-mono uppercase tracking-widest"},"PARAMETRIC / GRAPHIC EQ PRO — Pro-Q style interactive"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 font-mono"},"20Hz – 20kHz · ±24dB · ",EQPRO_MAX_BANDS," bands max")),/*#__PURE__*/React.createElement(InteractiveEqPro,{track:track,params:ap,onChange:next=>setParams(activeIdx,next),getModule:()=>window.__getTrackFxModule?window.__getTrackFxModule(track.id,'eqpro'):null,spectrumModules:()=>{const a=window.__getTrackFxModule?window.__getTrackFxModule(track.id,'eqpro'):null;const b=window.__getTrackSfFxModule?window.__getTrackSfFxModule(track.id,'eqpro'):null;return[a,b].filter(Boolean);}})),activeMod&&activeMod.type==='compressor'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('THRESHOLD',ap.threshold,-60,0,0.5,'#fbbf24',v=>setParams(activeIdx,{threshold:v}),v=>`${v.toFixed(1)} dB`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('RATIO',ap.ratio,1,20,0.5,'#f59e0b',v=>setParams(activeIdx,{ratio:v}),v=>`${v.toFixed(1)} : 1`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('MAKE-UP',ap.makeup,0,12,0.1,'#f59e0b',v=>setParams(activeIdx,{makeup:v}),v=>`${v.toFixed(1)} dB`))),activeMod&&activeMod.type==='limiter'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('CEILING',ap.ceiling,-24,0,0.1,'#f43f5e',v=>setParams(activeIdx,{ceiling:v}),v=>`${v.toFixed(1)} dB`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg flex items-center justify-center text-[10px] text-slate-500 font-mono text-center leading-snug"},"True-Peak limiting",/*#__PURE__*/React.createElement("br",null),"Ratio 20:1 · Knee 0dB")),activeMod&&activeMod.type==='exciter'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('DRIVE / MIX',ap.drive,0,100,1,'#c084fc',v=>setParams(activeIdx,{drive:v}),v=>`${v}%`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg flex items-center justify-center text-[10px] text-slate-500 font-mono text-center leading-snug"},"WaveShaper saturation",/*#__PURE__*/React.createElement("br",null),"High-pass 2kHz · 4× oversampled")),activeMod&&activeMod.type==='rebalance'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('MID GAIN',ap.mid,-12,12,0.1,'#38bdf8',v=>setParams(activeIdx,{mid:v}),v=>`${v>0?'+':''}${v.toFixed(1)} dB`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('SIDE GAIN',ap.side,-12,12,0.1,'#22d3ee',v=>setParams(activeIdx,{side:v}),v=>`${v>0?'+':''}${v.toFixed(1)} dB`)))),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded-xl p-3 flex flex-col gap-2 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-xs font-mono border-b border-slate-800/80 pb-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"font-bold text-white tracking-wider"},"WAVE OBSERVER"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-400 px-1.5 py-0.5 rounded"},"Real-time Oscilloscope")),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-slate-500 truncate max-w-[200px]"},"Context: ",track.name)),/*#__PURE__*/React.createElement("div",{className:"relative w-full h-36 bg-slate-950 border border-slate-900 rounded-lg overflow-hidden"},/*#__PURE__*/React.createElement("canvas",{ref:scopeCanvasRef,className:"w-full h-full block cursor-crosshair"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between bg-slate-900/90 border border-slate-800 rounded-lg px-3 py-1.5 text-xs font-mono flex-wrap gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 pr-3 border-r border-slate-800"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-slate-400"},"Input"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 w-12"},/*#__PURE__*/React.createElement("div",{className:"h-1.5 bg-slate-950 rounded overflow-hidden flex"},/*#__PURE__*/React.createElement("div",{ref:scopeMeterLRef,className:"h-full bg-cyan-400 w-0 transition-all"})),/*#__PURE__*/React.createElement("div",{className:"h-1.5 bg-slate-950 rounded overflow-hidden flex"},/*#__PURE__*/React.createElement("div",{ref:scopeMeterRRef,className:"h-full bg-cyan-400 w-0 transition-all"})))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"Channel:"),/*#__PURE__*/React.createElement("select",{value:scopeChannel,onChange:e=>setScopeChannel(e.target.value),className:"bg-slate-950 border border-slate-800 text-slate-300 rounded px-1.5 py-0.5 text-[11px] outline-none"},/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo"),/*#__PURE__*/React.createElement("option",{value:"left"},"Left"),/*#__PURE__*/React.createElement("option",{value:"right"},"Right"),/*#__PURE__*/React.createElement("option",{value:"mid"},"Mid"),/*#__PURE__*/React.createElement("option",{value:"side"},"Side"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"Mode:"),/*#__PURE__*/React.createElement("select",{value:scopeMode,onChange:e=>setScopeMode(e.target.value),className:"bg-slate-950 border border-slate-800 text-slate-300 rounded px-1.5 py-0.5 text-[11px] outline-none"},/*#__PURE__*/React.createElement("option",{value:"waveform"},"Waveform"),/*#__PURE__*/React.createElement("option",{value:"lissajous"},"Lissajous"),/*#__PURE__*/React.createElement("option",{value:"spectrum"},"Spectrum"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"Duration:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold text-[11px] w-12"},scopeDuration.toFixed(2),"s"),/*#__PURE__*/React.createElement("input",{type:"range",min:"0.1",max:"5.0",step:"0.1",value:scopeDuration,onChange:e=>setScopeDuration(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer",style:{accentColor:'#38bdf8'}})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"V.Zoom:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold text-[11px] w-12"},scopeZoom>0?'+':'',scopeZoom.toFixed(1)," dB"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"20",step:"0.5",value:scopeZoom,onChange:e=>setScopeZoom(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer",style:{accentColor:'#38bdf8'}})),/*#__PURE__*/React.createElement("button",{onClick:()=>setScopePaused(p=>!p),className:`px-3 py-1 font-semibold rounded text-[11px] border transition-colors ${scopePaused?'bg-amber-600 border-amber-400 text-white':'bg-slate-800 hover:bg-slate-700 text-slate-300 border-slate-700'}`},scopePaused?'Resume':'Pause'))))),addModuleOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[120] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:()=>setAddModuleOpen(false)},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] overflow-y-auto",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider font-mono"},"THÊM MODULE VÀO FX CHAIN"),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(false),className:"text-slate-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3 text-xs"},Object.keys(TRACK_FX_META).map(t=>{const meta=TRACK_FX_META[t];return/*#__PURE__*/React.createElement("button",{key:t,onClick:()=>addMod(t),className:"p-3 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-xl text-left space-y-1 transition-colors"},/*#__PURE__*/React.createElement("div",{className:"font-bold flex items-center gap-1.5",style:{color:meta.color}},/*#__PURE__*/React.createElement("i",{"data-lucide":meta.icon,className:"w-3.5 h-3.5"})," ",meta.name),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400"},meta.sub));})))),/*#__PURE__*/React.createElement(VstPluginPicker,{open:vstPickerOpen,onClose:()=>setVstPickerOpen(false),plugins:fxList,loading:fxLoading,onPick:f=>addVst(f.path)}));};// ────────────────────────────────────────────── +ctx.fillStyle='#475569';ctx.font='9px monospace';ctx.fillText('+12 dB',4,midY-h/2+10);ctx.fillText('0 dB',4,midY+3);ctx.fillText('-12 dB',4,midY+h/2-4);ctx.fillText('20Hz',4,h-2);ctx.fillText('20kHz',w-38,h-2);},[ap.g1,ap.g2,ap.g3,ap.g4,activeMod]);return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[115] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-4xl bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl flex flex-col max-h-[90vh] text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("div",{className:"w-3 h-3 rounded-full bg-cyan-400 animate-pulse"}),/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider font-mono"},track.name," \u2014 FX CHAIN")),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-white text-base"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"h-16 bg-slate-950 border border-slate-800 rounded-xl px-3 flex items-center gap-2 overflow-x-auto shrink-0 select-none"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-slate-500 uppercase tracking-widest mr-1 font-mono shrink-0"},"CHAIN:"),unifiedChain.length===0&&/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-slate-500 font-mono"},"Ch\u01B0a c\xF3 FX \u2014 b\u1EA5m [+] (DSP) ho\u1EB7c [+VST] \u0111\u1EC3 th\xEAm (signal \u0111i th\u1EB3ng: Source \u2192 Fader)"),unifiedChain.map((u,ui)=>{if(u.kind==='builtin'){const m=u.m;const meta=TRACK_FX_META[m.type]||{name:m.type,icon:'circle',color:'#94a3b8',sub:''};const on=m.active!==false;return/*#__PURE__*/React.createElement("div",{key:`b${u.i}`,draggable:true,onDragStart:e=>{dragChainIndexRef.current=ui;e.dataTransfer.effectAllowed='move';},onDragOver:e=>{e.preventDefault();e.dataTransfer.dropEffect='move';},onDrop:e=>{e.preventDefault();const from=dragChainIndexRef.current;if(from!==null&&from!==ui){const next=[...unifiedChain];const mv=next.splice(from,1)[0];next.splice(ui,0,mv);const nb=next.filter(x=>x.kind==='builtin').map(x=>x.m);const nv=next.filter(x=>x.kind==='vst').map(x=>x.m);setChain(nb);setVstChain(nv);if(mv.kind==='vst')setActiveVstIdx(nv.indexOf(mv.m));}dragChainIndexRef.current=null;},onClick:()=>{closeFxGui();setActiveType(m.type);},className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeType===m.type?'border-2 border-cyan-400 bg-slate-800':'bg-slate-900 border border-slate-800 hover:border-slate-600'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 min-w-0"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleMod(u.i);},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0",style:{backgroundColor:on?'#38bdf8':'#334155',color:on?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200 truncate"},meta.name),/*#__PURE__*/React.createElement("div",{className:"text-[9px] font-mono truncate",style:{color:meta.color}},u.i+1,". ",meta.sub))),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeMod(u.i);},className:"text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0",title:"X\xF3a module"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));}const s=u.m;return/*#__PURE__*/React.createElement("div",{key:`v${u.i}`,draggable:true,onDragStart:e=>{dragChainIndexRef.current=ui;e.dataTransfer.effectAllowed='move';},onDragOver:e=>{e.preventDefault();e.dataTransfer.dropEffect='move';},onDrop:e=>{e.preventDefault();const from=dragChainIndexRef.current;if(from!==null&&from!==ui){const next=[...unifiedChain];const mv=next.splice(from,1)[0];next.splice(ui,0,mv);const nb=next.filter(x=>x.kind==='builtin').map(x=>x.m);const nv=next.filter(x=>x.kind==='vst').map(x=>x.m);setChain(nb);setVstChain(nv);if(mv.kind==='vst')setActiveVstIdx(nv.indexOf(mv.m));}dragChainIndexRef.current=null;},onClick:()=>{closeFxGui();setActiveVstIdx(vstChain.indexOf(s));},className:`w-36 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${activeVstIdx===vstChain.indexOf(s)?'border-2 border-teal-400 bg-slate-800':'bg-slate-900 border border-teal-900/70 hover:border-teal-600'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 min-w-0"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleVst(vstChain.indexOf(s));},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0",style:{backgroundColor:!s.bypass?'#2dd4bf':'#334155',color:!s.bypass?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-teal-200 truncate"},s.name||s.path),/*#__PURE__*/React.createElement("div",{className:"text-[9px] font-mono truncate text-teal-500/80"},u.i+1,". ",s.preset_b64?'✓ preset':'no preset'))),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeVst(vstChain.indexOf(s));},className:"text-slate-600 hover:text-red-400 text-xs px-0.5 shrink-0",title:"X\xF3a slot"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})));}),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(true),className:"w-16 h-12 rounded-lg border border-dashed border-slate-700 hover:border-cyan-500 flex items-center justify-center text-slate-500 hover:text-cyan-400 cursor-pointer transition-all bg-slate-900/40 shrink-0",title:"Th\xEAm module DSP builtin"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-4 h-4"})),/*#__PURE__*/React.createElement("button",{onClick:()=>setVstPickerOpen(true),className:"w-16 h-12 rounded-lg border border-dashed border-teal-800 hover:border-teal-500 flex items-center justify-center text-teal-500 hover:text-teal-300 cursor-pointer transition-all bg-slate-900/40 shrink-0",title:"Th\xEAm VST FX (Plugin Picker theo category)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plug",className:"w-3.5 h-3.5"}),/*#__PURE__*/React.createElement("span",{className:"text-[9px] font-bold font-mono ml-0.5"},"VST"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between flex-wrap gap-2 shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 font-mono truncate"},"VST3 \u2014 realtime qua bridge (SHM) khi play + \xE1p d\u1EE5ng l\xFAc Export/Offline render"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:previewFx,className:"px-2 py-1 bg-teal-800 hover:bg-teal-700 text-teal-100 text-[10px] font-semibold rounded transition flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"play",className:"w-3 h-3"})," Nghe th\u1EED (render offline)"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-slate-400 font-mono cursor-pointer"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:vstBypass,onChange:e=>{const val=e.target.checked;setVstBypass(val);if(onUpdateTrack)onUpdateTrack(track.id,{vstFxBypass:val});if(window.__rebuildTrackFxGraph)window.__rebuildTrackFxGraph(track.id,{vstFxBypass:val});},className:"w-3 h-3"})," Bypass"))),fxGuiOpen?/*#__PURE__*/React.createElement("div",{className:"flex-1 min-h-0"},/*#__PURE__*/React.createElement(VstGuiEmbed,{url:fxGuiOpen.url,onClose:closeFxGui,onDead:()=>{window.showToast&&window.showToast('Bridge VST GUI đã mất kết nối — trả về graph','error');closeFxGui();}})):/*#__PURE__*/React.createElement(React.Fragment,null,activeVstIdx>=0&&activeVstIdxapplyPreset(e.target.value),className:"bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-xs outline-none focus:border-cyan-500"},Object.keys(TRACK_EQ_PRESETS).map(k=>/*#__PURE__*/React.createElement("option",{key:k,value:k},TRACK_EQ_PRESETS[k].name))))),!activeMod&&/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-500 font-mono text-center py-10"},"Ch\u01B0a c\xF3 module. B\u1EA5m [+] \u0111\u1EC3 th\xEAm EQ / Compressor / Limiter / Exciter / Rebalance."),activeMod&&activeMod.type==='eq'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"space-y-3"},/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-4 gap-4"},[['g1','BAND 1 (LOW)','100 Hz','#22d3ee'],['g2','BAND 2 (MID LOW)','800 Hz','#fbbf24'],['g3','BAND 3 (MID HIGH)','3.2 kHz','#a855f7'],['g4','BAND 4 (HIGH)','10 kHz','#34d399']].map(([key,label,freq,color])=>/*#__PURE__*/React.createElement("div",{key:key,className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg space-y-2"},slider(label,ap[key]!==undefined?ap[key]:0,-12,12,0.1,color,v=>setParams(activeIdx,{[key]:v}),v=>`${v>0?'+':''}${v.toFixed(1)} dB`),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono text-center"},freq)))),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 rounded-lg p-2"},/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 font-mono mb-1 flex justify-between"},/*#__PURE__*/React.createElement("span",null,"VECTOR DISPLAY \u2014 EQ RESPONSE"),/*#__PURE__*/React.createElement("span",null,"20Hz \u2013 20kHz")),/*#__PURE__*/React.createElement("canvas",{ref:eqCurveRef,className:"w-full h-[88px] block"}))),activeMod&&activeMod.type==='eqpro'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"space-y-3"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-teal-400 font-mono uppercase tracking-widest"},"PARAMETRIC / GRAPHIC EQ PRO \u2014 Pro-Q style interactive"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-slate-500 font-mono"},"20Hz \u2013 20kHz \xB7 \xB124dB \xB7 ",EQPRO_MAX_BANDS," bands max")),/*#__PURE__*/React.createElement(InteractiveEqPro,{track:track,params:ap,onChange:next=>setParams(activeIdx,next),getModule:()=>window.__getTrackFxModule?window.__getTrackFxModule(track.id,'eqpro'):null,spectrumModules:()=>{const a=window.__getTrackFxModule?window.__getTrackFxModule(track.id,'eqpro'):null;const b=window.__getTrackSfFxModule?window.__getTrackSfFxModule(track.id,'eqpro'):null;return[a,b].filter(Boolean);}})),activeMod&&activeMod.type==='compressor'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('THRESHOLD',ap.threshold,-60,0,0.5,'#fbbf24',v=>setParams(activeIdx,{threshold:v}),v=>`${v.toFixed(1)} dB`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('RATIO',ap.ratio,1,20,0.5,'#f59e0b',v=>setParams(activeIdx,{ratio:v}),v=>`${v.toFixed(1)} : 1`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('MAKE-UP',ap.makeup,0,12,0.1,'#f59e0b',v=>setParams(activeIdx,{makeup:v}),v=>`${v.toFixed(1)} dB`))),activeMod&&activeMod.type==='limiter'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('CEILING',ap.ceiling,-24,0,0.1,'#f43f5e',v=>setParams(activeIdx,{ceiling:v}),v=>`${v.toFixed(1)} dB`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg flex items-center justify-center text-[10px] text-slate-500 font-mono text-center leading-snug"},"True-Peak limiting",/*#__PURE__*/React.createElement("br",null),"Ratio 20:1 \xB7 Knee 0dB")),activeMod&&activeMod.type==='exciter'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('DRIVE / MIX',ap.drive,0,100,1,'#c084fc',v=>setParams(activeIdx,{drive:v}),v=>`${v}%`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg flex items-center justify-center text-[10px] text-slate-500 font-mono text-center leading-snug"},"WaveShaper saturation",/*#__PURE__*/React.createElement("br",null),"High-pass 2kHz \xB7 4\xD7 oversampled")),activeMod&&activeMod.type==='rebalance'&&activeIdx>=0&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('MID GAIN',ap.mid,-12,12,0.1,'#38bdf8',v=>setParams(activeIdx,{mid:v}),v=>`${v>0?'+':''}${v.toFixed(1)} dB`)),/*#__PURE__*/React.createElement("div",{className:"bg-slate-900/80 border border-slate-800 p-3 rounded-lg"},slider('SIDE GAIN',ap.side,-12,12,0.1,'#22d3ee',v=>setParams(activeIdx,{side:v}),v=>`${v>0?'+':''}${v.toFixed(1)} dB`)))),/*#__PURE__*/React.createElement("div",{className:"bg-slate-950 border border-slate-800 rounded-xl p-3 flex flex-col gap-2 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-xs font-mono border-b border-slate-800/80 pb-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"font-bold text-white tracking-wider"},"WAVE OBSERVER"),/*#__PURE__*/React.createElement("span",{className:"text-[9px] bg-cyan-950 border border-cyan-800 text-cyan-400 px-1.5 py-0.5 rounded"},"Real-time Oscilloscope")),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-slate-500 truncate max-w-[200px]"},"Context: ",track.name)),/*#__PURE__*/React.createElement("div",{className:"relative w-full h-36 bg-slate-950 border border-slate-900 rounded-lg overflow-hidden"},/*#__PURE__*/React.createElement("canvas",{ref:scopeCanvasRef,className:"w-full h-full block cursor-crosshair"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between bg-slate-900/90 border border-slate-800 rounded-lg px-3 py-1.5 text-xs font-mono flex-wrap gap-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 pr-3 border-r border-slate-800"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-slate-400"},"Input"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 w-12"},/*#__PURE__*/React.createElement("div",{className:"h-1.5 bg-slate-950 rounded overflow-hidden flex"},/*#__PURE__*/React.createElement("div",{ref:scopeMeterLRef,className:"h-full bg-cyan-400 w-0 transition-all"})),/*#__PURE__*/React.createElement("div",{className:"h-1.5 bg-slate-950 rounded overflow-hidden flex"},/*#__PURE__*/React.createElement("div",{ref:scopeMeterRRef,className:"h-full bg-cyan-400 w-0 transition-all"})))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"Channel:"),/*#__PURE__*/React.createElement("select",{value:scopeChannel,onChange:e=>setScopeChannel(e.target.value),className:"bg-slate-950 border border-slate-800 text-slate-300 rounded px-1.5 py-0.5 text-[11px] outline-none"},/*#__PURE__*/React.createElement("option",{value:"stereo"},"Stereo"),/*#__PURE__*/React.createElement("option",{value:"left"},"Left"),/*#__PURE__*/React.createElement("option",{value:"right"},"Right"),/*#__PURE__*/React.createElement("option",{value:"mid"},"Mid"),/*#__PURE__*/React.createElement("option",{value:"side"},"Side"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"Mode:"),/*#__PURE__*/React.createElement("select",{value:scopeMode,onChange:e=>setScopeMode(e.target.value),className:"bg-slate-950 border border-slate-800 text-slate-300 rounded px-1.5 py-0.5 text-[11px] outline-none"},/*#__PURE__*/React.createElement("option",{value:"waveform"},"Waveform"),/*#__PURE__*/React.createElement("option",{value:"lissajous"},"Lissajous"),/*#__PURE__*/React.createElement("option",{value:"spectrum"},"Spectrum"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"Duration:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold text-[11px] w-12"},scopeDuration.toFixed(2),"s"),/*#__PURE__*/React.createElement("input",{type:"range",min:"0.1",max:"5.0",step:"0.1",value:scopeDuration,onChange:e=>setScopeDuration(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer",style:{accentColor:'#38bdf8'}})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 text-[10px]"},"V.Zoom:"),/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-bold text-[11px] w-12"},scopeZoom>0?'+':'',scopeZoom.toFixed(1)," dB"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"20",step:"0.5",value:scopeZoom,onChange:e=>setScopeZoom(parseFloat(e.target.value)),className:"w-16 h-1 cursor-pointer",style:{accentColor:'#38bdf8'}})),/*#__PURE__*/React.createElement("button",{onClick:()=>setScopePaused(p=>!p),className:`px-3 py-1 font-semibold rounded text-[11px] border transition-colors ${scopePaused?'bg-amber-600 border-amber-400 text-white':'bg-slate-800 hover:bg-slate-700 text-slate-300 border-slate-700'}`},scopePaused?'Resume':'Pause'))))),addModuleOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[120] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:()=>setAddModuleOpen(false)},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] overflow-y-auto",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider font-mono"},"TH\xCAM MODULE V\xC0O FX CHAIN"),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(false),className:"text-slate-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3 text-xs"},Object.keys(TRACK_FX_META).map(t=>{const meta=TRACK_FX_META[t];return/*#__PURE__*/React.createElement("button",{key:t,onClick:()=>addMod(t),className:"p-3 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-xl text-left space-y-1 transition-colors"},/*#__PURE__*/React.createElement("div",{className:"font-bold flex items-center gap-1.5",style:{color:meta.color}},/*#__PURE__*/React.createElement("i",{"data-lucide":meta.icon,className:"w-3.5 h-3.5"})," ",meta.name),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400"},meta.sub));})))),/*#__PURE__*/React.createElement(VstPluginPicker,{open:vstPickerOpen,onClose:()=>setVstPickerOpen(false),plugins:fxList,loading:fxLoading,onPick:f=>addVst(f.path)}));};// ────────────────────────────────────────────── // MASTERING MODAL COMPONENT (from md/45_MASTERING_MODULE.md) // ────────────────────────────────────────────── // GUI embed state survives MasteringModal unmount so the native window @@ -1003,7 +1031,7 @@ const[masterFxList,setMasterFxList]=React.useState([]);const[masterFxLoading,set const[fxRtStats,setFxRtStats]=React.useState(null);React.useEffect(()=>{if(!isOpen)return;const iv=setInterval(()=>{if(fxRt&&fxRt.stats)setFxRtStats({...fxRt.stats});},2000);return()=>clearInterval(iv);},[isOpen]);// Wave Observer Refs & States const woCanvasRef=React.useRef(null);const woLeftHistoryRef=React.useRef(new Float32Array(400).fill(0));const woRightHistoryRef=React.useRef(new Float32Array(400).fill(0));const woLeftMeterRef=React.useRef(null);const woRightMeterRef=React.useRef(null);const[woPaused,setWoPaused]=React.useState(false);const[woChannel,setWoChannel]=React.useState('stereo');const[woMode,setWoMode]=React.useState('waveform');const[woDuration,setWoDuration]=React.useState(2.0);const[woZoom,setWoZoom]=React.useState(0.0);// Scope KHÔNG tự chạy khi mở modal (fix: Wave Observer "built-in" auto-load // khi nhấn VST FX) — chỉ render khi user bật toggle, mặc định TẮT. -const[woEnabled,setWoEnabled]=React.useState(false);const woPausedRef=React.useRef(woPaused);woPausedRef.current=woPaused;const woChannelRef=React.useRef(woChannel);woChannelRef.current=woChannel;const woModeRef=React.useRef(woMode);woModeRef.current=woMode;const woDurationRef=React.useRef(woDuration);woDurationRef.current=woDuration;const woZoomRef=React.useRef(woZoom);woZoomRef.current=woZoom;const woEnabledRef=React.useRef(woEnabled);woEnabledRef.current=woEnabled;const ozStateRef=React.useRef(ozState);ozStateRef.current=ozState;function startAudioDemo(){getAudioContext();const ctx=audioCtx;if(ctx.state==='suspended')ctx.resume();stopAudioDemo();const sampleRate=ctx.sampleRate;const bufferSize=sampleRate*4;const buffer=ctx.createBuffer(2,bufferSize,sampleRate);const left=buffer.getChannelData(0);const right=buffer.getChannelData(1);for(let i=0;i{if(!isOpen)return;getAudioContext();function resizeAll(){const resizeCanvas=ref=>{const el=ref.current;if(el){el.width=el.clientWidth;el.height=el.clientHeight;}};resizeCanvas(eqCanvasRef);resizeCanvas(imagerCanvasRef);resizeCanvas(inMeterCanvasRef);resizeCanvas(outMeterCanvasRef);resizeCanvas(woCanvasRef);}resizeAll();window.addEventListener('resize',resizeAll);const fftData=new Uint8Array(1024);function getPeakLevel(analyser){if(!analyser)return 0;const bufferLength=analyser.fftSize;const dataArray=new Float32Array(bufferLength);analyser.getFloatTimeDomainData(dataArray);let maxVal=0;for(let i=0;imaxVal){maxVal=val;}}return maxVal;}function renderFrame(){animFrameRef.current=requestAnimationFrame(renderFrame);const s=ozStateRef.current;// EQ Spectrum +const[woEnabled,setWoEnabled]=React.useState(false);const woPausedRef=React.useRef(woPaused);woPausedRef.current=woPaused;const woChannelRef=React.useRef(woChannel);woChannelRef.current=woChannel;const woModeRef=React.useRef(woMode);woModeRef.current=woMode;const woDurationRef=React.useRef(woDuration);woDurationRef.current=woDuration;const woZoomRef=React.useRef(woZoom);woZoomRef.current=woZoom;const woEnabledRef=React.useRef(woEnabled);woEnabledRef.current=woEnabled;const ozStateRef=React.useRef(ozState);ozStateRef.current=ozState;function startAudioDemo(){getAudioContext();const ctx=audioCtx;if(ctx.state==='suspended')ctx.resume();stopAudioDemo();const sampleRate=ctx.sampleRate;const bufferSize=sampleRate*4;const buffer=ctx.createBuffer(2,bufferSize,sampleRate);const left=buffer.getChannelData(0);const right=buffer.getChannelData(1);for(let i=0;i{if(!isOpen)return;getAudioContext();function resizeAll(){const resizeCanvas=ref=>{const el=ref.current;if(el){el.width=el.clientWidth;el.height=el.clientHeight;}};resizeCanvas(eqCanvasRef);resizeCanvas(imagerCanvasRef);resizeCanvas(inMeterCanvasRef);resizeCanvas(outMeterCanvasRef);resizeCanvas(woCanvasRef);}resizeAll();window.addEventListener('resize',resizeAll);const fftData=new Uint8Array(1024);let _peakScratch=new Float32Array(2048);function getPeakLevel(analyser){if(!analyser)return 0;const bufferLength=analyser.fftSize;if(_peakScratch.lengthmaxVal){maxVal=val;}}return maxVal;}function renderFrame(){animFrameRef.current=requestAnimationFrame(renderFrame);const s=ozStateRef.current;// EQ Spectrum const eqCanvas=eqCanvasRef.current;if(eqCanvas){const w=eqCanvas.width,h=eqCanvas.height;const eqCtx=eqCanvas.getContext('2d');eqCtx.clearRect(0,0,w,h);eqCtx.strokeStyle='rgba(51, 65, 85, 0.3)';eqCtx.lineWidth=1;eqCtx.font='9px JetBrains Mono';eqCtx.fillStyle='#475569';const freqs=[20,50,100,200,500,1000,2000,5000,10000,20000];freqs.forEach(f=>{const x=Math.log10(f/20)/Math.log10(20000/20)*w;eqCtx.beginPath();eqCtx.moveTo(x,0);eqCtx.lineTo(x,h);eqCtx.stroke();if(f>=1000)eqCtx.fillText(`${f/1000}k`,x+3,h-6);else eqCtx.fillText(`${f}`,x+3,h-6);});if(masterBus&&masterBus.outputAnalyser){masterBus.outputAnalyser.getByteFrequencyData(fftData);eqCtx.fillStyle='rgba(56, 189, 248, 0.15)';const barWidth=w/128;for(let i=0;i<128;i++){const val=fftData[i*4]/255;eqCtx.fillRect(i*barWidth,h-val*h,barWidth-1,val*h);}}eqCtx.strokeStyle='#38bdf8';eqCtx.lineWidth=2.5;eqCtx.beginPath();for(let x=0;x{const canvas=ctxRef.current;if(!can renderMeter(masterBus&&masterBus.analyser,outMeterCanvasRef,'outPeakText');// Loudness measurement (post-mastering output): RMS dBFS + LUFS approx // (ITU-R BS.1770 mean-square blocks, ~400ms, without K-weighting — close // enough to gauge perceived loudness of the processed master). -const outAn=masterBus&&masterBus.analyser;if(outAn){const outBuf=new Float32Array(outAn.fftSize);outAn.getFloatTimeDomainData(outBuf);let sq=0;for(let i=0;i=accLen){const blocks=lufsAccRef.current.splice(0,accLen);const m=blocks.reduce((a,b)=>a+b,0)/blocks.length;const lufs=-0.691+10*Math.log10(m+1e-12);const lufsEl=document.getElementById('lufsText');if(lufsEl)lufsEl.innerText=lufs.toFixed(1)+' LUFS';}}// Safety watchdog: if the mastering chain is broken (signal in, silence // out — e.g. a biquad in a bad state), fall back to the direct routing so // audio is NEVER globally silent. The user can re-enable mastering after. @@ -1042,7 +1070,7 @@ const MODULE_META={eq:{name:'Dynamic EQ',sub:'4-Band Peak',icon:'activity',color const EQ_PRESET_LIBRARY={flat:{name:'Flat / Reset',bands:[{id:1,type:'lowshelf',freq:100,gain:0.0,q:0.7},{id:2,type:'peaking',freq:800,gain:0.0,q:0.7},{id:3,type:'peaking',freq:3200,gain:0.0,q:1.2},{id:4,type:'highshelf',freq:10000,gain:0.0,q:0.7}]},vocal_clarity:{name:'Vocal Unmask & Clarity',bands:[{id:1,type:'lowshelf',freq:90,gain:-2.5,q:0.7},{id:2,type:'peaking',freq:500,gain:-1.8,q:1.0},{id:3,type:'peaking',freq:2800,gain:3.2,q:1.2},{id:4,type:'highshelf',freq:12000,gain:2.0,q:0.7}]},bass_punch:{name:'EDM Low-End Punch',bands:[{id:1,type:'lowshelf',freq:80,gain:4.0,q:0.8},{id:2,type:'peaking',freq:300,gain:-3.0,q:1.4},{id:3,type:'peaking',freq:4000,gain:1.5,q:1.0},{id:4,type:'highshelf',freq:10000,gain:1.0,q:0.7}]},warm_tape:{name:'Warm Vintage Analog',bands:[{id:1,type:'lowshelf',freq:120,gain:2.0,q:0.6},{id:2,type:'peaking',freq:1500,gain:1.0,q:0.5},{id:3,type:'peaking',freq:5000,gain:-2.0,q:1.0},{id:4,type:'highshelf',freq:8000,gain:-3.0,q:0.7}]}};const applyEQPreset=presetKey=>{const preset=EQ_PRESET_LIBRARY[presetKey];if(!preset)return;// Phase 4.5: EQ DSP chạy trong bridge — preset chỉ sync state; engine // nhận qua fxRtPushAllParams() ở applyMasteringSettings. // Sync UI knobs + canvas: [eqLowGain, eqMid1Gain, eqMid2Gain, eqHighGain] -setOzState(prev=>({...prev,eqLowGain:preset.bands[0]?preset.bands[0].gain:prev.eqLowGain,eqMid1Gain:preset.bands[1]?preset.bands[1].gain:prev.eqMid1Gain,eqMid2Gain:preset.bands[2]?preset.bands[2].gain:prev.eqMid2Gain,eqHighGain:preset.bands[3]?preset.bands[3].gain:prev.eqHighGain,eqPreset:presetKey}));};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"},"MASTER FX CHAIN ",/*#__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:"),(ozState.chain||[]).map((mod,idx)=>{const isVst=mod.type==='vst3';const meta=MODULE_META[mod.type]||{name:mod.type,sub:'',icon:'circle',color:'#94a3b8'};const isEditing=!isVst&&ozState.activeModule===mod.type||isVst&&activeMasterVstPath===mod.path;const isOn=isVst?mod.active!==false:chainActive(mod.type);return/*#__PURE__*/React.createElement("div",{key:mod.id,draggable:true,onDragStart:e=>{dragChainIndexRef.current=idx;e.dataTransfer.effectAllowed='move';},onDragOver:e=>{e.preventDefault();e.dataTransfer.dropEffect='move';},onDrop:e=>{e.preventDefault();const from=dragChainIndexRef.current;if(from!==null&&from!==idx)reorderChain(from,idx);dragChainIndexRef.current=null;},onClick:()=>{if(mod.type==='vst3'){openMasterVstGui({path:mod.path,name:mod.name||mod.path});}else{closeMasterGui();switchModule(mod.type);}},className:`w-40 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${isEditing?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 min-w-0"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleChainModule(mod.id);},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0",style:{backgroundColor:isOn?'#38bdf8':'#334155',color:isOn?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200 truncate"},isVst?mod.name||meta.name:meta.name),/*#__PURE__*/React.createElement("div",{className:"text-[9px] oz-font-mono truncate",style:{color:meta.color}},idx+1,". ",isVst?mod.path?mod.path.split(/[\\/]/).pop():'VST3':meta.sub))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":meta.icon,className:"w-3 h-3 text-slate-500"}),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeChainModule(mod.id);},className:"text-slate-600 hover:text-red-400 text-xs w-4 h-4 flex items-center justify-center shrink-0",title:"Xóa module"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));}),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(true),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",title:"Thêm module vào chain"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{id:"fxrt-stats-ui",className:"h-6 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-3 text-[10px] oz-font-mono shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 font-bold uppercase tracking-widest"},"FXRT"),fxRtStats?/*#__PURE__*/React.createElement("span",{className:fxRtStats.underruns>0||fxRtStats.silences>0?'text-red-400':'text-emerald-400'},"u=",fxRtStats.underruns," h=",fxRtStats.holds," f=",fxRtStats.fades," s=",fxRtStats.silences," q=",fxRtStats.queueLen," cap=",fxRtStats.capDrops," fadeIn=",fxRtStats.fadeIns):/*#__PURE__*/React.createElement("span",{className:"text-slate-600"},"off"),fxRtStats&&(fxRtStats.underruns>0||fxRtStats.silences>0)&&/*#__PURE__*/React.createElement("span",{className:"text-red-400"},"⚠ main-thread stall? Giảm tải UI / tăng FILL")),/*#__PURE__*/React.createElement("main",{className:"flex-1 flex overflow-hidden min-h-0"},masterGuiOpen?/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col bg-slate-950 relative min-h-0"},/*#__PURE__*/React.createElement(VstGuiEmbed,{url:masterGuiOpen.url,onClose:closeMasterGui,onDead:()=>{window.showToast&&window.showToast('Bridge VST GUI đã mất kết nối — trả về graph','error');closeMasterGui();}})):/*#__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]"},ozState.activeModule==='eq'&&/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"EQ Presets:"),/*#__PURE__*/React.createElement("select",{value:ozState.eqPreset||'flat',onChange:e=>applyEQPreset(e.target.value),className:"bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-[11px] outline-none focus:border-cyan-500"},Object.keys(EQ_PRESET_LIBRARY).map(k=>/*#__PURE__*/React.createElement("option",{key:k,value:k},EQ_PRESET_LIBRARY[k].name)))),/*#__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==='eqpro'?'':'hidden'}`},(()=>{const chainMods=ozState.chain||[];const cm=chainMods.find(m=>m.type==='eqpro'&&m.active!==false)||chainMods.find(m=>m.type==='eqpro');if(!cm)return/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-500 font-mono py-10 text-center"},"Chưa có module EQ PRO trong chain — bấm [+] để thêm.");return/*#__PURE__*/React.createElement(InteractiveEqPro,{track:null,params:cm.params||{},onChange:next=>updateChainEntryParams(cm.id,next),getModule:()=>null,spectrumModules:()=>[],applyTo:fn=>{const chain=fxRtChain();const slot=chain.findIndex(m=>m&&m.type==='eqpro');if(slot<0)return;fn({setBand:(idx,patch)=>{Object.keys(patch||{}).forEach(k=>{if(fxRt&&fxRt.ws&&fxRt.ws.readyState===1)fxRt.ws.send(JSON.stringify({cmd:'set_param',slot,key:'band'+idx+'_'+k,value:patch[k]}));});},setAmount:v=>{if(fxRt&&fxRt.ws&&fxRt.ws.readyState===1)fxRt.ws.send(JSON.stringify({cmd:'set_param',slot,key:'amount',value:v}));},syncBands:bands=>{(bands||[]).forEach((b,i)=>{['freq','gain','q'].forEach(k=>{if(fxRt&&fxRt.ws&&fxRt.ws.readyState===1)fxRt.ws.send(JSON.stringify({cmd:'set_param',slot,key:'band'+i+'_'+k,value:b[k]}));});});}});}});})()),/*#__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 flex items-center justify-between w-full"},/*#__PURE__*/React.createElement("span",null,/*#__PURE__*/React.createElement("i",{"data-lucide":"radio",className:"w-3.5 h-3.5"})," Polar Vectorscope & Correlation Meter"),/*#__PURE__*/React.createElement("span",{className:"text-[11px] text-slate-400 normal-case"},"Corr: ",/*#__PURE__*/React.createElement("span",{id:"corrText",className:"font-bold text-emerald-400"},"1.00"))),/*#__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:"text-[10px] text-slate-500 oz-font-mono leading-snug"},"0% = Mono · 100% = Original · 200% = 2× Width"),/*#__PURE__*/React.createElement("div",{className:"space-y-3 my-auto"},[{id:'w1',label:'Band 1 (20-100Hz)',color:'#22d3ee',val:ozState.w1},{id:'w2',label:'Band 2 (100Hz-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:"0",max:"200",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:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='compressor'?'':'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-amber-400 uppercase oz-font-mono mb-3"},"Bus Compressor"),/*#__PURE__*/React.createElement("div",{className:"my-2"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"compMakeup",min:0,max:12,value:ozState.compMakeup,unit:"dB",label:"MAKE-UP GAIN",color:"#fbbf24",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='compressor')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.compActive?'bg-amber-700 border-amber-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.compActive?'ON':'OFF')),/*#__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:"compThreshold",min:-60,max:0,value:ozState.compThreshold,unit:"dB",label:"THRESHOLD",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:"compRatio",min:1,max:20,value:ozState.compRatio,unit:":1",label:"RATIO",color:"#f59e0b",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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"Glue & Punch",/*#__PURE__*/React.createElement("br",null),"Attack 20ms · Release 250ms · Knee 8dB"))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='limiter'?'':'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-rose-400 uppercase oz-font-mono mb-3"},"Brickwall Limiter"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='limiter')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.limActive?'bg-rose-700 border-rose-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.limActive?'ON':'OFF')),/*#__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:"limThreshold",min:-24,max:0,value:ozState.limThreshold,unit:"dB",label:"CEILING",color:"#f43f5e",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 gap-2 h-full"},/*#__PURE__*/React.createElement("button",{onClick:()=>setOzState(prev=>({...prev,limMode:prev.limMode?0:1})),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors ${ozState.limMode?'bg-rose-700 border-rose-500 text-white':'bg-slate-800 border-slate-700 text-slate-400'}`},ozState.limMode?'BRICKWALL':'SOFT'),/*#__PURE__*/React.createElement(MasteringKnob,{param:"limLookahead",min:1,max:5,value:ozState.limLookahead,unit:"ms",label:"LOOKAHEAD",color:"#f43f5e",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))}))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='multiband'?'':'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-lime-400 uppercase oz-font-mono mb-3"},"Multiband Compressor"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 oz-font-mono text-center mb-2"},"LR4 crossover · low/mid/high"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule(mb?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.mbActive?'bg-lime-700 border-lime-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.mbActive?'ON':'OFF')),/*#__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:"lf_cross",min:40,max:1000,value:mb?.params?.lf_cross??200,unit:"Hz",label:"LF XOVER",color:"#a3e635",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{lf_cross: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:"hf_cross",min:1000,max:12000,value:mb?.params?.hf_cross??4000,unit:"Hz",label:"HF XOVER",color:"#a3e635",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{hf_cross: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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"Low < LF",/*#__PURE__*/React.createElement("br",null),"Mid giữa 2 crossover",/*#__PURE__*/React.createElement("br",null),"High > HF · Release 250ms")),/*#__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:"low_thr",min:-60,max:0,value:mb?.params?.low_thr??-24,unit:"dB",label:"LOW THRESHOLD",color:"#84cc16",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{low_thr: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:"low_ratio",min:1,max:20,value:mb?.params?.low_ratio??3,unit:":1",label:"LOW RATIO",color:"#84cc16",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{low_ratio: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:"low_makeup",min:0,max:12,value:mb?.params?.low_makeup??0,unit:"dB",label:"LOW MAKE-UP",color:"#84cc16",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{low_makeup: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:"mid_thr",min:-60,max:0,value:mb?.params?.mid_thr??-24,unit:"dB",label:"MID THRESHOLD",color:"#65a30d",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{mid_thr: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:"mid_ratio",min:1,max:20,value:mb?.params?.mid_ratio??3,unit:":1",label:"MID RATIO",color:"#65a30d",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{mid_ratio: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:"mid_makeup",min:0,max:12,value:mb?.params?.mid_makeup??0,unit:"dB",label:"MID MAKE-UP",color:"#65a30d",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{mid_makeup: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:"high_thr",min:-60,max:0,value:mb?.params?.high_thr??-24,unit:"dB",label:"HIGH THRESHOLD",color:"#4d7c0f",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{high_thr: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:"high_ratio",min:1,max:20,value:mb?.params?.high_ratio??3,unit:":1",label:"HIGH RATIO",color:"#4d7c0f",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{high_ratio: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:"high_makeup",min:0,max:12,value:mb?.params?.high_makeup??0,unit:"dB",label:"HIGH MAKE-UP",color:"#4d7c0f",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{high_makeup:v})}))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='deesser'?'':'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-orange-400 uppercase oz-font-mono mb-3"},"De-Esser"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 oz-font-mono text-center mb-2"},"Detect sibilance @freq · envelope release 150ms"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule(de?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.deessActive?'bg-orange-700 border-orange-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.deessActive?'ON':'OFF')),/*#__PURE__*/React.createElement("div",{className:"col-span-8 grid grid-cols-4 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:"threshold",min:-60,max:0,value:de?.params?.threshold??-30,unit:"dB",label:"THRESHOLD",color:"#fb923c",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{threshold: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:"ratio",min:0,max:100,value:de?.params?.ratio??40,unit:"%",label:"STRENGTH",color:"#fb923c",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{ratio: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:"freq",min:2000,max:12000,value:de?.params?.freq??6000,unit:"Hz",label:"FREQ",color:"#f97316",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{freq: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:"makeup",min:0,max:12,value:de?.params?.makeup??0,unit:"dB",label:"MAKE-UP",color:"#f97316",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{makeup:v})}))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='exciter'?'':'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-purple-400 uppercase oz-font-mono mb-3"},"Harmonic Exciter"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='exciter')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.excActive?'bg-purple-700 border-purple-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.excActive?'ON':'OFF')),/*#__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:"excDrive",min:0,max:100,value:ozState.excDrive,unit:"%",label:"DRIVE / MIX",color:"#c084fc",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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"WaveShaper saturation",/*#__PURE__*/React.createElement("br",null),"High-pass 2kHz",/*#__PURE__*/React.createElement("br",null),"4× oversampled · wet/dry mix"))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='rebalance'?'':'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-sky-400 uppercase oz-font-mono mb-3"},"Master Rebalance (M/S)"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='rebalance')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.rebalActive?'bg-sky-700 border-sky-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.rebalActive?'ON':'OFF')),/*#__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:"rebalMid",min:-12,max:12,value:ozState.rebalMid,unit:"dB",label:"MID GAIN",color:"#38bdf8",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:"rebalSide",min:-12,max:12,value:ozState.rebalSide,unit:"dB",label:"SIDE GAIN",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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"ChannelSplitter + M/S gains",/*#__PURE__*/React.createElement("br",null),"Center (vocal/bass) vs Sides (stereo width)")))))),/*#__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:"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"},"OUT RMS"),/*#__PURE__*/React.createElement("div",{id:"outRmsText",className:"text-sky-300 font-bold oz-font-mono"},"-inf")),/*#__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"},"LOUDNESS"),/*#__PURE__*/React.createElement("div",{id:"lufsText",className:"text-fuchsia-300 font-bold oz-font-mono"},"--.-"))),/*#__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"))))),addModuleOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[110] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:()=>setAddModuleOpen(false)},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] overflow-y-auto",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider oz-font-mono"},"THÊM MODULE VÀO MASTER FX CHAIN"),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(false),className:"text-slate-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3 text-xs"},[{t:'eqpro',c:'text-teal-400',i:'chart-area',d:'Parametric / Graphic EQ PRO — Pro-Q style, 8 bands, interactive canvas + spectrum.'},{t:'compressor',c:'text-amber-400',i:'compress',d:'Nén dynamic range, glue & punch cho master.'},{t:'limiter',c:'text-rose-400',i:'shield-half',d:'True-Peak ceiling (ratio 20:1, knee 0) chống clipping.'},{t:'exciter',c:'text-purple-400',i:'wand-2',d:'Saturation hài cho warmth và top-end brilliance.'},{t:'rebalance',c:'text-sky-400',i:'sliders-horizontal',d:'Cân bằng Mid/Side (Vocal/Bass vs stereo width).'},{t:'eq',c:'text-cyan-400',i:'activity',d:'EQ 4-band (lowshelf, 2× peaking, highshelf) + presets.'},{t:'imager',c:'text-fuchsia-400',i:'radio',d:'Stereo width 4-band M/S + vectorscope/correlation.'},{t:'maximizer',c:'text-emerald-400',i:'gauge',d:'Maximizer: boost, soft clip, upward comp, ceiling.'},{t:'multiband',c:'text-lime-400',i:'layers',d:'Compressor 3 băng tần low/mid/high với crossover LR4.'},{t:'deesser',c:'text-orange-400',i:'mic-2',d:'Giảm sibilance (âm xuýt) theo tần số phát hiện.'}].map(m=>/*#__PURE__*/React.createElement("button",{key:m.t,onClick:()=>addModuleToChain(m.t),className:"p-3 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-xl text-left space-y-1 transition-colors"},/*#__PURE__*/React.createElement("div",{className:`font-bold ${m.c} flex items-center gap-1.5`},/*#__PURE__*/React.createElement("i",{"data-lucide":m.i,className:"w-3.5 h-3.5"})," ",MODULE_META[m.t].name),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400"},m.d)))),/*#__PURE__*/React.createElement("div",{className:"border-t border-slate-800 pt-3"},/*#__PURE__*/React.createElement("div",{className:"text-[10px] font-bold text-teal-400 uppercase tracking-wider oz-font-mono mb-2"},"VST FX (Ozone...)"),/*#__PURE__*/React.createElement("button",{onClick:()=>setMasterVstPickerOpen(true),className:"w-full bg-slate-950 hover:bg-slate-800 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-2 text-[11px] text-left transition-colors",title:"Plugin Picker theo category (EQ / Reverb / Comp / Utility)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plug",className:"w-3 h-3 inline-block mr-1"})," Chọn VST FX từ thư viện — Picker: EQ / Reverb / Comp / Utility")))),/*#__PURE__*/React.createElement(VstPluginPicker,{open:masterVstPickerOpen,onClose:()=>setMasterVstPickerOpen(false),plugins:masterFxList,loading:masterFxLoading,onPick:f=>addVstToChain(f.path)}));};// ── Media Explorer Panel (from md/51_MEDIA_EXPLORER.md) ── +setOzState(prev=>({...prev,eqLowGain:preset.bands[0]?preset.bands[0].gain:prev.eqLowGain,eqMid1Gain:preset.bands[1]?preset.bands[1].gain:prev.eqMid1Gain,eqMid2Gain:preset.bands[2]?preset.bands[2].gain:prev.eqMid2Gain,eqHighGain:preset.bands[3]?preset.bands[3].gain:prev.eqHighGain,eqPreset:presetKey}));};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"},"MASTER FX CHAIN ",/*#__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"},"\u0110\xF3ng"),/*#__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:"),(ozState.chain||[]).map((mod,idx)=>{const isVst=mod.type==='vst3';const meta=MODULE_META[mod.type]||{name:mod.type,sub:'',icon:'circle',color:'#94a3b8'};const isEditing=!isVst&&ozState.activeModule===mod.type||isVst&&activeMasterVstPath===mod.path;const isOn=isVst?mod.active!==false:chainActive(mod.type);return/*#__PURE__*/React.createElement("div",{key:mod.id,draggable:true,onDragStart:e=>{dragChainIndexRef.current=idx;e.dataTransfer.effectAllowed='move';},onDragOver:e=>{e.preventDefault();e.dataTransfer.dropEffect='move';},onDrop:e=>{e.preventDefault();const from=dragChainIndexRef.current;if(from!==null&&from!==idx)reorderChain(from,idx);dragChainIndexRef.current=null;},onClick:()=>{if(mod.type==='vst3'){openMasterVstGui({path:mod.path,name:mod.name||mod.path});}else{closeMasterGui();switchModule(mod.type);}},className:`w-40 h-12 rounded-lg p-2 flex items-center justify-between cursor-pointer transition-all shrink-0 ${isEditing?'oz-card-active':'oz-card'}`},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 min-w-0"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleChainModule(mod.id);},className:"w-4 h-4 rounded-full flex items-center justify-center text-[9px] font-bold shrink-0",style:{backgroundColor:isOn?'#38bdf8':'#334155',color:isOn?'#0f172a':'#94a3b8'}},/*#__PURE__*/React.createElement("i",{"data-lucide":"power",className:"w-2.5 h-2.5"})),/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-slate-200 truncate"},isVst?mod.name||meta.name:meta.name),/*#__PURE__*/React.createElement("div",{className:"text-[9px] oz-font-mono truncate",style:{color:meta.color}},idx+1,". ",isVst?mod.path?mod.path.split(/[\\/]/).pop():'VST3':meta.sub))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":meta.icon,className:"w-3 h-3 text-slate-500"}),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();removeChainModule(mod.id);},className:"text-slate-600 hover:text-red-400 text-xs w-4 h-4 flex items-center justify-center shrink-0",title:"X\xF3a module"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));}),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(true),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",title:"Th\xEAm module v\xE0o chain"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{id:"fxrt-stats-ui",className:"h-6 bg-slate-950 border-b border-slate-800 px-4 flex items-center gap-3 text-[10px] oz-font-mono shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-slate-500 font-bold uppercase tracking-widest"},"FXRT"),fxRtStats?/*#__PURE__*/React.createElement("span",{className:fxRtStats.underruns>0||fxRtStats.silences>0?'text-red-400':'text-emerald-400'},"u=",fxRtStats.underruns," h=",fxRtStats.holds," f=",fxRtStats.fades," s=",fxRtStats.silences," q=",fxRtStats.queueLen," cap=",fxRtStats.capDrops," fadeIn=",fxRtStats.fadeIns):/*#__PURE__*/React.createElement("span",{className:"text-slate-600"},"off"),fxRtStats&&(fxRtStats.underruns>0||fxRtStats.silences>0)&&/*#__PURE__*/React.createElement("span",{className:"text-red-400"},"\u26A0 main-thread stall? Gi\u1EA3m t\u1EA3i UI / t\u0103ng FILL")),/*#__PURE__*/React.createElement("main",{className:"flex-1 flex overflow-hidden min-h-0"},masterGuiOpen?/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col bg-slate-950 relative min-h-0"},/*#__PURE__*/React.createElement(VstGuiEmbed,{url:masterGuiOpen.url,onClose:closeMasterGui,onDead:()=>{window.showToast&&window.showToast('Bridge VST GUI đã mất kết nối — trả về graph','error');closeMasterGui();}})):/*#__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]"},ozState.activeModule==='eq'&&/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"text-slate-400"},"EQ Presets:"),/*#__PURE__*/React.createElement("select",{value:ozState.eqPreset||'flat',onChange:e=>applyEQPreset(e.target.value),className:"bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-[11px] outline-none focus:border-cyan-500"},Object.keys(EQ_PRESET_LIBRARY).map(k=>/*#__PURE__*/React.createElement("option",{key:k,value:k},EQ_PRESET_LIBRARY[k].name)))),/*#__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==='eqpro'?'':'hidden'}`},(()=>{const chainMods=ozState.chain||[];const cm=chainMods.find(m=>m.type==='eqpro'&&m.active!==false)||chainMods.find(m=>m.type==='eqpro');if(!cm)return/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-500 font-mono py-10 text-center"},"Ch\u01B0a c\xF3 module EQ PRO trong chain \u2014 b\u1EA5m [+] \u0111\u1EC3 th\xEAm.");return/*#__PURE__*/React.createElement(InteractiveEqPro,{track:null,params:cm.params||{},onChange:next=>updateChainEntryParams(cm.id,next),getModule:()=>null,spectrumModules:()=>[],applyTo:fn=>{const chain=fxRtChain();const slot=chain.findIndex(m=>m&&m.type==='eqpro');if(slot<0)return;fn({setBand:(idx,patch)=>{Object.keys(patch||{}).forEach(k=>{if(fxRt&&fxRt.ws&&fxRt.ws.readyState===1)fxRt.ws.send(JSON.stringify({cmd:'set_param',slot,key:'band'+idx+'_'+k,value:patch[k]}));});},setAmount:v=>{if(fxRt&&fxRt.ws&&fxRt.ws.readyState===1)fxRt.ws.send(JSON.stringify({cmd:'set_param',slot,key:'amount',value:v}));},syncBands:bands=>{(bands||[]).forEach((b,i)=>{['freq','gain','q'].forEach(k=>{if(fxRt&&fxRt.ws&&fxRt.ws.readyState===1)fxRt.ws.send(JSON.stringify({cmd:'set_param',slot,key:'band'+i+'_'+k,value:b[k]}));});});}});}});})()),/*#__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 flex items-center justify-between w-full"},/*#__PURE__*/React.createElement("span",null,/*#__PURE__*/React.createElement("i",{"data-lucide":"radio",className:"w-3.5 h-3.5"})," Polar Vectorscope & Correlation Meter"),/*#__PURE__*/React.createElement("span",{className:"text-[11px] text-slate-400 normal-case"},"Corr: ",/*#__PURE__*/React.createElement("span",{id:"corrText",className:"font-bold text-emerald-400"},"1.00"))),/*#__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:"text-[10px] text-slate-500 oz-font-mono leading-snug"},"0% = Mono \xB7 100% = Original \xB7 200% = 2\xD7 Width"),/*#__PURE__*/React.createElement("div",{className:"space-y-3 my-auto"},[{id:'w1',label:'Band 1 (20-100Hz)',color:'#22d3ee',val:ozState.w1},{id:'w2',label:'Band 2 (100Hz-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:"0",max:"200",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:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='compressor'?'':'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-amber-400 uppercase oz-font-mono mb-3"},"Bus Compressor"),/*#__PURE__*/React.createElement("div",{className:"my-2"},/*#__PURE__*/React.createElement(MasteringKnob,{param:"compMakeup",min:0,max:12,value:ozState.compMakeup,unit:"dB",label:"MAKE-UP GAIN",color:"#fbbf24",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))})),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='compressor')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.compActive?'bg-amber-700 border-amber-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.compActive?'ON':'OFF')),/*#__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:"compThreshold",min:-60,max:0,value:ozState.compThreshold,unit:"dB",label:"THRESHOLD",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:"compRatio",min:1,max:20,value:ozState.compRatio,unit:":1",label:"RATIO",color:"#f59e0b",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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"Glue & Punch",/*#__PURE__*/React.createElement("br",null),"Attack 20ms \xB7 Release 250ms \xB7 Knee 8dB"))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='limiter'?'':'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-rose-400 uppercase oz-font-mono mb-3"},"Brickwall Limiter"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='limiter')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.limActive?'bg-rose-700 border-rose-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.limActive?'ON':'OFF')),/*#__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:"limThreshold",min:-24,max:0,value:ozState.limThreshold,unit:"dB",label:"CEILING",color:"#f43f5e",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 gap-2 h-full"},/*#__PURE__*/React.createElement("button",{onClick:()=>setOzState(prev=>({...prev,limMode:prev.limMode?0:1})),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors ${ozState.limMode?'bg-rose-700 border-rose-500 text-white':'bg-slate-800 border-slate-700 text-slate-400'}`},ozState.limMode?'BRICKWALL':'SOFT'),/*#__PURE__*/React.createElement(MasteringKnob,{param:"limLookahead",min:1,max:5,value:ozState.limLookahead,unit:"ms",label:"LOOKAHEAD",color:"#f43f5e",onChange:(p,v)=>setOzState(prev=>({...prev,[p]:v}))}))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='multiband'?'':'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-lime-400 uppercase oz-font-mono mb-3"},"Multiband Compressor"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 oz-font-mono text-center mb-2"},"LR4 crossover \xB7 low/mid/high"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule(mb?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.mbActive?'bg-lime-700 border-lime-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.mbActive?'ON':'OFF')),/*#__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:"lf_cross",min:40,max:1000,value:mb?.params?.lf_cross??200,unit:"Hz",label:"LF XOVER",color:"#a3e635",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{lf_cross: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:"hf_cross",min:1000,max:12000,value:mb?.params?.hf_cross??4000,unit:"Hz",label:"HF XOVER",color:"#a3e635",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{hf_cross: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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"Low < LF",/*#__PURE__*/React.createElement("br",null),"Mid gi\u1EEFa 2 crossover",/*#__PURE__*/React.createElement("br",null),"High > HF \xB7 Release 250ms")),/*#__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:"low_thr",min:-60,max:0,value:mb?.params?.low_thr??-24,unit:"dB",label:"LOW THRESHOLD",color:"#84cc16",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{low_thr: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:"low_ratio",min:1,max:20,value:mb?.params?.low_ratio??3,unit:":1",label:"LOW RATIO",color:"#84cc16",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{low_ratio: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:"low_makeup",min:0,max:12,value:mb?.params?.low_makeup??0,unit:"dB",label:"LOW MAKE-UP",color:"#84cc16",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{low_makeup: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:"mid_thr",min:-60,max:0,value:mb?.params?.mid_thr??-24,unit:"dB",label:"MID THRESHOLD",color:"#65a30d",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{mid_thr: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:"mid_ratio",min:1,max:20,value:mb?.params?.mid_ratio??3,unit:":1",label:"MID RATIO",color:"#65a30d",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{mid_ratio: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:"mid_makeup",min:0,max:12,value:mb?.params?.mid_makeup??0,unit:"dB",label:"MID MAKE-UP",color:"#65a30d",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{mid_makeup: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:"high_thr",min:-60,max:0,value:mb?.params?.high_thr??-24,unit:"dB",label:"HIGH THRESHOLD",color:"#4d7c0f",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{high_thr: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:"high_ratio",min:1,max:20,value:mb?.params?.high_ratio??3,unit:":1",label:"HIGH RATIO",color:"#4d7c0f",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{high_ratio: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:"high_makeup",min:0,max:12,value:mb?.params?.high_makeup??0,unit:"dB",label:"HIGH MAKE-UP",color:"#4d7c0f",onChange:(p,v)=>mb&&updateChainEntryParams(mb.id,{high_makeup:v})}))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='deesser'?'':'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-orange-400 uppercase oz-font-mono mb-3"},"De-Esser"),/*#__PURE__*/React.createElement("div",{className:"text-[9px] text-slate-500 oz-font-mono text-center mb-2"},"Detect sibilance @freq \xB7 envelope release 150ms"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule(de?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.deessActive?'bg-orange-700 border-orange-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.deessActive?'ON':'OFF')),/*#__PURE__*/React.createElement("div",{className:"col-span-8 grid grid-cols-4 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:"threshold",min:-60,max:0,value:de?.params?.threshold??-30,unit:"dB",label:"THRESHOLD",color:"#fb923c",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{threshold: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:"ratio",min:0,max:100,value:de?.params?.ratio??40,unit:"%",label:"STRENGTH",color:"#fb923c",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{ratio: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:"freq",min:2000,max:12000,value:de?.params?.freq??6000,unit:"Hz",label:"FREQ",color:"#f97316",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{freq: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:"makeup",min:0,max:12,value:de?.params?.makeup??0,unit:"dB",label:"MAKE-UP",color:"#f97316",onChange:(p,v)=>de&&updateChainEntryParams(de.id,{makeup:v})}))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='exciter'?'':'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-purple-400 uppercase oz-font-mono mb-3"},"Harmonic Exciter"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='exciter')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.excActive?'bg-purple-700 border-purple-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.excActive?'ON':'OFF')),/*#__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:"excDrive",min:0,max:100,value:ozState.excDrive,unit:"%",label:"DRIVE / MIX",color:"#c084fc",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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"WaveShaper saturation",/*#__PURE__*/React.createElement("br",null),"High-pass 2kHz",/*#__PURE__*/React.createElement("br",null),"4\xD7 oversampled \xB7 wet/dry mix"))))),/*#__PURE__*/React.createElement("div",{className:`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule==='rebalance'?'':'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-sky-400 uppercase oz-font-mono mb-3"},"Master Rebalance (M/S)"),/*#__PURE__*/React.createElement("button",{onClick:()=>toggleChainModule((ozState.chain||[]).find(m=>m.type==='rebalance')?.id),className:`px-3 py-1 rounded text-[10px] font-bold border transition-colors mt-2 ${ozState.rebalActive?'bg-sky-700 border-sky-500 text-white':'bg-slate-800 border-slate-700 text-slate-300'}`},ozState.rebalActive?'ON':'OFF')),/*#__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:"rebalMid",min:-12,max:12,value:ozState.rebalMid,unit:"dB",label:"MID GAIN",color:"#38bdf8",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:"rebalSide",min:-12,max:12,value:ozState.rebalSide,unit:"dB",label:"SIDE GAIN",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("span",{className:"text-[10px] text-slate-500 oz-font-mono text-center leading-snug px-2"},"ChannelSplitter + M/S gains",/*#__PURE__*/React.createElement("br",null),"Center (vocal/bass) vs Sides (stereo width)")))))),/*#__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:"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"},"OUT RMS"),/*#__PURE__*/React.createElement("div",{id:"outRmsText",className:"text-sky-300 font-bold oz-font-mono"},"-inf")),/*#__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"},"LOUDNESS"),/*#__PURE__*/React.createElement("div",{id:"lufsText",className:"text-fuchsia-300 font-bold oz-font-mono"},"--.-"))),/*#__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"))))),addModuleOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[110] bg-black/80 backdrop-blur-sm flex items-center justify-center p-4",onClick:()=>setAddModuleOpen(false)},/*#__PURE__*/React.createElement("div",{className:"w-full max-w-lg bg-slate-900 border border-slate-700 rounded-2xl p-5 space-y-4 shadow-2xl max-h-[80vh] overflow-y-auto",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between border-b border-slate-800 pb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-xs font-bold text-white uppercase tracking-wider oz-font-mono"},"TH\xCAM MODULE V\xC0O MASTER FX CHAIN"),/*#__PURE__*/React.createElement("button",{onClick:()=>setAddModuleOpen(false),className:"text-slate-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3 text-xs"},[{t:'eqpro',c:'text-teal-400',i:'chart-area',d:'Parametric / Graphic EQ PRO — Pro-Q style, 8 bands, interactive canvas + spectrum.'},{t:'compressor',c:'text-amber-400',i:'compress',d:'Nén dynamic range, glue & punch cho master.'},{t:'limiter',c:'text-rose-400',i:'shield-half',d:'True-Peak ceiling (ratio 20:1, knee 0) chống clipping.'},{t:'exciter',c:'text-purple-400',i:'wand-2',d:'Saturation hài cho warmth và top-end brilliance.'},{t:'rebalance',c:'text-sky-400',i:'sliders-horizontal',d:'Cân bằng Mid/Side (Vocal/Bass vs stereo width).'},{t:'eq',c:'text-cyan-400',i:'activity',d:'EQ 4-band (lowshelf, 2× peaking, highshelf) + presets.'},{t:'imager',c:'text-fuchsia-400',i:'radio',d:'Stereo width 4-band M/S + vectorscope/correlation.'},{t:'maximizer',c:'text-emerald-400',i:'gauge',d:'Maximizer: boost, soft clip, upward comp, ceiling.'},{t:'multiband',c:'text-lime-400',i:'layers',d:'Compressor 3 băng tần low/mid/high với crossover LR4.'},{t:'deesser',c:'text-orange-400',i:'mic-2',d:'Giảm sibilance (âm xuýt) theo tần số phát hiện.'}].map(m=>/*#__PURE__*/React.createElement("button",{key:m.t,onClick:()=>addModuleToChain(m.t),className:"p-3 bg-slate-950 hover:bg-slate-800 border border-slate-800 rounded-xl text-left space-y-1 transition-colors"},/*#__PURE__*/React.createElement("div",{className:`font-bold ${m.c} flex items-center gap-1.5`},/*#__PURE__*/React.createElement("i",{"data-lucide":m.i,className:"w-3.5 h-3.5"})," ",MODULE_META[m.t].name),/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-slate-400"},m.d)))),/*#__PURE__*/React.createElement("div",{className:"border-t border-slate-800 pt-3"},/*#__PURE__*/React.createElement("div",{className:"text-[10px] font-bold text-teal-400 uppercase tracking-wider oz-font-mono mb-2"},"VST FX (Ozone...)"),/*#__PURE__*/React.createElement("button",{onClick:()=>setMasterVstPickerOpen(true),className:"w-full bg-slate-950 hover:bg-slate-800 border border-dashed border-slate-700 hover:border-teal-600 text-teal-300 rounded px-2 py-2 text-[11px] text-left transition-colors",title:"Plugin Picker theo category (EQ / Reverb / Comp / Utility)"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plug",className:"w-3 h-3 inline-block mr-1"})," Ch\u1ECDn VST FX t\u1EEB th\u01B0 vi\u1EC7n \u2014 Picker: EQ / Reverb / Comp / Utility")))),/*#__PURE__*/React.createElement(VstPluginPicker,{open:masterVstPickerOpen,onClose:()=>setMasterVstPickerOpen(false),plugins:masterFxList,loading:masterFxLoading,onPick:f=>addVstToChain(f.path)}));};// ── 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,clipboardRef,active})=>{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[audioDuration,setAudioDuration]=React.useState(0);const[midiNotes,setMidiNotes]=React.useState(null);const[midiTotal,setMidiTotal]=React.useState(4);const[midiBars,setMidiBars]=React.useState(1);const[midiTotalBeats,setMidiTotalBeats]=React.useState(16);const[midiFileBpm,setMidiFileBpm]=React.useState(120);const[tempo,setTempo]=React.useState(function(){var saved=localStorage.getItem('studio_media_explorer_tempo');return saved?parseInt(saved):120;});const[tempoText,setTempoText]=React.useState(String(function(){var saved=localStorage.getItem('studio_media_explorer_tempo');return saved?parseInt(saved):120;}()));const[zoom,setZoom]=React.useState(1.0);// Đồng bộ zoom ra window ref — serializeProjectToSchema lưu vào metadata.zoom // (module-level không truy cập state) → restore/open khôi phục đúng zoom. React.useEffect(()=>{window.__currentZoom=zoom;},[zoom]);const[scrollOffset,setScrollOffset]=React.useState(0);const scrollOffsetRef=React.useRef(0);scrollOffsetRef.current=scrollOffset;const[selStart,setSelStart]=React.useState(null);const[selEnd,setSelEnd]=React.useState(null);const[isDragging,setIsDragging]=React.useState(false);const[previewCtxMenu,setPreviewCtxMenu]=React.useState(null);// { x, y } @@ -1132,17 +1160,17 @@ isLoopingRef.current=next;const cur=selectedRef.current;const st=playStateRef.cu // the loop points to the current selection so it loops continuously // over the selected region until Stop is pressed. if(next&&st.source.buffer){if(hasSelection){st.source.loopStart=Math.min(sStart,sEnd);st.source.loopEnd=Math.max(sStart,sEnd);}else{st.source.loopStart=0;st.source.loopEnd=st.source.buffer.duration;}}}if(next&&isMidiFile(cur)){// Re-schedule loop for the currently previewing MIDI file -if(cur&&(cur.handle||cur.path||cur.file_id||cur.fileId||cur.kind==='midi'&&cur.name)){selectTokenRef.current++;const token=selectTokenRef.current;try{if(window.SonicMidiRouter)window.SonicMidiRouter.panic();else if(window.SonicSF&&typeof window.SonicSF.stopAll==='function')window.SonicSF.stopAll();}catch(e){}stopAllNativeSfNotes();stopAllNativeVstNotes();playMidiPreview(cur,token);}}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||tempo;return/*#__PURE__*/React.createElement("div",{ref:containerRef,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 relative"},/*#__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",{ref:treePaneRef,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 font-bold ${folder==='computer'&&computerPath==='favorited'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>{openFavorited();setFavoritedExpanded(!favoritedExpanded);}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-star text-amber-500 text-[11px] font-bold"})," Favorited"),favoritedExpanded&&/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},favorites.map((fav,fi)=>/*#__PURE__*/React.createElement("div",{key:fav.path+fi,"data-tree-path":fav.path,className:"flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm hover:bg-amber-100 text-slate-800",style:{paddingLeft:20},onClick:()=>openFavorite(fav),onContextMenu:e=>{e.preventDefault();e.stopPropagation();setFavContext(Object.assign({},fav,{x:e.clientX,y:e.clientY}));}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752] shrink-0"}),/*#__PURE__*/React.createElement("span",{className:"truncate flex-1"},fav.name),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-star text-amber-500 text-[9px] shrink-0"}))),favorites.length===0&&/*#__PURE__*/React.createElement("div",{className:"pl-4 py-0.5 text-slate-400 italic text-[11px]"},"No favorites")),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder==='computer'&&computerPath!=='favorited'?'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'&&computerPath!=='favorited'&&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:"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"})),favContext&&/*#__PURE__*/React.createElement("div",{className:"fixed z-[100] bg-white border border-[#808080] shadow-lg rounded-sm text-xs font-sans text-slate-800 min-w-[180px]",style:{left:favContext.x,top:favContext.y},onMouseLeave:()=>setFavContext(null)},/*#__PURE__*/React.createElement("div",{className:`px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5 ${isFavorite(favContext)?'text-amber-700':''}`,onClick:()=>{toggleFavorite(favContext);setFavContext(null);}},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isFavorite(favContext)?'fa-star text-amber-500':'fa-star text-slate-400'} text-[11px]`}),isFavorite(favContext)?'Gỡ khỏi Favorited':'Thêm vào Favorited'),/*#__PURE__*/React.createElement("div",{className:"px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5",onClick:()=>{if(favContext)browseComputerDir(favContext);setFavContext(null);}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752] text-[11px]"})," Mở thư mục")),/*#__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",style:{tableLayout:'fixed'}},/*#__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] relative",style:viewMode==='details'?{width:colWidths.file}:undefined},/*#__PURE__*/React.createElement("div",{className:"truncate pr-2"},"File"),/*#__PURE__*/React.createElement("div",{className:"absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400",onMouseDown:e=>startColResize('file',e)})),viewMode==='details'&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0] relative",style:{width:colWidths.size}},/*#__PURE__*/React.createElement("div",{className:"truncate pr-2"},"Size"),/*#__PURE__*/React.createElement("div",{className:"absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400",onMouseDown:e=>startColResize('size',e)})),/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0] relative",style:{width:colWidths.type}},/*#__PURE__*/React.createElement("div",{className:"truncate pr-2"},"Type"),/*#__PURE__*/React.createElement("div",{className:"absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400",onMouseDown:e=>startColResize('type',e)}))))),/*#__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,draggable:!f.is_dir,onDragStart:e=>{if(f.is_dir){e.preventDefault();return;}e.dataTransfer.setData('text/plain',f.name||f.original_name||'');e.dataTransfer.effectAllowed='copy';window.__mediaExplorerDragFile=f;},onDragEnd:()=>{window.__mediaExplorerDragFile=null;},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),onContextMenu:e=>{if(f.is_dir){e.preventDefault();e.stopPropagation();setFavContext(Object.assign({},f,{x:e.clientX,y:e.clientY}));}}},/*#__PURE__*/React.createElement("td",{className:"py-1 px-2 truncate"},/*#__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 truncate"},f.size_mb!=null?f.size_mb.toFixed(2)+' MB':isMidi?(f.tpqn||'MIDI')+' TPQN':'-'),viewMode==='details'&&/*#__PURE__*/React.createElement("td",{className:"py-1 px-2 truncate"},f.is_dir?'Folder':isMidi?'MIDI':f.kind==='audio'?'Audio':'File'));}),visibleFiles.length===0&&/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("td",{className:"py-3 px-2 text-slate-400 italic",colSpan:viewMode==='details'?3: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]'}`,ref:btnSynthRef,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&&(()=>{// Portal ra document.body: dropdown absolute bottom-8 bị clip bởi +if(cur&&(cur.handle||cur.path||cur.file_id||cur.fileId||cur.kind==='midi'&&cur.name)){selectTokenRef.current++;const token=selectTokenRef.current;try{if(window.SonicMidiRouter)window.SonicMidiRouter.panic();else if(window.SonicSF&&typeof window.SonicSF.stopAll==='function')window.SonicSF.stopAll();}catch(e){}stopAllNativeSfNotes();stopAllNativeVstNotes();playMidiPreview(cur,token);}}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||tempo;return/*#__PURE__*/React.createElement("div",{ref:containerRef,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 relative"},/*#__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",{ref:treePaneRef,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 font-bold ${folder==='computer'&&computerPath==='favorited'?'bg-slate-300 text-slate-900':'hover:bg-slate-200 text-slate-800'}`,onClick:()=>{openFavorited();setFavoritedExpanded(!favoritedExpanded);}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-star text-amber-500 text-[11px] font-bold"})," Favorited"),favoritedExpanded&&/*#__PURE__*/React.createElement("div",{className:"pl-3 space-y-0.5"},favorites.map((fav,fi)=>/*#__PURE__*/React.createElement("div",{key:fav.path+fi,"data-tree-path":fav.path,className:"flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm hover:bg-amber-100 text-slate-800",style:{paddingLeft:20},onClick:()=>openFavorite(fav),onContextMenu:e=>{e.preventDefault();e.stopPropagation();setFavContext(Object.assign({},fav,{x:e.clientX,y:e.clientY}));}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder text-[#d9a752] shrink-0"}),/*#__PURE__*/React.createElement("span",{className:"truncate flex-1"},fav.name),/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-star text-amber-500 text-[9px] shrink-0"}))),favorites.length===0&&/*#__PURE__*/React.createElement("div",{className:"pl-4 py-0.5 text-slate-400 italic text-[11px]"},"No favorites")),/*#__PURE__*/React.createElement("div",{className:`flex items-center gap-1 px-1 py-0.5 cursor-pointer rounded-sm ${folder==='computer'&&computerPath!=='favorited'?'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'&&computerPath!=='favorited'&&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:"w-1.5 cursor-ew-resize hover:bg-blue-500/40 active:bg-blue-500/60 transition-colors shrink-0",onMouseDown:startTreeResize,title:"K\xE9o \u0111\u1EC3 thay \u0111\u1ED5i chi\u1EC1u r\u1ED9ng"})),favContext&&/*#__PURE__*/React.createElement("div",{className:"fixed z-[100] bg-white border border-[#808080] shadow-lg rounded-sm text-xs font-sans text-slate-800 min-w-[180px]",style:{left:favContext.x,top:favContext.y},onMouseLeave:()=>setFavContext(null)},/*#__PURE__*/React.createElement("div",{className:`px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5 ${isFavorite(favContext)?'text-amber-700':''}`,onClick:()=>{toggleFavorite(favContext);setFavContext(null);}},/*#__PURE__*/React.createElement("i",{className:`fa-solid ${isFavorite(favContext)?'fa-star text-amber-500':'fa-star text-slate-400'} text-[11px]`}),isFavorite(favContext)?'Gỡ khỏi Favorited':'Thêm vào Favorited'),/*#__PURE__*/React.createElement("div",{className:"px-2 py-1 cursor-pointer hover:bg-blue-100 flex items-center gap-1.5",onClick:()=>{if(favContext)browseComputerDir(favContext);setFavContext(null);}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-folder-open text-[#d9a752] text-[11px]"})," M\u1EDF th\u01B0 m\u1EE5c")),/*#__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",style:{tableLayout:'fixed'}},/*#__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] relative",style:viewMode==='details'?{width:colWidths.file}:undefined},/*#__PURE__*/React.createElement("div",{className:"truncate pr-2"},"File"),/*#__PURE__*/React.createElement("div",{className:"absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400",onMouseDown:e=>startColResize('file',e)})),viewMode==='details'&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0] relative",style:{width:colWidths.size}},/*#__PURE__*/React.createElement("div",{className:"truncate pr-2"},"Size"),/*#__PURE__*/React.createElement("div",{className:"absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400",onMouseDown:e=>startColResize('size',e)})),/*#__PURE__*/React.createElement("th",{className:"py-1 px-2 border-r border-[#b0b0b0] relative",style:{width:colWidths.type}},/*#__PURE__*/React.createElement("div",{className:"truncate pr-2"},"Type"),/*#__PURE__*/React.createElement("div",{className:"absolute right-0 top-0 bottom-0 w-1.5 cursor-col-resize hover:bg-slate-400",onMouseDown:e=>startColResize('type',e)}))))),/*#__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,draggable:!f.is_dir,onDragStart:e=>{if(f.is_dir){e.preventDefault();return;}e.dataTransfer.setData('text/plain',f.name||f.original_name||'');e.dataTransfer.effectAllowed='copy';window.__mediaExplorerDragFile=f;},onDragEnd:()=>{window.__mediaExplorerDragFile=null;},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),onContextMenu:e=>{if(f.is_dir){e.preventDefault();e.stopPropagation();setFavContext(Object.assign({},f,{x:e.clientX,y:e.clientY}));}}},/*#__PURE__*/React.createElement("td",{className:"py-1 px-2 truncate"},/*#__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 truncate"},f.size_mb!=null?f.size_mb.toFixed(2)+' MB':isMidi?(f.tpqn||'MIDI')+' TPQN':'-'),viewMode==='details'&&/*#__PURE__*/React.createElement("td",{className:"py-1 px-2 truncate"},f.is_dir?'Folder':isMidi?'MIDI':f.kind==='audio'?'Audio':'File'));}),visibleFiles.length===0&&/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("td",{className:"py-3 px-2 text-slate-400 italic",colSpan:viewMode==='details'?3: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]'}`,ref:btnSynthRef,title:"Ch\u1ECDn instrument \u0111\u1EC3 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&&(()=>{// Portal ra document.body: dropdown absolute bottom-8 bị clip bởi // overflow-hidden của root Media Explorer khi list dài (max-h-72 // trồi lên trên) → header + ô tìm kiếm mất khi mở lần 2+. -const btnRect=btnSynthRef.current?btnSynthRef.current.getBoundingClientRect():null;return ReactDOM.createPortal(/*#__PURE__*/React.createElement("div",{className:"fixed 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",style:{left:btnRect?btnRect.left:0,bottom:btnRect?window.innerHeight-btnRect.top+8:0}},/*#__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 z-20"},/*#__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"}))),/*#__PURE__*/React.createElement("div",{className:"sticky top-[23px] bg-white p-1 border-b border-[#c0c0c0] z-20 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-magnifying-glass text-slate-400 pl-1 text-[10px]"}),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Tìm nhạc cụ...",value:synthFilter,onChange:e=>setSynthFilter(e.target.value),onClick:e=>e.stopPropagation(),className:"w-full px-1 py-0.5 border border-[#c0c0c0] rounded-sm text-xs font-sans focus:outline-none focus:border-blue-500"}),synthFilter&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setSynthFilter('');},className:"text-slate-400 hover:text-slate-700 px-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark text-[10px]"}))),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&&filteredSynthList&&filteredSynthList.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);}))),!synthLoading&&filteredSynthList&&filteredSynthList.length===0&&synthFilter&&/*#__PURE__*/React.createElement("div",{className:"px-2 py-2 text-slate-400 italic"},"Không tìm thấy nhạc cụ trùng khớp")),document.body);})()),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1",title:"Tempo preview MIDI"},/*#__PURE__*/React.createElement("span",{className:"font-mono text-[10px] text-slate-700"},"Tempo:"),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>commitTempo((tempo||120)-1)},"-"),/*#__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",min:"40",max:"300",value:tempoText,onChange:e=>{const raw=e.target.value;setTempoText(raw);const n=parseInt(raw);if(n>=40&&n<=300)commitTempo(n);},onBlur:()=>{const n=parseInt(tempoText);commitTempo(n);},onKeyDown:e=>{if(e.key==='Enter'){e.currentTarget.blur();}},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:()=>commitTempo((tempo||120)+1)},"+"),/*#__PURE__*/React.createElement("span",{className:"text-slate-600 text-[10px]"},"BPM"))),/*#__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 p-0.5"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-full block cursor-pointer",onMouseDown:handleCanvasMouseDown,onMouseMove:handleCanvasMouseMove,onMouseUp:handleCanvasMouseUp,onContextMenu:handleCanvasContextMenu}),/*#__PURE__*/React.createElement("div",{className:"absolute top-1.5 right-1.5 flex gap-1 z-10"},/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-xs font-bold select-none cursor-pointer",title:"Zoom In",onClick:()=>setZoom(prev=>Math.min(10.0,prev*1.25))},"+"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-xs font-bold select-none cursor-pointer",title:"Zoom Out",onClick:()=>setZoom(prev=>Math.max(0.2,prev/1.25))},"-"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-[9px] select-none cursor-pointer",title:"Reset Zoom",onClick:()=>setZoom(1.0)},"1x")),previewCtxMenu&&/*#__PURE__*/React.createElement("div",{className:"fixed bg-[#1e1e24] border border-[#3e3e4a] rounded shadow-md z-[9999] py-1 font-sans text-xs text-slate-300 w-32 cursor-pointer select-none",style:{top:previewCtxMenu.y,left:previewCtxMenu.x},onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"px-3 py-1.5 hover:bg-slate-700 hover:text-white flex items-center gap-2",onClick:()=>{handleCopySelection();setPreviewCtxMenu(null);}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-copy"})," Copy"),/*#__PURE__*/React.createElement("div",{className:"px-3 py-1.5 hover:bg-slate-700 hover:text-white border-t border-[#3e3e4a] flex items-center gap-2",onClick:()=>setPreviewCtxMenu(null)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"})," Cancel"))),/*#__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':'-'),selIsMidi?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Bars: ",midiBars||1),/*#__PURE__*/React.createElement("div",null,"Beats: ",Math.round(midiTotalBeats||16)),/*#__PURE__*/React.createElement("div",null,"BPM: ",midiFileBpm||120),/*#__PURE__*/React.createElement("div",null,"Duration: ",selDur.toFixed(2),"s")):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__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?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"},selIsMidi&&midiNotes&&midiNotes.length?/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1.5 text-slate-900 font-bold"},"Bar ",Math.max(1,Math.floor(currentTime/(4*60/(tempo||120)))+1)," / ",midiBars||1,/*#__PURE__*/React.createElement("span",{className:"text-slate-500 ml-1"},"| ",formatTime(currentTime)," / ",formatTime(selDur))):/*#__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)))));};// ── Virtual MIDI Keyboard component (Phase 5) ────────────────────────────── +const btnRect=btnSynthRef.current?btnSynthRef.current.getBoundingClientRect():null;return ReactDOM.createPortal(/*#__PURE__*/React.createElement("div",{className:"fixed 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",style:{left:btnRect?btnRect.left:0,bottom:btnRect?window.innerHeight-btnRect.top+8:0}},/*#__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 z-20"},/*#__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"}))),/*#__PURE__*/React.createElement("div",{className:"sticky top-[23px] bg-white p-1 border-b border-[#c0c0c0] z-20 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-magnifying-glass text-slate-400 pl-1 text-[10px]"}),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\xECm nh\u1EA1c c\u1EE5...",value:synthFilter,onChange:e=>setSynthFilter(e.target.value),onClick:e=>e.stopPropagation(),className:"w-full px-1 py-0.5 border border-[#c0c0c0] rounded-sm text-xs font-sans focus:outline-none focus:border-blue-500"}),synthFilter&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();setSynthFilter('');},className:"text-slate-400 hover:text-slate-700 px-1"},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark text-[10px]"}))),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\xF4ng c\xF3 SoundFont n\xE0o"),/*#__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\u1EB7c \u0111\u1ECBnh)"),!synthLoading&&filteredSynthList&&filteredSynthList.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);}))),!synthLoading&&filteredSynthList&&filteredSynthList.length===0&&synthFilter&&/*#__PURE__*/React.createElement("div",{className:"px-2 py-2 text-slate-400 italic"},"Kh\xF4ng t\xECm th\u1EA5y nh\u1EA1c c\u1EE5 tr\xF9ng kh\u1EDBp")),document.body);})()),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1",title:"Tempo preview MIDI"},/*#__PURE__*/React.createElement("span",{className:"font-mono text-[10px] text-slate-700"},"Tempo:"),/*#__PURE__*/React.createElement("button",{className:"px-1 bg-[#e0e0e0] hover:bg-white border border-[#707070] rounded-sm text-[10px]",onClick:()=>commitTempo((tempo||120)-1)},"-"),/*#__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",min:"40",max:"300",value:tempoText,onChange:e=>{const raw=e.target.value;setTempoText(raw);const n=parseInt(raw);if(n>=40&&n<=300)commitTempo(n);},onBlur:()=>{const n=parseInt(tempoText);commitTempo(n);},onKeyDown:e=>{if(e.key==='Enter'){e.currentTarget.blur();}},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:()=>commitTempo((tempo||120)+1)},"+"),/*#__PURE__*/React.createElement("span",{className:"text-slate-600 text-[10px]"},"BPM"))),/*#__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 p-0.5"},/*#__PURE__*/React.createElement("canvas",{ref:canvasRef,className:"w-full h-full block cursor-pointer",onMouseDown:handleCanvasMouseDown,onMouseMove:handleCanvasMouseMove,onMouseUp:handleCanvasMouseUp,onContextMenu:handleCanvasContextMenu}),/*#__PURE__*/React.createElement("div",{className:"absolute top-1.5 right-1.5 flex gap-1 z-10"},/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-xs font-bold select-none cursor-pointer",title:"Zoom In",onClick:()=>setZoom(prev=>Math.min(10.0,prev*1.25))},"+"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-xs font-bold select-none cursor-pointer",title:"Zoom Out",onClick:()=>setZoom(prev=>Math.max(0.2,prev/1.25))},"-"),/*#__PURE__*/React.createElement("button",{className:"w-5 h-5 bg-[#2a2a2a] hover:bg-slate-700 border border-[#444] rounded flex items-center justify-center text-white text-[9px] select-none cursor-pointer",title:"Reset Zoom",onClick:()=>setZoom(1.0)},"1x")),previewCtxMenu&&/*#__PURE__*/React.createElement("div",{className:"fixed bg-[#1e1e24] border border-[#3e3e4a] rounded shadow-md z-[9999] py-1 font-sans text-xs text-slate-300 w-32 cursor-pointer select-none",style:{top:previewCtxMenu.y,left:previewCtxMenu.x},onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"px-3 py-1.5 hover:bg-slate-700 hover:text-white flex items-center gap-2",onClick:()=>{handleCopySelection();setPreviewCtxMenu(null);}},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-copy"})," Copy"),/*#__PURE__*/React.createElement("div",{className:"px-3 py-1.5 hover:bg-slate-700 hover:text-white border-t border-[#3e3e4a] flex items-center gap-2",onClick:()=>setPreviewCtxMenu(null)},/*#__PURE__*/React.createElement("i",{className:"fa-solid fa-xmark"})," Cancel"))),/*#__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':'-'),selIsMidi?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",null,"Bars: ",midiBars||1),/*#__PURE__*/React.createElement("div",null,"Beats: ",Math.round(midiTotalBeats||16)),/*#__PURE__*/React.createElement("div",null,"BPM: ",midiFileBpm||120),/*#__PURE__*/React.createElement("div",null,"Duration: ",selDur.toFixed(2),"s")):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__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?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"},selIsMidi&&midiNotes&&midiNotes.length?/*#__PURE__*/React.createElement("div",{className:"bg-white border border-[#808080] px-1.5 text-slate-900 font-bold"},"Bar ",Math.max(1,Math.floor(currentTime/(4*60/(tempo||120)))+1)," / ",midiBars||1,/*#__PURE__*/React.createElement("span",{className:"text-slate-500 ml-1"},"| ",formatTime(currentTime)," / ",formatTime(selDur))):/*#__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)))));};// ── Virtual MIDI Keyboard component (Phase 5) ────────────────────────────── // Floating draggable QWERTY keyboard — F2 / menu Tools. Không nuốt Space/R // (handler toàn cục chỉ bắt phím trong VKB_MAP). Persist localStorage. const VKB_WHITE_OFFSETS=[0,2,4,5,7,9,11,12,14,16,17,19,21,23,24];const VKB_BLACK_OFFSETS=[1,3,6,8,10,13,15,18,20,22];const VKB_KEY_LABELS=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const VKB_SCALES={'None':null,'Major':[0,2,4,5,7,9,11],'Minor':[0,2,3,5,7,8,10],'Pentatonic Major':[0,2,4,7,9],'Pentatonic Minor':[0,3,5,7,10],'Dorian':[0,2,3,5,7,9,10],'Mixolydian':[0,2,4,5,7,9,10]};const VirtualMidiKeyboard=({settings,onChange,tracks,activePitches,onNoteOn,onNoteOff,onClose})=>{const KEY_W=24;const KEY_H=84;const BLACK_W=14;const BLACK_H=54;const octave=Math.max(0,Math.min(7,parseInt(settings.octave)||4));const basePitch=(octave+1)*12;const scale=VKB_SCALES[settings.scale]||null;const inScale=pitch=>{if(!scale)return false;const pc=(pitch%12+12)%12;return scale.indexOf(pc)!==-1;};const velNum=Math.max(1,Math.min(127,parseInt(settings.velocity)||100));// Channel hiển thị: user đã chọn (1..16) hoặc mặc định = channel track arm/select đầu tiên let displayChannel=parseInt(settings.channel)||null;if(displayChannel===null){try{const trks=tracks||[];if(trks.length){const t=trks.find(x=>x.isArmed)||trks[0];displayChannel=assignTrackMidiChannel(t,trks)+1;}}catch(e){}if(displayChannel===null)displayChannel=1;}const startDrag=e=>{e.preventDefault();const sx=e.clientX,sy=e.clientY;const ox=settings.x,oy=settings.y;const onMove=ev=>{onChange({x:Math.max(-280,Math.min(window.innerWidth-80,ox+(ev.clientX-sx))),y:Math.max(0,Math.min(window.innerHeight-36,oy+(ev.clientY-sy)))});};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const fireOn=(e,pitch)=>{e.preventDefault();e.stopPropagation();onNoteOn(pitch,velNum/127);};const whiteKeys=VKB_WHITE_OFFSETS.map(function(off,wi){const pitch=Math.min(127,basePitch+off);const active=activePitches&&activePitches.has(pitch);const sc=inScale(pitch);return/*#__PURE__*/React.createElement("div",{key:'vkbw'+off,onMouseDown:e=>fireOn(e,pitch),onMouseUp:()=>onNoteOff(pitch),onMouseLeave:()=>onNoteOff(pitch),style:{position:'absolute',left:wi*KEY_W,top:0,width:KEY_W,height:KEY_H,boxSizing:'border-box'},className:"flex items-end justify-center pb-1 text-[9px] font-bold cursor-pointer rounded-b transition "+(active?'bg-emerald-500 text-white':'bg-zinc-100 text-zinc-600 hover:bg-amber-100')+(sc?' ring-1 ring-inset ring-amber-400':'')},off%12===0?VKB_KEY_LABELS[0]+(Math.floor(pitch/12)-1):'');});const blackKeys=VKB_BLACK_OFFSETS.map(function(off){const pitch=Math.min(127,basePitch+off);const active=activePitches&&activePitches.has(pitch);const sc=inScale(pitch);const wi=VKB_WHITE_OFFSETS.indexOf(off-1);return/*#__PURE__*/React.createElement("div",{key:'vkbb'+off,onMouseDown:e=>fireOn(e,pitch),onMouseUp:()=>onNoteOff(pitch),onMouseLeave:()=>onNoteOff(pitch),style:{position:'absolute',left:(wi+1)*KEY_W-BLACK_W/2,top:0,width:BLACK_W,height:BLACK_H,boxSizing:'border-box',zIndex:5},className:"cursor-pointer rounded-b transition "+(active?'bg-emerald-600':'bg-zinc-950 hover:bg-zinc-800')+(sc?' ring-1 ring-inset ring-amber-400':'')});});return/*#__PURE__*/React.createElement("div",{style:{position:'fixed',left:settings.x,top:settings.y,zIndex:9990,width:15*KEY_W+12},className:"bg-[#1c1c1e] border border-zinc-700 rounded-xl shadow-2xl select-none"},/*#__PURE__*/React.createElement("div",{onMouseDown:startDrag,className:"flex items-center justify-between px-3 py-1.5 cursor-move bg-zinc-800 rounded-t-xl border-b border-zinc-700"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-zinc-300 tracking-wide"},"VIRTUAL MIDI KEYBOARD"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},"F2"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();onClose();},className:"text-zinc-500 hover:text-red-400 text-xs px-1"},"\u2715"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 px-2 py-1.5 border-b border-zinc-800 text-[10px] text-zinc-400 flex-wrap"},/*#__PURE__*/React.createElement("button",{onClick:()=>onChange({octave:Math.max(0,octave-1)}),className:"px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 rounded"},"Oct -"),/*#__PURE__*/React.createElement("span",{className:"font-mono text-amber-400"},"Octave: "+octave),/*#__PURE__*/React.createElement("button",{onClick:()=>onChange({octave:Math.min(7,octave+1)}),className:"px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 rounded"},"Oct +"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Vel",/*#__PURE__*/React.createElement("input",{type:"range",min:1,max:127,value:velNum,onChange:e=>onChange({velocity:parseInt(e.target.value)||100}),className:"w-16"}),/*#__PURE__*/React.createElement("span",{className:"font-mono w-7 text-right"},velNum)),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Ch",/*#__PURE__*/React.createElement("select",{value:displayChannel,onChange:e=>onChange({channel:parseInt(e.target.value)}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5"},Array.from({length:16},function(_,i){return/*#__PURE__*/React.createElement("option",{key:i+1,value:i+1},i+1);}))),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Tr",/*#__PURE__*/React.createElement("input",{type:"number",min:-24,max:24,value:settings.transpose,onChange:e=>onChange({transpose:parseInt(e.target.value)||0}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5 w-10 font-mono"})),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Scale",/*#__PURE__*/React.createElement("select",{value:settings.scale,onChange:e=>onChange({scale:e.target.value}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5"},Object.keys(VKB_SCALES).map(function(s){return/*#__PURE__*/React.createElement("option",{key:s,value:s},s);})))),/*#__PURE__*/React.createElement("div",{className:"relative mx-1 my-2",style:{width:15*KEY_W,height:KEY_H}},whiteKeys,blackKeys),/*#__PURE__*/React.createElement("div",{className:"px-2 pb-1.5 text-[9px] text-zinc-600"},"QWERTY: Z S X D C V G B H N J M / Q 2 W 3 E R 5 T 6 Y 7 U I"));};// ── Chords Panel (Phase 6) ───────────────────────────────────────────── // Floating chord progression panel — Insert menu / Shift+K / piano-roll // toolbar. Presets theo virtual_keyboard_and_chords_panel_spec.md §III.2. -const CHORD_PRESETS=[{id:'pop',label:'Pop',genre:'Pop',tokens:'I-V-vi-IV',key:'C',voicing:'root',rhythm:'whole',hint:'C → G → Am → F'},{id:'jazz',label:'Jazz Neo-Soul',genre:'Jazz Neo-Soul',tokens:'ii7-V7-Imaj7-VI7',key:'C',voicing:'drop2',rhythm:'whole',hint:'Dm7 → G7 → Cmaj7 → A7'},{id:'cinematic',label:'Cinematic',genre:'Cinematic',tokens:'i-VI-III-VII',key:'C',voicing:'root',rhythm:'whole',hint:'Cm → Ab → Eb → Bb'},{id:'edm',label:'EDM',genre:'EDM',tokens:'vi-IV-I-V',key:'C',voicing:'first',rhythm:'quarter',hint:'Am → F → C → G'},{id:'lofi',label:'Lo-Fi',genre:'Lo-Fi',tokens:'i7-iv7-v7-i7',key:'C',voicing:'root',rhythm:'whole',hint:'Cm7 → Fm7 → Gm7 → Cm7'},{id:'rnb',label:'R&B',genre:'R&B',tokens:'Imaj7-iii7-IVmaj7-V13',key:'C',voicing:'root',rhythm:'whole',hint:'Cmaj7 → Em7 → Fmaj7 → G13'}];const CHORD_RHYTHM_BEATS={whole:4,quarter:1,arpeggio:1,strum:1};const CHORD_RHYTHM_LABELS={whole:'Whole (4 beats)',quarter:'Quarter (1 beat)',arpeggio:'Arpeggio',strum:'Strum'};const CHORD_KEYS=['C','C#','D','Eb','E','F','F#','G','Ab','A','Bb','B'];const CHORD_VOICING_LABELS={root:'Root',first:'1st Inv',drop2:'Drop 2'};const ChordsPanel=({onClose,onInsert,onNoteOn,onNoteOff,bpm})=>{const[tab,setTab]=useState('preset');const[search,setSearch]=useState('');const[key,setKey]=useState('C');const[favorites,setFavorites]=useState(function(){try{return JSON.parse(localStorage.getItem('daw_chord_favorites')||'[]');}catch(e){return[];}});const[customList,setCustomList]=useState(function(){try{return JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]');}catch(e){return[];}});const[cName,setCName]=useState('');const[cGenre,setCGenre]=useState('');const[cTokens,setCTokens]=useState('I-vi-IV-V');const[cRhythm,setCRhythm]=useState('whole');const[cVoicing,setCVoicing]=useState('root');const[customKey,setCustomKey]=useState('C');const previewTimerRef=useRef(null);useEffect(function(){const onSaved=function(){try{setCustomList(JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]'));}catch(e){}};window.addEventListener('CUSTOM_CHORD_SAVED',onSaved);return function(){window.removeEventListener('CUSTOM_CHORD_SAVED',onSaved);};},[]);useEffect(function(){try{localStorage.setItem('daw_chord_favorites',JSON.stringify(favorites));}catch(e){}},[favorites]);useEffect(function(){return function(){if(previewTimerRef.current)clearTimeout(previewTimerRef.current);};},[]);const resolveProgression=function(tokens,progKey,progVoicing){try{return(window.ChordTheory||ChordTheory).progressionToChords(tokens,progKey||key,progVoicing||'root');}catch(e){return[];}};const previewChord=function(chord){if(!chord||!chord.pitches||!chord.pitches.length)return;const vel=0.8;chord.pitches.forEach(function(p){try{onNoteOn(p,vel);}catch(e){}});if(previewTimerRef.current)clearTimeout(previewTimerRef.current);previewTimerRef.current=setTimeout(function(){chord.pitches.forEach(function(p){try{onNoteOff(p);}catch(e){}});},900);};const toggleFavorite=function(id){setFavorites(function(prev){return prev.indexOf(id)!==-1?prev.filter(function(x){return x!==id;}):prev.concat([id]);});};const insertProg=function(label,chords,rhythm){const dur=CHORD_RHYTHM_BEATS[rhythm]!=null?CHORD_RHYTHM_BEATS[rhythm]:4;const payload=(chords||[]).filter(function(c){return c.pitches&&c.pitches.length;}).map(function(c){return{name:c.name||c.token,pitches:c.pitches,duration_beats:dur};});if(!payload.length){if(window.showToast)window.showToast('Không có chord để chèn','warning');return;}onInsert({label:label,chords:payload});};const saveCustom=function(){const tokens=(cTokens||'').trim();if(!tokens){if(window.showToast)window.showToast('Nhập progression tokens','warning');return;}let chords=[];try{chords=(window.ChordTheory||ChordTheory).progressionToChords(tokens,customKey,cVoicing);}catch(e){}if(!chords.length||chords.some(function(c){return!c.pitches||!c.pitches.length;})){if(window.showToast)window.showToast('Progression không parse được','warning');return;}const entry={id:'custom_'+Date.now(),name:(cName||'').trim()||'My Chord',genre:(cGenre||'').trim()||'Custom',tokens:tokens,key:customKey,rhythm:cRhythm,voicing:cVoicing,chords:chords};try{const list=JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]');list.push(entry);localStorage.setItem('daw_user_custom_chords',JSON.stringify(list));setCustomList(list);window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'));if(window.showToast)window.showToast('Đã lưu chord custom: '+entry.name,'success');}catch(e){if(window.showToast)window.showToast('Lưu thất bại','error');}};const deleteCustom=function(id){try{const list=JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]').filter(function(x){return x.id!==id;});localStorage.setItem('daw_user_custom_chords',JSON.stringify(list));setCustomList(list);window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'));}catch(e){}};const q=(search||'').trim().toLowerCase();const filteredPresets=CHORD_PRESETS.filter(function(p){if(!q)return true;const chords=resolveProgression(p.tokens,key,p.voicing);const hay=(p.label+' '+p.genre+' '+p.hint+' '+chords.map(function(c){return c.name;}).join(' ')).toLowerCase();return hay.indexOf(q)!==-1;});return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9995] flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-3xl p-5 text-slate-200 max-h-[85vh] overflow-y-auto",onClick:function(e){e.stopPropagation();}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between pb-3 border-b border-[#383838] mb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Chords Panel"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-500 hover:text-red-400 text-xs px-1",title:"Close"},"✕")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 flex-wrap"},/*#__PURE__*/React.createElement("div",{className:"flex gap-1"},[['preset','Presets'],['custom','Custom'],['ai','AI']].map(function(t){return/*#__PURE__*/React.createElement("button",{key:t[0],onClick:function(){setTab(t[0]);},className:"px-3 py-1 rounded text-[11px] font-bold "+(tab===t[0]?'bg-amber-700 text-white':'bg-zinc-800 text-zinc-400 hover:bg-zinc-700')},t[1]);})),tab!=='ai'&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("select",{value:key,onChange:function(e){setKey(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none",title:"Key (transpose)"},CHORD_KEYS.map(function(k){return/*#__PURE__*/React.createElement("option",{key:k,value:k},k);})),tab==='preset'&&/*#__PURE__*/React.createElement("input",{value:search,onChange:function(e){setSearch(e.target.value);},placeholder:"Search preset...",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-[11px] outline-none focus:border-amber-500 flex-1 min-w-[120px]"}))),tab==='preset'&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-3"},filteredPresets.map(function(p){const chords=resolveProgression(p.tokens,key,p.voicing);const display=chords.map(function(c){return c.name;}).join(' → ');const fav=favorites.indexOf(p.id)!==-1;return/*#__PURE__*/React.createElement("div",{key:p.id,className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-amber-400"},p.label),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},p.genre," · ",CHORD_VOICING_LABELS[p.voicing])),/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-mono text-zinc-300 mb-2"},display||p.hint),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 flex-wrap"},/*#__PURE__*/React.createElement("button",{onClick:function(){previewChord(chords[0]);},disabled:!chords.length,className:"px-2 py-1 rounded text-[10px] font-semibold bg-emerald-800 hover:bg-emerald-700 text-emerald-100 disabled:opacity-40"},"Preview Sound"),/*#__PURE__*/React.createElement("button",{onClick:function(){insertProg(p.label,chords,p.rhythm);},disabled:!chords.length,className:"px-2 py-1 rounded text-[10px] font-semibold bg-blue-800 hover:bg-blue-700 text-blue-100 disabled:opacity-40"},"Insert to Timeline"),/*#__PURE__*/React.createElement("button",{onClick:function(){toggleFavorite(p.id);},className:"px-2 py-1 rounded text-[10px] font-semibold "+(fav?'bg-yellow-700 text-yellow-100':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},fav?'★ Favorite':'☆ Favorite')));}),!filteredPresets.length&&/*#__PURE__*/React.createElement("div",{className:"col-span-full text-[11px] text-zinc-500"},"Không tìm thấy preset.")),tab==='custom'&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-amber-400 mb-2"},"Build Custom Progression"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2"},/*#__PURE__*/React.createElement("input",{value:cName,onChange:function(e){setCName(e.target.value);},placeholder:"Tên (vd: Ballad)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("input",{value:cGenre,onChange:function(e){setCGenre(e.target.value);},placeholder:"Genre (vd: Ballad)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("input",{value:cTokens,onChange:function(e){setCTokens(e.target.value);},placeholder:"Tokens (vd: I-vi-IV-V)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-2 flex-wrap"},/*#__PURE__*/React.createElement("select",{value:customKey,onChange:function(e){setCustomKey(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},CHORD_KEYS.map(function(k){return/*#__PURE__*/React.createElement("option",{key:k,value:k},k);})),/*#__PURE__*/React.createElement("select",{value:cRhythm,onChange:function(e){setCRhythm(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},Object.keys(CHORD_RHYTHM_BEATS).map(function(r){return/*#__PURE__*/React.createElement("option",{key:r,value:r},CHORD_RHYTHM_LABELS[r]);})),/*#__PURE__*/React.createElement("select",{value:cVoicing,onChange:function(e){setCVoicing(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},Object.keys(CHORD_VOICING_LABELS).map(function(v){return/*#__PURE__*/React.createElement("option",{key:v,value:v},CHORD_VOICING_LABELS[v]);}))),/*#__PURE__*/React.createElement("button",{onClick:saveCustom,className:"px-3 py-1.5 rounded text-xs font-bold bg-amber-700 hover:bg-amber-600 text-white"},"Save Custom"))),/*#__PURE__*/React.createElement("div",{className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-amber-400 mb-2"},"My Customs (",customList.length,")"),customList.length===0?/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500"},"Chưa có custom nào — tạo bên trái rồi Save."):/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2 max-h-48 overflow-y-auto"},customList.map(function(c){return/*#__PURE__*/React.createElement("div",{key:c.id,className:"border border-zinc-700 rounded p-2 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-zinc-200"},c.name," ",/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},"(",c.genre,")")),/*#__PURE__*/React.createElement("div",{className:"text-[10px] font-mono text-zinc-400 truncate"},c.chords.map(function(x){return x.name;}).join(' → '))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:function(){previewChord(c.chords[0]);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-emerald-800 hover:bg-emerald-700 text-emerald-100"},"Preview"),/*#__PURE__*/React.createElement("button",{onClick:function(){insertProg(c.name,c.chords,c.rhythm);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-blue-800 hover:bg-blue-700 text-blue-100"},"Insert"),/*#__PURE__*/React.createElement("button",{onClick:function(){deleteCustom(c.id);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-red-900 hover:bg-red-800 text-red-100"},"Del")));})))),tab==='ai'&&/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center justify-center py-12 text-center"},/*#__PURE__*/React.createElement("div",{className:"text-3xl mb-2"},"♪"),/*#__PURE__*/React.createElement("div",{className:"text-sm text-zinc-300 font-semibold mb-1"},"AI Chord Suggestions"),/*#__PURE__*/React.createElement("div",{className:"text-[11px] text-zinc-500 max-w-sm"},"AI chưa cấu hình. Bật AI Gateway trong Settings để nhận gợi ý chord theo genre / cảm xúc."))));};const App=()=>{// ── State Definitions ── +const CHORD_PRESETS=[{id:'pop',label:'Pop',genre:'Pop',tokens:'I-V-vi-IV',key:'C',voicing:'root',rhythm:'whole',hint:'C → G → Am → F'},{id:'jazz',label:'Jazz Neo-Soul',genre:'Jazz Neo-Soul',tokens:'ii7-V7-Imaj7-VI7',key:'C',voicing:'drop2',rhythm:'whole',hint:'Dm7 → G7 → Cmaj7 → A7'},{id:'cinematic',label:'Cinematic',genre:'Cinematic',tokens:'i-VI-III-VII',key:'C',voicing:'root',rhythm:'whole',hint:'Cm → Ab → Eb → Bb'},{id:'edm',label:'EDM',genre:'EDM',tokens:'vi-IV-I-V',key:'C',voicing:'first',rhythm:'quarter',hint:'Am → F → C → G'},{id:'lofi',label:'Lo-Fi',genre:'Lo-Fi',tokens:'i7-iv7-v7-i7',key:'C',voicing:'root',rhythm:'whole',hint:'Cm7 → Fm7 → Gm7 → Cm7'},{id:'rnb',label:'R&B',genre:'R&B',tokens:'Imaj7-iii7-IVmaj7-V13',key:'C',voicing:'root',rhythm:'whole',hint:'Cmaj7 → Em7 → Fmaj7 → G13'}];const CHORD_RHYTHM_BEATS={whole:4,quarter:1,arpeggio:1,strum:1};const CHORD_RHYTHM_LABELS={whole:'Whole (4 beats)',quarter:'Quarter (1 beat)',arpeggio:'Arpeggio',strum:'Strum'};const CHORD_KEYS=['C','C#','D','Eb','E','F','F#','G','Ab','A','Bb','B'];const CHORD_VOICING_LABELS={root:'Root',first:'1st Inv',drop2:'Drop 2'};const ChordsPanel=({onClose,onInsert,onNoteOn,onNoteOff,bpm})=>{const[tab,setTab]=useState('preset');const[search,setSearch]=useState('');const[key,setKey]=useState('C');const[favorites,setFavorites]=useState(function(){try{return JSON.parse(localStorage.getItem('daw_chord_favorites')||'[]');}catch(e){return[];}});const[customList,setCustomList]=useState(function(){try{return JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]');}catch(e){return[];}});const[cName,setCName]=useState('');const[cGenre,setCGenre]=useState('');const[cTokens,setCTokens]=useState('I-vi-IV-V');const[cRhythm,setCRhythm]=useState('whole');const[cVoicing,setCVoicing]=useState('root');const[customKey,setCustomKey]=useState('C');const previewTimerRef=useRef(null);useEffect(function(){const onSaved=function(){try{setCustomList(JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]'));}catch(e){}};window.addEventListener('CUSTOM_CHORD_SAVED',onSaved);return function(){window.removeEventListener('CUSTOM_CHORD_SAVED',onSaved);};},[]);useEffect(function(){try{localStorage.setItem('daw_chord_favorites',JSON.stringify(favorites));}catch(e){}},[favorites]);useEffect(function(){return function(){if(previewTimerRef.current)clearTimeout(previewTimerRef.current);};},[]);const resolveProgression=function(tokens,progKey,progVoicing){try{return(window.ChordTheory||ChordTheory).progressionToChords(tokens,progKey||key,progVoicing||'root');}catch(e){return[];}};const previewChord=function(chord){if(!chord||!chord.pitches||!chord.pitches.length)return;const vel=0.8;chord.pitches.forEach(function(p){try{onNoteOn(p,vel);}catch(e){}});if(previewTimerRef.current)clearTimeout(previewTimerRef.current);previewTimerRef.current=setTimeout(function(){chord.pitches.forEach(function(p){try{onNoteOff(p);}catch(e){}});},900);};const toggleFavorite=function(id){setFavorites(function(prev){return prev.indexOf(id)!==-1?prev.filter(function(x){return x!==id;}):prev.concat([id]);});};const insertProg=function(label,chords,rhythm){const dur=CHORD_RHYTHM_BEATS[rhythm]!=null?CHORD_RHYTHM_BEATS[rhythm]:4;const payload=(chords||[]).filter(function(c){return c.pitches&&c.pitches.length;}).map(function(c){return{name:c.name||c.token,pitches:c.pitches,duration_beats:dur};});if(!payload.length){if(window.showToast)window.showToast('Không có chord để chèn','warning');return;}onInsert({label:label,chords:payload});};const saveCustom=function(){const tokens=(cTokens||'').trim();if(!tokens){if(window.showToast)window.showToast('Nhập progression tokens','warning');return;}let chords=[];try{chords=(window.ChordTheory||ChordTheory).progressionToChords(tokens,customKey,cVoicing);}catch(e){}if(!chords.length||chords.some(function(c){return!c.pitches||!c.pitches.length;})){if(window.showToast)window.showToast('Progression không parse được','warning');return;}const entry={id:'custom_'+Date.now(),name:(cName||'').trim()||'My Chord',genre:(cGenre||'').trim()||'Custom',tokens:tokens,key:customKey,rhythm:cRhythm,voicing:cVoicing,chords:chords};try{const list=JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]');list.push(entry);localStorage.setItem('daw_user_custom_chords',JSON.stringify(list));setCustomList(list);window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'));if(window.showToast)window.showToast('Đã lưu chord custom: '+entry.name,'success');}catch(e){if(window.showToast)window.showToast('Lưu thất bại','error');}};const deleteCustom=function(id){try{const list=JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]').filter(function(x){return x.id!==id;});localStorage.setItem('daw_user_custom_chords',JSON.stringify(list));setCustomList(list);window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'));}catch(e){}};const q=(search||'').trim().toLowerCase();const filteredPresets=CHORD_PRESETS.filter(function(p){if(!q)return true;const chords=resolveProgression(p.tokens,key,p.voicing);const hay=(p.label+' '+p.genre+' '+p.hint+' '+chords.map(function(c){return c.name;}).join(' ')).toLowerCase();return hay.indexOf(q)!==-1;});return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9995] flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-3xl p-5 text-slate-200 max-h-[85vh] overflow-y-auto",onClick:function(e){e.stopPropagation();}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between pb-3 border-b border-[#383838] mb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Chords Panel"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-500 hover:text-red-400 text-xs px-1",title:"Close"},"\u2715")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 flex-wrap"},/*#__PURE__*/React.createElement("div",{className:"flex gap-1"},[['preset','Presets'],['custom','Custom'],['ai','AI']].map(function(t){return/*#__PURE__*/React.createElement("button",{key:t[0],onClick:function(){setTab(t[0]);},className:"px-3 py-1 rounded text-[11px] font-bold "+(tab===t[0]?'bg-amber-700 text-white':'bg-zinc-800 text-zinc-400 hover:bg-zinc-700')},t[1]);})),tab!=='ai'&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("select",{value:key,onChange:function(e){setKey(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none",title:"Key (transpose)"},CHORD_KEYS.map(function(k){return/*#__PURE__*/React.createElement("option",{key:k,value:k},k);})),tab==='preset'&&/*#__PURE__*/React.createElement("input",{value:search,onChange:function(e){setSearch(e.target.value);},placeholder:"Search preset...",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-[11px] outline-none focus:border-amber-500 flex-1 min-w-[120px]"}))),tab==='preset'&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-3"},filteredPresets.map(function(p){const chords=resolveProgression(p.tokens,key,p.voicing);const display=chords.map(function(c){return c.name;}).join(' → ');const fav=favorites.indexOf(p.id)!==-1;return/*#__PURE__*/React.createElement("div",{key:p.id,className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-amber-400"},p.label),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},p.genre," \xB7 ",CHORD_VOICING_LABELS[p.voicing])),/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-mono text-zinc-300 mb-2"},display||p.hint),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 flex-wrap"},/*#__PURE__*/React.createElement("button",{onClick:function(){previewChord(chords[0]);},disabled:!chords.length,className:"px-2 py-1 rounded text-[10px] font-semibold bg-emerald-800 hover:bg-emerald-700 text-emerald-100 disabled:opacity-40"},"Preview Sound"),/*#__PURE__*/React.createElement("button",{onClick:function(){insertProg(p.label,chords,p.rhythm);},disabled:!chords.length,className:"px-2 py-1 rounded text-[10px] font-semibold bg-blue-800 hover:bg-blue-700 text-blue-100 disabled:opacity-40"},"Insert to Timeline"),/*#__PURE__*/React.createElement("button",{onClick:function(){toggleFavorite(p.id);},className:"px-2 py-1 rounded text-[10px] font-semibold "+(fav?'bg-yellow-700 text-yellow-100':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},fav?'★ Favorite':'☆ Favorite')));}),!filteredPresets.length&&/*#__PURE__*/React.createElement("div",{className:"col-span-full text-[11px] text-zinc-500"},"Kh\xF4ng t\xECm th\u1EA5y preset.")),tab==='custom'&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-amber-400 mb-2"},"Build Custom Progression"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2"},/*#__PURE__*/React.createElement("input",{value:cName,onChange:function(e){setCName(e.target.value);},placeholder:"T\xEAn (vd: Ballad)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("input",{value:cGenre,onChange:function(e){setCGenre(e.target.value);},placeholder:"Genre (vd: Ballad)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("input",{value:cTokens,onChange:function(e){setCTokens(e.target.value);},placeholder:"Tokens (vd: I-vi-IV-V)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-2 flex-wrap"},/*#__PURE__*/React.createElement("select",{value:customKey,onChange:function(e){setCustomKey(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},CHORD_KEYS.map(function(k){return/*#__PURE__*/React.createElement("option",{key:k,value:k},k);})),/*#__PURE__*/React.createElement("select",{value:cRhythm,onChange:function(e){setCRhythm(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},Object.keys(CHORD_RHYTHM_BEATS).map(function(r){return/*#__PURE__*/React.createElement("option",{key:r,value:r},CHORD_RHYTHM_LABELS[r]);})),/*#__PURE__*/React.createElement("select",{value:cVoicing,onChange:function(e){setCVoicing(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},Object.keys(CHORD_VOICING_LABELS).map(function(v){return/*#__PURE__*/React.createElement("option",{key:v,value:v},CHORD_VOICING_LABELS[v]);}))),/*#__PURE__*/React.createElement("button",{onClick:saveCustom,className:"px-3 py-1.5 rounded text-xs font-bold bg-amber-700 hover:bg-amber-600 text-white"},"Save Custom"))),/*#__PURE__*/React.createElement("div",{className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-amber-400 mb-2"},"My Customs (",customList.length,")"),customList.length===0?/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500"},"Ch\u01B0a c\xF3 custom n\xE0o \u2014 t\u1EA1o b\xEAn tr\xE1i r\u1ED3i Save."):/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2 max-h-48 overflow-y-auto"},customList.map(function(c){return/*#__PURE__*/React.createElement("div",{key:c.id,className:"border border-zinc-700 rounded p-2 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-zinc-200"},c.name," ",/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},"(",c.genre,")")),/*#__PURE__*/React.createElement("div",{className:"text-[10px] font-mono text-zinc-400 truncate"},c.chords.map(function(x){return x.name;}).join(' → '))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:function(){previewChord(c.chords[0]);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-emerald-800 hover:bg-emerald-700 text-emerald-100"},"Preview"),/*#__PURE__*/React.createElement("button",{onClick:function(){insertProg(c.name,c.chords,c.rhythm);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-blue-800 hover:bg-blue-700 text-blue-100"},"Insert"),/*#__PURE__*/React.createElement("button",{onClick:function(){deleteCustom(c.id);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-red-900 hover:bg-red-800 text-red-100"},"Del")));})))),tab==='ai'&&/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center justify-center py-12 text-center"},/*#__PURE__*/React.createElement("div",{className:"text-3xl mb-2"},"\u266A"),/*#__PURE__*/React.createElement("div",{className:"text-sm text-zinc-300 font-semibold mb-1"},"AI Chord Suggestions"),/*#__PURE__*/React.createElement("div",{className:"text-[11px] text-zinc-500 max-w-sm"},"AI ch\u01B0a c\u1EA5u h\xECnh. B\u1EADt AI Gateway trong Settings \u0111\u1EC3 nh\u1EADn g\u1EE3i \xFD chord theo genre / c\u1EA3m x\xFAc."))));};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 @@ -1478,7 +1506,21 @@ useEffect(()=>{setTimeout(()=>{if(window.lucide){window.lucide.createIcons();}}, const startColResize=e=>{e.preventDefault();const startX=e.clientX;const startWidth=rightSidebarWidth;const onMove=ev=>{const deltaX=startX-ev.clientX;const newWidth=Math.max(200,Math.min(600,startWidth+deltaX));setRightSidebarWidth(newWidth);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};// ── TCP Resizer ── const startTcpResize=e=>{e.preventDefault();const startX=e.clientX;const startWidth=tcpWidth;const onMove=ev=>{const deltaX=ev.clientX-startX;const newWidth=Math.max(280,Math.min(600,startWidth+deltaX));setTcpWidth(newWidth);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};// ── Right Sidebar Row Resizer (Media Explorer / AI Panel) ── const startRowResize=e=>{e.preventDefault();const startY=e.clientY;const startHeight=mediaExplorerHeight;const sidebarEl=document.getElementById('right-sidebar');const sidebarHeight=sidebarEl?sidebarEl.getBoundingClientRect().height:400;const onMove=ev=>{const deltaY=startY-ev.clientY;const pct=(startHeight/100*sidebarHeight+deltaY)/sidebarHeight*100;const newPct=Math.max(20,Math.min(80,pct));setMediaExplorerHeight(newPct);};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const rulerRef=useRef(null);const activeSourcesRef=useRef([]);const activeTrackNodesRef=useRef({});// { [trackId]: { gainNode, pannerNode } } -const startOffsetTimeRef=useRef(0);const startBufferOffsetRef=useRef(0);const startAudioTimeRef=useRef(0);const animationFrameIdRef=useRef(null);const toastTimeoutRef=useRef(null);const rulerDragStartRef=useRef(null);const rulerAnchorRef=useRef(null);const isDraggingRulerRef=useRef(false);const subTabDragStartRef=useRef(null);const isDraggingSubTabRef=useRef(false);const handlePlayPauseRef=useRef(null);const currentTimeRef=useRef(currentTime);// D9: playhead sample counter cho native bridge (A11/A13 dùng để đồng bộ +const nativeLoopSrcsRef=useRef(new Map());// V42A: { [trackId+'::'+clipId]: { src, loopStart, loopEnd } } — native WebAudio loop qua wrap +const startOffsetTimeRef=useRef(0);const startBufferOffsetRef=useRef(0);const startAudioTimeRef=useRef(0);// V40: real PDC — freeze playhead theo latency FX path (thay V39 freeze giả +// định 0.31s → under-estimate → playhead chạy trước ~0.66s). __fxRtPlayheadFreeze: +// gọi ở mọi play start (allowCold=true ở startTrackPlayback che cold-start VST +// pre-roll; section play không khởi động FX session → false). __fxRtSnapPlayhead: +// worklet fx_play_first → bỏ freeze đúng mẫu đầu tiên nghe được. Fail-safe: +// session không active trong 1500ms → hủy freeze (đừng đóng băng playhead vô hạn). +window.__fxRtPlayheadFreeze=function(allowCold){try{if(_fxRtFreezeTimer){clearTimeout(_fxRtFreezeTimer);_fxRtFreezeTimer=null;}}catch(e3){}// V41: gate theo CHAIN (không chỉ active) — session FX warm (stop giữ warm, +// bypass toggle gọi rebuildMasteringGraph KHÔNG gọi fxRtStop) → fxRt.active vẫn +// true khi bypass → trước đây freeze nhầm → playhead chạy trước → wrap sớm → vấp. +const _fxWillRun=!!(fxRt&&fxRtChain().length>0&&(fxRt.active||allowCold));if(!_fxWillRun)return;const _estMs=fxRt.pdcCalibMs||FX_RT_START_EST_MS;const _wasActiveAtFreeze=!!(fxRt&&fxRt.active);startAudioTimeRef.current=getAudioContext().currentTime+_estMs/1000;_fxRtPlayheadSnapPending=true;_fxRtFreezeTries=0;// V41: snap xác định bằng timer tại est — loop wrap warm không post fx_play_first +// cho pass mới (old-tail tiêu thụ dbgPlayFirst) nên không có event để snap. Cold +// start (fxRt.active=false) KHÔNG snap sớm: chờ fx_play_first thật (VST load 4s). +_fxRtFreezeTimer=setTimeout(function freezeSnapRetry(){_fxRtFreezeTimer=null;if(_wasActiveAtFreeze&&_fxRtPlayheadSnapPending){_fxRtPlayheadSnapPending=false;startAudioTimeRef.current=getAudioContext().currentTime;return;}if(fxRt&&fxRt.starting){if(_fxRtFreezeTries<20){_fxRtFreezeTries++;_fxRtFreezeTimer=setTimeout(freezeSnapRetry,500);}return;}if(!fxRt||!fxRt.active){startAudioTimeRef.current=getAudioContext().currentTime;}// session fail → bỏ freeze +},_estMs);};window.__fxRtSnapPlayhead=function(){if(fxRt&&fxRt.active&&_fxRtPlayheadSnapPending){_fxRtPlayheadSnapPending=false;startAudioTimeRef.current=getAudioContext().currentTime;}};const animationFrameIdRef=useRef(null);const toastTimeoutRef=useRef(null);const rulerDragStartRef=useRef(null);const rulerAnchorRef=useRef(null);const isDraggingRulerRef=useRef(false);const subTabDragStartRef=useRef(null);const isDraggingSubTabRef=useRef(false);const handlePlayPauseRef=useRef(null);const currentTimeRef=useRef(currentTime);// D9: playhead sample counter cho native bridge (A11/A13 dùng để đồng bộ // timeline sample-accurate); cập nhật trong updatePlayhead khi bridge active. const bridgePlayheadSampleRef=useRef(0);// V10: timer của transport STOP#2 (150ms sau STOP#1, V8 bug 3 guard) — loop // restart gọi stopAllPlayback() + startTrackPlayback() cùng lúc, STOP#2 trễ @@ -1686,7 +1728,12 @@ const startSubTabPlayback=(st,offsetWallTime)=>{const context=getAudioContext(); // V21: section play không gọi fxRtStart — hủy idle timer thủ công (không // để master FX bị teardown giữa lúc section đang phát). if(fxRtWarmIdleTimer){clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=null;}restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain -if(!st.buffer){st.buffer=context.createBuffer(1,128,context.sampleRate);}const speed=st.speed||1.0;const offsetBuffer=offsetWallTime*speed;const eff=st.buffer.getChannelData(0);const edBuffer=context.createBuffer(1,eff.length,st.buffer.sampleRate);const edData=edBuffer.getChannelData(0);edData.set(eff);const fx=st.effects||{};if(fx.reverse){const reversed=new Float32Array(edData);for(let i=0;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeInMs/1000*sr));for(let i=0;i0){const sr=edBuffer.sampleRate;const fadeSamples=Math.min(edData.length,Math.floor(fx.fadeOutMs/1000*sr));for(let i=edData.length-fadeSamples;i0){volumeGainNode.gain.cancelScheduledValues(context.currentTime);volNodes.forEach((n,i)=>{const t=context.currentTime+n.time/speed;const linearGain=Math.pow(10,n.db/20);if(i===0)volumeGainNode.gain.setValueAtTime(linearGain,t);else volumeGainNode.gain.linearRampToValueAtTime(linearGain,t);});}// Schedule panning automation nodes @@ -1695,7 +1742,9 @@ const duration=st.buffer.duration;const fIn=st.fadeInLen||0;const fOut=st.fadeOu const route=createMasteringRoute(context,{masteringBypass:!!trackAudioBypassMap[st.trackId]},masterBus);fadeGainNode.connect(route.routeGain);fadeGainNode.connect(route.dryGain);source.start(context.currentTime,offsetBuffer);activeSourcesRef.current=[source];// PIANO_ROLL: GIỮ track node thật trong ref (SF notes đã schedule tới nó — // ghi đè bằng silent node làm mất mastering route/updateSfRouting đúng). if(st.type!=='PIANO_ROLL'){activeTrackNodesRef.current[st.trackId]={gainNode:volumeGainNode,pannerNode,source};}// Realtime mute/solo for the newly created playback chain. -if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(st.trackId,null);startOffsetTimeRef.current=offsetWallTime;startBufferOffsetRef.current=offsetBuffer;startAudioTimeRef.current=context.currentTime;};const playMetronomeClick=(time,isDownbeat=false)=>{try{const ctx=getAudioContext();const osc=ctx.createOscillator();const gainNode=ctx.createGain();osc.connect(gainNode);gainNode.connect(masterBus?masterBus.input:ctx.destination);osc.frequency.setValueAtTime(isDownbeat?1000:800,time);gainNode.gain.setValueAtTime(0.08,time);gainNode.gain.exponentialRampToValueAtTime(0.001,time+0.08);osc.start(time);osc.stop(time+0.1);}catch(e){console.log('Metronome click play error:',e);}};// Realtime re-schedule tracking (loop play cập nhật khi items đổi vị trí) +if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(st.trackId,null);startOffsetTimeRef.current=offsetWallTime;startBufferOffsetRef.current=offsetBuffer;startAudioTimeRef.current=context.currentTime;// V40: real PDC — freeze playhead theo latency FX path (đo được, không ước +// như V39). Section play không khởi động FX session → allowCold=false. +try{if(window.__fxRtPlayheadFreeze)window.__fxRtPlayheadFreeze(false);}catch(e){}};const playMetronomeClick=(time,isDownbeat=false)=>{try{const ctx=getAudioContext();const osc=ctx.createOscillator();const gainNode=ctx.createGain();osc.connect(gainNode);gainNode.connect(masterBus?masterBus.input:ctx.destination);osc.frequency.setValueAtTime(isDownbeat?1000:800,time);gainNode.gain.setValueAtTime(0.08,time);gainNode.gain.exponentialRampToValueAtTime(0.001,time+0.08);osc.start(time);osc.stop(time+0.1);}catch(e){console.log('Metronome click play error:',e);}};// Realtime re-schedule tracking (loop play cập nhật khi items đổi vị trí) const scheduledItemsSigRef=React.useRef('');const playheadFrameCountRef=React.useRef(0);const pendingRescheduleRef=React.useRef(null);// Master-silence watchdog counter: đếm frame im lặng liên tục khi đang play const masterSilenceFramesRef=React.useRef(0);// Cooldown rebuild (ms) — phòng watchdog trigger lặp trong đoạn im tự nhiên const lastMasterRebuildTimeRef=React.useRef(0);const lastRescheduleTimeRef=React.useRef(0);// Re-schedule NGAY (reset cooldown) sau khi THẢ chuột — check kế tiếp trong @@ -1775,18 +1824,23 @@ if(isPlaying){const context=getAudioContext();const _ph=startOffsetTimeRef.curre else _local=_ph-_itemInfo.startTime;}// Log chẩn đoán (rate-limited ~2 lần/giây): xác nhận đồng bộ MAIN/ // SECTION ↔ PIANO ROLL — ph (vị trí play) → local (trong item). try{const _sf=window.__syncFrame=(window.__syncFrame||0)+1;if(_sf%45===0){console.log('[Sync] PR',st.id,'ph=',_ph.toFixed(2),'item.start=',_itemInfo?_itemInfo.startTime.toFixed(2):'-','dur=',_itemInfo?_itemInfo.duration.toFixed(2):'-','→ local=',_local.toFixed(2),'tab=',activeTabRef.current);}}catch(e){}setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:_local}:s));}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}const context=getAudioContext();const speedFactor=st.speed||1.0;const elapsed=context.currentTime-startAudioTimeRef.current;const wallTime=startOffsetTimeRef.current+elapsed;const bufferPos=startBufferOffsetRef.current+elapsed*speedFactor;// Loop sub-tab selection (bufferPos is buffer-time) -if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&st.isLooping){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +if(st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionStart!==st.selectionEnd&&st.isLooping){const start=Math.min(st.selectionStart,st.selectionEnd);const end=Math.max(st.selectionStart,st.selectionEnd);if(bufferPos>=end){fxRtWrapRestart=true;// V38: loop wrap restart +fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),currentTime:start,isPlaying:true}:s));schedulePianoRollMidi(st,start);startSubTabPlayback(st,start);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}const effectiveDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const bpmVal=parseInt(bpmRef?.current||bpm)||120;const beatSec=60.0/bpmVal;if(recordingStateRef.current==='RECORDING')return 600.0;// 10 min during recording -let maxEnd=0;notes.forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});return Math.max(maxEnd*beatSec,16*beatSec*4)+1.0;})():st.buffer.duration;if(bufferPos>=effectiveDuration){fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +let maxEnd=0;notes.forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});return Math.max(maxEnd*beatSec,16*beatSec*4)+1.0;})():st.buffer.duration;if(bufferPos>=effectiveDuration){fxRtWrapRestart=true;// V38: loop wrap restart +fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();if(st.isLooping){setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),currentTime:0,isPlaying:true}:s));// Loop lại: phải schedule notes MỚI (âm piano roll mất ở lần loop 2 // nếu chỉ chạy silent buffer). if(st.type==='PIANO_ROLL')schedulePianoRollMidi(st,0);startSubTabPlayback(st,0);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:0,isPlaying:false}:s));}return;}setSubTabs(prev=>prev.map(s=>s.id===activeTabRef.current?{...s,currentTime:wallTime}:s));animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(!isPlaying)return;const context=getAudioContext();const elapsed=context.currentTime-startAudioTimeRef.current;const updatedTime=startOffsetTimeRef.current+elapsed;// Selection Loop - LOOP_MAKER.md + LOOP_EDITOR_2.md §4.2 // If selection cleared by user, play linearly (don't loop) -if(!selectionCleared&&isLoopingSelection&&selLeft!==null&&selRight!==null){if(selRight>selLeft&&updatedTime>=selRight){if(hasAnySolo||selectionMode==='local'){fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime -stopAllPlayback();startOffsetTimeRef.current=selLeft;startAudioTimeRef.current=context.currentTime;if(hasAnySolo){const soloed=tracks.filter(t=>t.solo);soloed.forEach(t=>startLocalTrackPlayback(t.id,selLeft));}else{startLocalTrackPlayback(localSelectionTrackId,selLeft);}setCurrentTime(selLeft);setIsPlaying(true);}else{fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +if(!selectionCleared&&isLoopingSelection&&selLeft!==null&&selRight!==null){if(selRight>selLeft&&updatedTime>=selRight){if(hasAnySolo||selectionMode==='local'){fxRtWrapRestart=true;// V38: loop wrap restart +fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +stopAllPlayback();startOffsetTimeRef.current=selLeft;startAudioTimeRef.current=context.currentTime;if(hasAnySolo){const soloed=tracks.filter(t=>t.solo);soloed.forEach(t=>startLocalTrackPlayback(t.id,selLeft));}else{startLocalTrackPlayback(localSelectionTrackId,selLeft);}setCurrentTime(selLeft);setIsPlaying(true);}else{fxRtWrapRestart=true;// V38: loop wrap restart +fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();startOffsetTimeRef.current=selLeft;startAudioTimeRef.current=context.currentTime;startTrackPlayback(selLeft);setCurrentTime(selLeft);setIsPlaying(true);}animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}}// Hết bài: dừng/loop lại tại ENDTIME THẬT của session (projectEnd — không // buffer 12 bars như maxDuration dùng cho scroll/zoom). -if(updatedTime>=projectEndRef.current){console.log('[Stop] projectEnd reached — updatedTime=',updatedTime.toFixed(2),'projectEnd=',projectEndRef.current.toFixed(2),'isLoopingSelection=',isLoopingSelection);if(recordingStateRef.current==='RECORDING'){setCurrentTime(updatedTime);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(isLoopingSelection){fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +if(updatedTime>=projectEndRef.current){console.log('[Stop] projectEnd reached — updatedTime=',updatedTime.toFixed(2),'projectEnd=',projectEndRef.current.toFixed(2),'isLoopingSelection=',isLoopingSelection);if(recordingStateRef.current==='RECORDING'){setCurrentTime(updatedTime);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}if(isLoopingSelection){fxRtWrapRestart=true;// V38: loop wrap restart +fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime stopAllPlayback();startOffsetTimeRef.current=0;startAudioTimeRef.current=context.currentTime;startTrackPlayback(0);setCurrentTime(0);setIsPlaying(true);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);return;}stopAllPlayback();setCurrentTime(0);return;}setCurrentTime(updatedTime);animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);};useEffect(()=>{if(isPlaying||subTabs.some(s=>s.isPlaying)){animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}else{cancelAnimationFrame(animationFrameIdRef.current);}return()=>cancelAnimationFrame(animationFrameIdRef.current);},[isPlaying,subTabs,isLoopingSelection,selLeft,selRight,selectionMode,localSelectionTrackId,selectionCleared,activeTab]);// ── FX Nodes ── const createChorusNode=(context,inputNode,outputNode)=>{const dryGain=context.createGain();dryGain.gain.value=0.6;const wetGain=context.createGain();wetGain.gain.value=0.5;const delayNode=context.createDelay();delayNode.delayTime.value=0.02;const lfo=context.createOscillator();lfo.type='sine';lfo.frequency.value=1.5;const lfoGain=context.createGain();lfoGain.gain.value=0.002;lfo.connect(lfoGain);lfoGain.connect(delayNode.delayTime);lfo.start();inputNode.connect(dryGain);inputNode.connect(delayNode);delayNode.connect(wetGain);dryGain.connect(outputNode);wetGain.connect(outputNode);return{stop:()=>{try{lfo.stop();}catch(e){}}};};const createReverbNode=(context,inputNode,outputNode)=>{const dryGain=context.createGain();dryGain.gain.value=0.6;const wetGain=context.createGain();wetGain.gain.value=0.4;const convolver=context.createConvolver();const rate=context.sampleRate;const len=rate*2.0;const impulse=context.createBuffer(2,len,rate);const left=impulse.getChannelData(0);const right=impulse.getChannelData(1);for(let i=0;i{if(!track)return null;let node=activeTrackNodesRef.current[track.id];if(!node){const gainNode=context.createGain();const volDb=track.volumeDb??0;const volLinear=volDb<=-50?0:Math.pow(10,volDb/20);gainNode.gain.setValueAtTime(volLinear,context.currentTime);const pannerNode=context.createStereoPanner();pannerNode.pan.setValueAtTime((track.pan??0)/100,context.currentTime);// Ensure master bus is initialized for MAIN OUT routing @@ -1835,7 +1889,7 @@ fxRtTrackReconcile(trackId,node,track);}catch(e){console.warn('rebuildTrackFxGra // Cần activeTrackNodesRef/isPlayingRef. Session giống master (fxRtStart): // fetch start → AudioWorkletNode 'sf-fx-realtime' → WS; WS open → splice // worklet vào chain hiện tại (đọc mods MỚI NHẤT — an toàn với rebuild). -const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608231800`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1 +const fxRtTrackStart=async(trackId,track,path)=>{const key=trackId+':'+path;const chain=fxRtTrackChain(track);if(!chain.length)return;if(fxRtTracks[key]){if(!fxRtTracks[key].node||fxRtTracks[key].node.context!==getAudioContext())fxRtTrackStop(trackId,path,true);else return;}const ctx=getAudioContext();if(!ctx||!ctx.audioWorklet)return;const chainPayload=chain.map(s=>s.type==='vst3'?{type:'vst3',path:s.path,name:s.name||s.path,preset_b64:s.preset_b64||'',bypass:!!s.bypass||!!(track&&track.vstFxBypass)}:{type:'builtin',id:s.type,params:s.params&&Object.keys(s.params).length?s.params:{},bypass:!!s.bypass});let st;try{st=await window.SonicAPI.fxRealtimeStart({fx_chain:chainPayload,sample_rate:ctx.sampleRate||44100});}catch(e){console.warn('[FX RT track] start fail',trackId,path,e);return;}const entry={sessionId:st.session_id,ws:null,node:null,ready:false,path,spliced:null,chainKey:fxRtTrackChainKey(chain)+'|vb:'+(track&&track.vstFxBypass?'1':'0'),paramsBySlot:chain.map(s=>fxRtEntryParams(s,null)),inQueue:[],pumpTimer:null,lastInSent:0,flushing:false,flushTimer:null};fxRtTracks[key]=entry;fxRtGuiPushShm();try{await ctx.audioWorklet.addModule(`${API_BASE_URL}/static/js/sf-fx-realtime.js?v=202608272300`);const node=new AudioWorkletNode(ctx,'sf-fx-realtime',{numberOfInputs:1,numberOfOutputs:1,outputChannelCount:[2]});entry.node=node;const u=new URL(API_BASE_URL);const wsProto=u.protocol==='https:'?'wss://':'ws://';// WS to 'localhost' bi tre ~300ms (Chromium thu ::1 truoc roi fallback) - dung 127.0.0.1 const wsHost=u.hostname==='localhost'||u.hostname==='::1'?'127.0.0.1'+(u.port?':'+u.port:''):u.host;const tok=localStorage.getItem('sonic_token')||'';const ws=new WebSocket(`${wsProto}${wsHost}${st.ws_url}${tok?'?token='+encodeURIComponent(tok):''}`);entry.ws=ws;ws.binaryType='arraybuffer';ws.onopen=()=>{entry.ready=true;entry.inQueue=[];// bỏ audio stale trước khi WS open — hết lag khởi đầu fxRtTrackSplice(trackId,path);try{(entry.paramsBySlot||[]).forEach((pl,slot)=>pl.forEach(p=>ws.send(JSON.stringify({cmd:'set_param',slot,key:p.key,value:p.value}))));}catch(e){}};ws.onmessage=ev=>{if(typeof ev.data==='string'){try{const d=JSON.parse(ev.data);if(d&&d.cmd==='flush_done'){entry.flushing=false;if(entry.flushTimer){clearTimeout(entry.flushTimer);entry.flushTimer=null;}if(entry.node)entry.node.port.postMessage({type:'flush_done'});}}catch(e){}return;}if(!(ev.data instanceof ArrayBuffer)||!entry.node)return;if(entry.flushing)return;// V24: output stale trong luc flush — drop const f=new Float32Array(ev.data);const half=f.length/2;const l=new Float32Array(half);const r=new Float32Array(half);for(let i=0;ifxRtTrackStop(trackId,path,true);ws.onerror=()=>{try{ws.close();}catch(e){}};entry.node.port.onmessage=ev=>{const d=ev.data;if(!d||!entry.ws)return;if(d.type==='flush'){// V24: nhu master — worklet underrun-resume → flush engine ring @@ -1961,13 +2015,21 @@ const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNo // FluidSynth GM sai âm — safety net cho mọi caller (ghost notes, per-note). if(isStandaloneSf()&&isVstTrackEngine(synthEngine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive())){playNativeVstNote(track,pitch,velocity,durMs,startTime,track?track.id:'global');return;}window.SonicSF.playNote(pitch,velocity,durMs,startTime,program,destNode,ch,synthEngine);};const startTrackPlayback=offsetTime=>{const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade (play trong cửa sổ 80ms) restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain -// Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active. -try{fxRtStart();}catch(e){console.warn('[FX RT] start error:',e);}// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng +// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc +// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu). +try{if(window.BridgeAudioNode&&window.BridgeAudioNode.flush)window.BridgeAudioNode.flush();}catch(e){}// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong +// cho phia sau; flush ring bridge (V30) khong chaym queue nay. +const _wrapMode=fxRtWrapRestart?'loop':'play';fxRtWrapRestart=false;// V38 +try{if(fxRt&&fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'clear_queue',mode:_wrapMode});}catch(e){}_dbgPlayStartMs=performance.now();_dbgFirstAudioPending=true;_dbgFirstAudiblePending=true;_dbgLog('play_start','offset='+offsetTime+' epoch='+_nativePlayEpoch+' '+_dbgBridgeGain()+' '+_dbgWorkletState()+' fxrt='+(fxRt.active?1:0)+' node='+(fxRt.node?1:0)+' q='+(fxRt.stats?fxRt.stats.queueLen:'n/a'));// Realtime VST FX (master chain qua SHM bridge): bật khi play + chain active. +try{fxRtStart();}catch(e){console.warn('[FX RT] start error:',e);}// V40: real PDC — freeze playhead theo latency FX path; allowCold=true che +// cold-start VST pre-roll (playhead không chạy trước khi có tiếng). Bỏ V39 +// freeze giả định 0.31s (under-estimate → playhead chạy trước ~0.66s). +try{if(window.__fxRtPlayheadFreeze)window.__fxRtPlayheadFreeze(true);}catch(e){}// D2: bridge active -> báo transport PLAY (bridge flush note-off cũ + đồng // bộ timeline; A13 C++ xử lý arg1=playheadSamples sau này). if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){// V10: loop restart — stopAllPlayback vừa schedule STOP#2 (150ms); hủy // ngay khi PLAY, nếu không STOP#2 đến sau PLAY → bridge drop mọi note-on // (V8 guard transportStopped) → VSTi chỉ play 1 vòng loop. -if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);bridgeStopRetryTimerRef.current=null;}try{window.NativeBridgeService.transport('play');}catch(e){}}// V12: guardPlay (scheduleMidiNoteDispatch) đọc isPlayingRef.current — nếu +if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);bridgeStopRetryTimerRef.current=null;}try{window.NativeBridgeService.transport('play');}catch(e){}_dbgLog('play_transport','offset='+offsetTime);}// V12: guardPlay (scheduleMidiNoteDispatch) đọc isPlayingRef.current — nếu // để React render set (setIsPlaying(true) sau) thì note-on delay=0 của // loop restart (rAF callback → React commit trễ hơn setTimeout 0) bị drop // → loop pass 2+ câm. Set NGAY tại scheduler. @@ -1976,14 +2038,25 @@ try{applyMasterAutomationSchedule(offsetTime||0);}catch(e){console.warn('[Auto] // FX (khi bật). try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(e){}const allPlayTracks=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:activeTracks;// Capture items signature at schedule time — updatePlayhead so sánh với // signature này để phát hiện items đổi vị trí giữa lúc play (re-schedule). -scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING')+' projectEnd='+(projectEndRef.current||0).toFixed(2));const hasSolo=allPlayTracks.some(t=>t.solo);allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);// Realtime VST FX per-track (SHM bridge): mọi luồng track qua bridge — +// V28: defer bookkeeping signature — tính đồng bộ chặn main thread trước +// first-audio (góp vào delay onset); signature chỉ so sánh ~10fps trong +// updatePlayhead → trễ 0ms là đủ (re-schedule phát hiện muộn không sao). +setTimeout(function(){try{scheduledItemsSigRef.current=buildItemsSignature(allPlayTracks,sessionTabs);}catch(e){}},0);console.log('[Play] offset='+offsetTime+' tracks='+allPlayTracks.length+' masterBus='+!!masterBus+' dest='+(context.destination?'ok':'MISSING')+' projectEnd='+(projectEndRef.current||0).toFixed(2));const hasSolo=allPlayTracks.some(t=>t.solo);// V42A: native loop region — loop selection hợp lệ → [selLeft, selRight]; +// isLoopingSelection không selection → [0, projectEnd] (project-end wrap 24407). +// Không loop → _loopActive=false → fallback dip V42. +const _loopSel=!selectionCleared&&isLoopingSelection&&selLeft!==null&&selRight!==null&&selRight>selLeft;const _loopLs=_loopSel?selLeft:0;const _loopLe=_loopSel?selRight:projectEndRef.current;const _loopActive=isLoopingSelection&&_loopLe>_loopLs;allPlayTracks.forEach(track=>{const isPlayable=hasSolo?track.solo:!track.muted;if(!isPlayable)return;const gainNode=getOrCreateTrackNode(track,context);// Realtime VST FX per-track (SHM bridge): mọi luồng track qua bridge — // session/worklet cho TỪNG path có âm (audio-clips + MIDI/SF); splice // vào graph khi WS ready. try{const rtPaths=fxRtTrackPaths(track);if(rtPaths.indexOf('audio')>=0)fxRtTrackStart(track.id,track,'audio');if(rtPaths.indexOf('sf')>=0)fxRtTrackStart(track.id,track,'sf');}catch(e){console.warn('[FX RT track] start error:',e);}const pannerNode=activeTrackNodesRef.current[track.id].pannerNode;// Re-apply route gains theo content-type NGAY tại thời điểm play — node // có thể STALE (tạo từ MAIN play với track cùng id — route theo data cũ) // → first play sau double-click vào SECTION-TAB bị bypass. Re-apply // routeBypassFor(track) mỗi lần schedule sẽ hết chu kỳ "stop+play mới đúng". -const playNode=activeTrackNodesRef.current[track.id];if(playNode&&playNode.route&&playNode.route.routeGain){const rb=routeBypassFor(track);playNode.route.routeGain.gain.value=rb?0:1;playNode.route.dryGain.gain.value=rb?1:0;console.log('[Bypass] reapply track',track.id,'contentType='+(routeBypassFor._lastType?'midi':'audio'),'bypass='+rb,'→ routeGain='+playNode.route.routeGain.gain.value+'/dryGain='+playNode.route.dryGain.gain.value);}console.log('[Play] track',track.id,track.name,'node ok:',!!(gainNode&&pannerNode),'muted:',track.muted);const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];clips.forEach(clip=>{if(!clip.buffer)return;const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;if(offsetTime0?track.clips:track.buffer?[{id:'default',buffer:track.buffer,startTime:track.startTime||0,name:track.name,speed:track.speed||1.0}]:[];clips.forEach(clip=>{if(!clip.buffer)return;const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip.speed||1.0);const clipEnd=clipStart+clipDuration;// V42A: native WebAudio loop — wrap mẫu-chính-xác tại loopEnd, không +// restart → không click (thay dip V42). Chỉ khi loop active + track 1 +// clip + speed=1 + clip phủ đủ vùng loop. Source giữ nguyên qua wrap +// (doHardStop skip khi fxRtWrapRestart) → seam-free. +const _loopable=_loopActive&&clips.length===1&&Math.abs((clip.speed||1.0)-1)<0.0001&&clipStart<=_loopLs+0.001&&clipEnd>=_loopLe-0.001;const _key=track.id+'::'+(clip.id||'default');const _kept=nativeLoopSrcsRef.current.get(_key);if(_loopable&&_kept&&_kept.src&&_kept.src.playbackState!=='ended'){if(Math.abs(_kept.loopStart-_loopLs)<0.001&&Math.abs(_kept.loopEnd-_loopLe)<0.001){activeSourcesRef.current.push(_kept.src);// giữ source loop đang chạy qua wrap +return;}try{_kept.src.stop();}catch(e){}nativeLoopSrcsRef.current.delete(_key);}const source=context.createBufferSource();source.buffer=clip.buffer;source.playbackRate.value=clip.speed||1.0;source.connect(gainNode);if(_loopable){source.loop=true;source.loopStart=(_loopLs-clipStart)/(clip.speed||1.0);source.loopEnd=(_loopLe-clipStart)/(clip.speed||1.0);nativeLoopSrcsRef.current.set(_key,{src:source,loopStart:_loopLs,loopEnd:_loopLe});}if(offsetTime0)&&(window.SonicSF||nativeSf||nativeVst)){if(nativeSf||nativeVst){scheduleNativeTrack(track,offsetTime,context,gainNode,bpm,{playheadNow:()=>startOffsetTimeRef.current+(getAudioContext().currentTime-startAudioTimeRef.current),sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current});}else{// Preview cache: capture the soundfont (pre track-FX) for fast offline export ensureMidiCapture(track,activeTrackNodesRef.current[track.id]);var trkCh=assignTrackMidiChannel(track,allPlayTracks);// Ensure instrument is loaded in FluidSynth if(window.SonicSF&&track.synth_engine&&track.synth_engine.type==='soundfont'&&track.synth_engine.soundfont_id){window.SonicSF.selectInstrument(trkCh,track.synth_engine.soundfont_bank||0,track.synth_engine.soundfont_program||0,track.synth_engine.soundfont_id);}const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const itemEndSec=item.startTime+(item.duration||4);const notes=item.notes||[];notes.forEach(note=>{// note start/duration is in beats (for MIDI items) @@ -2014,10 +2087,15 @@ setTimeout(()=>{// ⚠️ Guard: stop → setTimeout sót không được fire ( // nhảy sau khi âm đã dừng — user bug 05:45). if(!isPlayingRef.current)return;if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20) window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}},delay*1000);}else{const remainingDurMs=(notePlayEndMain-offsetTime)*1000;if(window.SonicSF){scheduleMidiNoteDispatch(subTrack,note.pitch||60,note.velocity||0.8,remainingDurMs,context.currentTime,program,subNode,subCh,subTrack.synth_engine);scheduleNoteExpressionCC11(subTrack,note,subCh,context.currentTime,0,remainingDurMs/1000);}if(window.triggerMidiVuActivity){// VU key theo CONTEXT (tách MAIN vs SECTION — 07:20) -window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) -const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();// V24: solo/local path — gọi fxRtStart (idempotent) để âm solo qua master +window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+subTrack.id,note.velocity||0.8);window.triggerMidiVuActivity((activeTab&&activeTab.startsWith('session_')?'_sess_':'')+track.id,note.velocity||0.8);}}}});});}});});}});// V42A: sweep native-loop sources không được schedule pass này (track muted/ +// solo-off) → dừng + xóa — tránh source loop chơi âm thầm mãi. +try{nativeLoopSrcsRef.current.forEach((v,k)=>{if(!v||!v.src||v.src.playbackState==='ended'){nativeLoopSrcsRef.current.delete(k);return;}if(activeSourcesRef.current.indexOf(v.src)===-1){try{v.src.stop();}catch(e){}nativeLoopSrcsRef.current.delete(k);}});}catch(e){}updateSfRouting();};// Solo playback for Local Selection Loop (LOOP_MAKER.md §2.2) +const startLocalTrackPlayback=(trackId,offsetTime)=>{const context=getAudioContext();// V42A: local/solo path khong dung native loop — dung + xoa src loop con lai +// tu main play (wrap local giu chung qua doHardStop -> double audio neu khong). +try{nativeLoopSrcsRef.current.forEach(v=>{try{if(v&&v.src)v.src.stop();}catch(e){}});nativeLoopSrcsRef.current.clear();}catch(e){}// V24: solo/local path — gọi fxRtStart (idempotent) để âm solo qua master // chain (trước đây path này bỏ qua → solo không qua master FX). -try{fxRtStart();}catch(e){console.warn('[FX RT] start error:',e);}// V12: parity với startTrackPlayback — loop restart (solo/local) sau +try{fxRtStart();}catch(e){console.warn('[FX RT] start error:',e);}const _wrapMode=fxRtWrapRestart?'loop':'play';fxRtWrapRestart=false;// V38 +try{if(fxRt&&fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'clear_queue',mode:_wrapMode});}catch(e){}// V12: parity với startTrackPlayback — loop restart (solo/local) sau // stopAllPlayback(): hủy STOP#2 + báo PLAY (nếu không C++ transportStopped // giữ true → mọi note-on bị drop → solo/local loop pass 2+ câm) + set // isPlayingRef NGAY (guardPlay race — xem startTrackPlayback). @@ -2030,7 +2108,11 @@ const clipStart=clip.startTime||0;const clipDuration=clip.buffer.duration/(clip. const midiItems=track.midiItems||[];const nativeSf=isStandaloneSf()&&isSfTrackEngine(track.synth_engine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());const nativeVst=isStandaloneSf()&&isVstTrackEngine(track.synth_engine)&&!(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive());if((track.type==='MIDI'||midiItems.length>0)&&(window.SonicSF||nativeSf||nativeVst)){if(nativeSf||nativeVst){scheduleNativeTrack(track,offsetTime,context,gainNode,bpm,{playheadNow:()=>startOffsetTimeRef.current+(getAudioContext().currentTime-startAudioTimeRef.current),sources:activeSourcesRef.current,isActive:()=>!!isPlayingRef.current});return;// native render → audio buffer (không qua WASM per-note) }var lcCh=assignTrackMidiChannel(track,tracks);const bpmVal=parseInt(bpm)||120;const secondsPerBeat=60.0/bpmVal;midiItems.forEach(item=>{const notes=item.notes||[];notes.forEach(note=>{const noteStartSec=item.startTime+(note.start_beat||0)*secondsPerBeat;const noteDurSec=(note.duration_beats||1)*secondsPerBeat;const noteEndSec=noteStartSec+noteDurSec;if(offsetTime{if(st.type!=='PIANO_ROLL')return;const context=getAudioContext();_nativePlayEpoch++;// new play — vô hiệu doHardStop đang fade restorePendingFadeSession();// V17: huy fade dang treo — replay khong bi dip gain -// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 → +// V30: flush ring audio cu (engine idle blocks tu truoc) — xoa backlog truoc +// khi play; neu khong block dau cua play cho ring cu chay het (~341ms cap cu). +try{if(window.BridgeAudioNode&&window.BridgeAudioNode.flush)window.BridgeAudioNode.flush();}catch(e){}// V31: xoa backlog outQueue master FX (block idle ~341ms) — play moi khong +// cho phia sau; flush ring bridge (V30) khong chaym queue nay. +try{if(fxRt&&fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'clear_queue',mode:'play'});}catch(e){}// V10: piano roll loop restart cũng gọi stopAllPlayback() trước (STOP#1 → // bridge transportStopped=true) → mọi note-on bị drop nếu không PLAY lại. // Hủy STOP#2 (150ms) + báo PLAY để bridge nhận note mới (loop liên tục). if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);bridgeStopRetryTimerRef.current=null;}try{window.NativeBridgeService.transport('play');}catch(e){}}// V12: guardPlay race tương tự startTrackPlayback — set NGAY để note-on @@ -2060,8 +2142,15 @@ setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,isPlaying:false}:s));}else{t stopAllPlayback();const startOffset=st.currentTime||0;if(st.type==='PIANO_ROLL'){schedulePianoRollMidi(st,startOffset);startSubTabPlayback(st,startOffset);}else{startSubTabPlayback(st,startOffset);}setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,buffer:s.buffer||getAudioContext().createBuffer(1,128,getAudioContext().sampleRate),isPlaying:true,currentTime:startOffset}:s));// BẮT BUỘC start rAF loop updatePlayhead — nếu không, playhead sub-tab // không di chuyển (và loop/stop không bao giờ chạy). animationFrameIdRef.current=requestAnimationFrame(updatePlayhead);}catch(e){console.error('[Play] Piano Roll play error:',e);showToast('Lỗi phát Piano Roll: '+e.message,'error');}}return;}const context=getAudioContext();if(isPlaying){stopAllPlayback(true);// user stop — fade out -}else{startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=context.currentTime;startTrackPlayback(currentTime);setIsPlaying(true);}};handlePlayPauseRef.current=handlePlayPause;const handlePianoRollRealtimePlay=trackIds=>{const prSt=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(!prSt)return;var prLayers=[];var ghostData=window.SonicGhost&&window.SonicGhost.extractGhostLayers?window.SonicGhost.extractGhostLayers(activeTracks,prSt.trackId,prSt.target_id,parseInt(bpm)||120):[];(ghostData||[]).forEach(function(layer){if(!trackIds||trackIds.indexOf(layer.track_id)===-1)return;var ltrk=(activeTracks||[]).find(function(t){return t.id===layer.track_id;});prLayers.push({trackId:layer.track_id,notes:layer.notes.map(function(n){return{pitch:n.pitch,start_beat:n.relative_start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8};}),instrumentProgram:ltrk?ltrk.instrumentProgram:undefined,instrumentName:ltrk?ltrk.instrumentName:undefined,synthEngine:ltrk?ltrk.synth_engine:undefined});});fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime -stopAllPlayback();const prCtx=getAudioContext();const prTNode=activeTrackNodesRef.current[prSt.trackId];if(prTNode&&prTNode.gainNode){prTNode.gainNode.gain.setValueAtTime(prTNode.gainNode.gain.value||1,prCtx.currentTime);prTNode.gainNode.gain.linearRampToValueAtTime(0.001,prCtx.currentTime+0.04);}setTimeout(()=>{window.SonicSF.stopAll();stopAllNativeSfNotes();stopAllNativeVstNotes();if(prTNode&&prTNode.gainNode){const prTrackData=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===prSt.trackId):null;const prVolDb=prTrackData?prTrackData.volumeDb??0:0;const prVolLinear=prVolDb<=-50?0:Math.pow(10,prVolDb/20);prTNode.gainNode.gain.setValueAtTime(0.001,prCtx.currentTime);prTNode.gainNode.gain.linearRampToValueAtTime(prVolLinear||0.8,prCtx.currentTime+0.015);}const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=prCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);const playTab=Object.assign({},prSt,{ghostPlayLayers:prLayers});schedulePianoRollMidi(playTab,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{ghostPlayLayers:prLayers,isPlaying:true,currentTime:prOffset}):s));},60);};const handlePause=()=>{if(isPlaying||subTabs.some(s=>s.isPlaying))stopAllPlayback(true);};const stopAllPlayback=fade=>{try{isPlayingRef.current=false;// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng). +}else{startOffsetTimeRef.current=currentTime;startAudioTimeRef.current=context.currentTime;startTrackPlayback(currentTime);setIsPlaying(true);// V28: rAF kế tiếp sau setState → đo jank main thread (chênh với play_start). +requestAnimationFrame(function(){_dbgLog('play_rAF','delta_ms='+Math.round(performance.now()-_dbgPlayStartMs));});}};handlePlayPauseRef.current=handlePlayPause;const handlePianoRollRealtimePlay=trackIds=>{const prSt=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(!prSt)return;var prLayers=[];var ghostData=window.SonicGhost&&window.SonicGhost.extractGhostLayers?window.SonicGhost.extractGhostLayers(activeTracks,prSt.trackId,prSt.target_id,parseInt(bpm)||120):[];(ghostData||[]).forEach(function(layer){if(!trackIds||trackIds.indexOf(layer.track_id)===-1)return;var ltrk=(activeTracks||[]).find(function(t){return t.id===layer.track_id;});prLayers.push({trackId:layer.track_id,notes:layer.notes.map(function(n){return{pitch:n.pitch,start_beat:n.relative_start_beat,duration_beats:n.duration_beats,velocity:n.velocity||0.8};}),instrumentProgram:ltrk?ltrk.instrumentProgram:undefined,instrumentName:ltrk?ltrk.instrumentName:undefined,synthEngine:ltrk?ltrk.synth_engine:undefined});});fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +stopAllPlayback();const prCtx=getAudioContext();const prTNode=activeTrackNodesRef.current[prSt.trackId];if(prTNode&&prTNode.gainNode){prTNode.gainNode.gain.setValueAtTime(prTNode.gainNode.gain.value||1,prCtx.currentTime);prTNode.gainNode.gain.linearRampToValueAtTime(0.001,prCtx.currentTime+0.04);}setTimeout(()=>{window.SonicSF.stopAll();stopAllNativeSfNotes();stopAllNativeVstNotes();if(prTNode&&prTNode.gainNode){const prTrackData=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===prSt.trackId):null;const prVolDb=prTrackData?prTrackData.volumeDb??0:0;const prVolLinear=prVolDb<=-50?0:Math.pow(10,prVolDb/20);prTNode.gainNode.gain.setValueAtTime(0.001,prCtx.currentTime);prTNode.gainNode.gain.linearRampToValueAtTime(prVolLinear||0.8,prCtx.currentTime+0.015);}const prOffset=prSt.currentTime||0;startOffsetTimeRef.current=prOffset;startAudioTimeRef.current=prCtx.currentTime;startBufferOffsetRef.current=prOffset*(prSt.speed||1.0);const playTab=Object.assign({},prSt,{ghostPlayLayers:prLayers});schedulePianoRollMidi(playTab,prOffset);setSubTabs(prev=>prev.map(s=>s.id===prSt.id?Object.assign({},s,{ghostPlayLayers:prLayers,isPlaying:true,currentTime:prOffset}):s));},60);};const handlePause=()=>{if(isPlaying||subTabs.some(s=>s.isPlaying))stopAllPlayback(true);};const stopAllPlayback=fade=>{try{if(fxRtWrapRestart&&fxRtChain().length===0&&nativeLoopSrcsRef.current.size===0&&masterBus&&masterBus.muteGain&&masterBus.muteGain.gain){// V41: dip CHỈ khi bypass (chain rỗng). FX active → seam thật xảy ra sau +// latency (~80ms) tại _now+latency; dip tại _now chỉ tạo dropout 14ms giữa +// đuôi pass cũ (audible) mà không che được seam click. +// V42: rút ngắn dip 14ms → 5ms (2ms xuống + 3ms lên) — đủ che click seam +// nhưng dropout quá ngắn để nghe thấy (V39 14ms tạo dropout rõ). +const _actx=getAudioContext();const _now=_actx.currentTime;masterBus.muteGain.gain.cancelScheduledValues(_now);masterBus.muteGain.gain.setValueAtTime(masterBus.muteGain.gain.value||1,_now);masterBus.muteGain.gain.linearRampToValueAtTime(0.0001,_now+0.002);masterBus.muteGain.gain.linearRampToValueAtTime(1,_now+0.005);}// V39: dip input truoc hard-stop +_dbgLog('stop_start','fade='+!!fade+' '+_dbgBridgeGain());isPlayingRef.current=false;// Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng). try{if(masterBus&&masterBus.output&&masterBus.output.gain){const _actx=getAudioContext();masterBus.output.gain.cancelScheduledValues(_actx.currentTime);masterBus.output.gain.setValueAtTime(1,_actx.currentTime);}}catch(e){}// Realtime VST FX: dừng session + khôi phục output→analyser. Nhánh restart // (loop/seek/reschedule/solo...) set fxRtKeepForRestart=true → GIỮ session // (fxRtStart early-return khi active + chain khớp) → hết bypass+reload ~4s @@ -2079,25 +2168,42 @@ try{clearAllCC11Timers();}catch(e){}try{if(subTabsRef.current)subTabsRef.current // xuống 0 trong FADE_MS rồi mới hard-stop — hết click, âm ngân mượt. // Loop restart/seek (fade=false) giữ stop tức thì như cũ (không fade). // Lưu ref trước khi clear để doHardStop trễ không đụng playback mới. -const ctx=getAudioContext();const stopEpoch=_nativePlayEpoch;const FADE_MS=200;const capturedSources=activeSourcesRef.current.slice();const capturedNodes=Object.values(activeTrackNodesRef.current);activeSourcesRef.current=[];activeTrackNodesRef.current={};const fadedGains=[];// V16: stop NGAY clip source CHƯA phát (playbackState='scheduled') — nếu +const ctx=getAudioContext();const stopEpoch=_nativePlayEpoch;const FADE_MS=200;const capturedSources=activeSourcesRef.current.slice();const capturedNodes=Object.values(activeTrackNodesRef.current);activeSourcesRef.current=[];// V42A: wrap (fade=false) GIỮ track nodes — source native-loop kept vẫn +// routed (node cũ). User stop (fade=true) mới xóa nodes. +if(fade)activeTrackNodesRef.current={};const fadedGains=[];// V16: stop NGAY clip source CHƯA phát (playbackState='scheduled') — nếu // chờ tới doHardStop (sau FADE_MS) chúng đã bắt đầu phát trong lúc fade → // nghe như "play lại rồi tắt". Source đang kêu ('playing') giữ lại cho ramp. try{capturedSources.forEach(src=>{try{if(src.playbackState==='scheduled')src.stop();}catch(e){}});}catch(e){}let bridgeGainNode=null;if(fade&&ctx){// V17: restore fade session cu TRUOC khi capture prev — double-stop trong // cua so fade: neu khong, session cu stale -> bo restore, session moi capture // prev giua ramp (vd 0.5) -> restore sai -> gain ket. restorePendingFadeSession();try{const now=ctx.currentTime;capturedNodes.forEach(n=>{if(n&&n.gainNode&&n.gainNode.gain){fadedGains.push({node:n.gainNode,prev:n.gainNode.gain.value});n.gainNode.gain.cancelScheduledValues(now);n.gainNode.gain.setValueAtTime(n.gainNode.gain.value,now);n.gainNode.gain.linearRampToValueAtTime(0.0001,now+FADE_MS/1000);}});if(window.BridgeAudioNode&&window.BridgeAudioNode.getOutputNode){const bg=window.BridgeAudioNode.getOutputNode();if(bg&&bg.gain){bridgeGainNode=bg;bg.gain.cancelScheduledValues(now);bg.gain.setValueAtTime(bg.gain.value,now);bg.gain.linearRampToValueAtTime(0.0001,now+FADE_MS/1000);}}}catch(e){console.warn('[Stop] fade setup error:',e);}_pendingFadeSession={fadedGains,bridgeGainNode,stopEpoch};}const doHardStop=()=>{try{const stale=_nativePlayEpoch!==stopEpoch;// play mới bắt đầu trong lúc fade -capturedSources.forEach(src=>{try{src.stop();}catch(e){}});capturedNodes.forEach(n=>{if(n.fxStopFn){try{n.fxStopFn();}catch(e){}}});// V17: restore gain LUON — play moi trong cua so fade (stale) truoc day bo qua +// V42A: wrap (fxRtWrapRestart, fade=false) — giữ source native-loop đang +// chạy qua restart (seam-free tại loopEnd); stop/seek thật → dừng + xóa. +const _wrapKeepLoop=!stale&&fxRtWrapRestart&&nativeLoopSrcsRef.current.size>0;const _loopSrcs=_wrapKeepLoop?new Set(Array.from(nativeLoopSrcsRef.current.values(),v=>v.src)):null;capturedSources.forEach(src=>{try{if(_loopSrcs&&_loopSrcs.has(src))return;src.stop();}catch(e){}});capturedNodes.forEach(n=>{if(n.fxStopFn){try{n.fxStopFn();}catch(e){}}});// V17: restore gain LUON — play moi trong cua so fade (stale) truoc day bo qua // restore -> track/bridge gain ket 0.0001 -> moi am sau bi fade-out dai dang. // Chi skip khi co fade session MOI hon (session nay het pending — session moi tu restore). -if(_pendingFadeSession&&_pendingFadeSession.stopEpoch===stopEpoch){restorePendingFadeSession();}if(stale)return;// play mới đang chạy — không dừng bridge/SF của nó -stopMidiCapture();stopAllNativeSfNotes();stopAllNativeVstNotes();// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết +if(stale){if(_pendingFadeSession&&_pendingFadeSession.stopEpoch===stopEpoch){restorePendingFadeSession();}return;// play mới đang chạy — không dừng bridge/SF của nó +}if(!_wrapKeepLoop)nativeLoopSrcsRef.current.clear();// V42A: stop/seek thật — quên native loop +// V26: user stop (fade) — dừng master FX tail: xóa outQueue worklet + flush +// engine ring (audio đã xử lí còn trong pipeline phát tiếp ~0.3-0.5s sau stop +// = "ding"). Worklet tự fade-out 21ms (hết click) rồi báo main flush engine; +// session giữ warm (play kế tiếp FX ngay, không reload VST). +try{if(fade&&fxRt&&fxRt.node&&fxRt.active){fxRt.node.port.postMessage({type:'flush_for_stop'});}}catch(e){console.warn('[Stop] fx flush error:',e);}stopMidiCapture();stopAllNativeSfNotes();stopAllNativeVstNotes();// D2/D6: bridge active -> transport STOP (bridge flush toàn pitch, hết // âm ngân tức thì; A13 C++ flush note-off). -if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try{window.NativeBridgeService.transport('stop');}catch(e){}// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync +if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){try{window.NativeBridgeService.transport('stop');}catch(e){}_dbgLog('stop_transport','epoch='+stopEpoch);// V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay. +try{if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive&&window.SonicMidiRouter.isBridgeActive()){Object.keys(trackMidiChannelsRef.current||{}).forEach(function(_tid){var _ch=trackMidiChannelsRef.current[_tid];window.NativeBridgeService.dispatchMidiEvent('CC',_ch,123,0,0,0,0,false);window.NativeBridgeService.dispatchMidiEvent('CC',_ch,120,0,0,0,0,false);});}}catch(e){console.warn('[Stop] midi panic error:',e);}// V27: xóa audio còn buffer trong pipeline (preBuffer/chunks + worklet +// ring) — nếu không, âm trước stop còn trong ring phát tiếp sau fade. +try{if(window.BridgeAudioNode&&window.BridgeAudioNode.flush)window.BridgeAudioNode.flush();}catch(e){console.warn('[Stop] bridge flush error:',e);}// V8 bug 3: transport stop thu 2 sau 150ms — note-on timer guard sync // qua React effect (cham) van bay toi bridge sau STOP → retrigger am // treo. V10: timer lưu ref — startTrackPlayback (loop restart) hủy // khi PLAY, tránh STOP#2 giết loop tiếp theo (VSTi chỉ play 1 vòng). if(bridgeStopRetryTimerRef.current){clearTimeout(bridgeStopRetryTimerRef.current);}bridgeStopRetryTimerRef.current=setTimeout(()=>{bridgeStopRetryTimerRef.current=null;try{window.NativeBridgeService.transport('stop');}catch(e){}},150);}if(window.SonicSF){try{window.SonicSF.stopAll();}catch(e){console.warn('[Stop] stopAll error:',e);}// Dừng triệt để: noteoff từng note + hủy scheduled note-on (hết âm stuck) -if(window.SonicSF.panic){try{window.SonicSF.panic();}catch(e){console.warn('[Stop] panic error:',e);}}}}catch(e){console.warn('[Stop] stopAllPlayback error:',e);}};if(fade&&ctx){try{setTimeout(doHardStop,FADE_MS);}catch(e){doHardStop();}}else{doHardStop();}// Clear VU activity NGAY: hết âm → VU tắt tức thì (không decay 0.35s từ +if(window.SonicSF.panic){try{window.SonicSF.panic();}catch(e){console.warn('[Stop] panic error:',e);}}}// V28: KHÔNG auto-restore gain sau stop — VSTi release tail có thể dài +// hơn 800ms (V27 vẫn còn "ding"). Gain giữ ~0.0001 vô thời hạn; restore +// khi: play mới (startTrackPlayback entry) hoặc midi bất kỳ (pushEvent → +// __restorePendingFadeSession — keyboard/pad preview). Tail vẫn render +// nhưng im lặng qua bridge gain. +_dbgLog('stop_hard','epoch='+stopEpoch+' '+_dbgBridgeGain());}catch(e){console.warn('[Stop] stopAllPlayback error:',e);}};if(fade&&ctx){try{setTimeout(doHardStop,FADE_MS);}catch(e){doHardStop();}}else{doHardStop();}// Clear VU activity NGAY: hết âm → VU tắt tức thì (không decay 0.35s từ // âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main // items phải tắt theo, không "diễn" tiếp). try{midiVuActivityRef.current={};}catch(e){}// Clear luôn counter note đang giữ — nếu không, sau stop (âm đã dừng) @@ -2488,4 +2594,4 @@ const tid=selectedTrackId||activeTracks&&activeTracks[0]&&activeTracks[0].id;if( instrumentSelectorData?.vst_instruments?.length>0&&/*#__PURE__*/React.createElement("div",{className:"mt-3 text-[10px] text-zinc-500 uppercase font-bold px-1 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3 text-violet-400"}),"VST Instruments"),(instrumentSelectorData?.vst_instruments||[]).map(v=>/*#__PURE__*/React.createElement("button",{key:'vst_modal_'+(v.id||v.name),onClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,v.id,v.name||v.id);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectorLoadedInst&&selectorLoadedInst.instId===v.id?"bg-violet-800 text-white":"bg-zinc-800 hover:bg-violet-800 text-zinc-300")+" flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-cyan-400 shrink-0"},v.type||"VST")))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto space-y-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),sfPresets===null?(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},"Loading instruments...")):sfPresets.length>0?sfPresets.filter(p=>!selectedSoundFontId||p._sfId===selectedSoundFontId).filter(p=>!sfPresetSearchQuery||(p.name||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())||(p._sfName||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:i,onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program),onDoubleClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectorLoadedInst&&selectorLoadedInst.sfId===p._sfId&&selectorLoadedInst.bank===p.bank&&selectorLoadedInst.prog===p.program?"bg-amber-800/80 text-white ring-1 ring-amber-400/60":"bg-zinc-800/60 hover:bg-amber-800 text-zinc-300")+" truncate flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 shrink-0"},p._sfDisplay),p.bank===128?/*#__PURE__*/React.createElement("span",{className:"mr-1"},"🥁"):null,p.name||'Preset '+p.program)):(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},sfPresetSearchQuery?"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 ph\u00f9 h\u1ee3p.":"No presets found.")))))),fxSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:()=>setFxSelectorTrackId(null)},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-64 p-4 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400 mb-3"},"Track FX"),/*#__PURE__*/React.createElement("div",{className:"space-y-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'chorus'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300"},"Chorus"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'reverb'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300"},"Reverb")))),instrumentDropdownTrackId&&instrumentDropdownBtnRect&&/*#__PURE__*/React.createElement("div",{"data-instr-dropdown":"",className:"fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",style:{top:instrumentDropdownBtnRect.bottom+4,left:Math.max(4,Math.min(instrumentDropdownBtnRect.left,window.innerWidth-224))}},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:instrumentSearchQuery,onChange:e=>setInstrumentSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs outline-none"}),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentDropdownTrackId,null),className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),filteredInstruments.soundfonts.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"SoundFonts"),filteredInstruments.soundfonts.map((sf,i)=>/*#__PURE__*/React.createElement("button",{key:"sfd_"+i,onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,"sf_"+sf.id,sf.display||sf.name||sf.id,undefined,undefined);},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},sf.display||sf.name||sf.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-amber-400 shrink-0 ml-1"},"SF"))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Instruments"),filteredInstruments.vst.map((v,i)=>/*#__PURE__*/React.createElement("div",{key:"vstd_"+i,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,v.id,v.name||v.id);},className:"flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-cyan-400 shrink-0 ml-1"},v.type||"VST")))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Presets (.vstpreset)"),(window.SonicRuntime&&window.SonicRuntime.presets||[]).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:"psd_"+i,onClick:()=>setTrackPreset(instrumentDropdownTrackId,p.id),className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name||p.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-emerald-400 shrink-0 ml-1"},"PRESET"))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;window.SonicAPI.uploadPreset(f).then(function(r){if(r&&r.success){if(window.SonicRuntime)window.SonicRuntime.refreshPresets();showToast('Đã upload preset: '+(r.original_name||r.name||''),'success');}else{showToast('Upload preset thất bại','error');}}).catch(function(err){showToast('Upload lỗi: '+(err.message||err),'error');});};inp.click();},className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",title:"Upload preset .vstpreset xuất từ native GUI"},"\u2B06 Upload preset (t\u1EEB native GUI...)"),!filteredInstruments.soundfonts.length&&!filteredInstruments.vst.length&&/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-500 py-4 text-center"},"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o"))));};// React onWheel is passive -> e.preventDefault() can't stop panel scroll, so the // ruler slips away from the cursor mid-scroll. Native capture listener (non-passive) // blocks scrolling whenever the wheel starts over a [data-sf-wheel] container. -document.addEventListener('wheel',e=>{const t=e.target;if(t&&t.closest&&t.closest('[data-sf-wheel]'))e.preventDefault();},{passive:false,capture:true});const root=ReactDOM.createRoot(document.getElementById('root'));root.render(/*#__PURE__*/React.createElement(App,null));setTimeout(()=>lucide.createIcons(),300); +document.addEventListener('wheel',e=>{const t=e.target;if(t&&t.closest&&t.closest('[data-sf-wheel]'))e.preventDefault();},{passive:false,capture:true});const root=ReactDOM.createRoot(document.getElementById('root'));root.render(/*#__PURE__*/React.createElement(App,null));setTimeout(()=>lucide.createIcons(),300); \ No newline at end of file diff --git a/app/static/js/services/bridgeAudioNode.js b/app/static/js/services/bridgeAudioNode.js index af6fa6b..7cd01f2 100644 --- a/app/static/js/services/bridgeAudioNode.js +++ b/app/static/js/services/bridgeAudioNode.js @@ -31,7 +31,7 @@ console.warn('[BridgeAudioNode] AudioWorklet unavailable — ScriptProcessor fallback'); return false; } - var url = '/static/js/services/bridgeAudioWorklet.js?v=202608152200'; + var url = '/static/js/services/bridgeAudioWorklet.js?v=202608272301'; _ctx.audioWorklet.addModule(url).then(function () { if (!_initialized) return; // disconnect() happened while loading if (_useWorklet) return; // already set up (init retry) @@ -49,12 +49,15 @@ _workletNode.port.postMessage({ type: 'audio', l: b.l, r: b.r }); } console.log('[BridgeAudioNode] AudioWorklet active'); + try { if (window.__dbgLog) window.__dbgLog('worklet', 'active pre=' + _preBuffer.length); } catch (e) {} } catch (err) { console.warn('[BridgeAudioNode] AudioWorkletNode failed — ScriptProcessor fallback', err); _useWorklet = false; + try { if (window.__dbgLog) window.__dbgLog('worklet', 'NODE_FAIL'); } catch (e) {} } }).catch(function (err) { console.warn('[BridgeAudioNode] addModule failed — ScriptProcessor fallback', err); + try { if (window.__dbgLog) window.__dbgLog('worklet', 'MODULE_FAIL'); } catch (e) {} }); return true; // pending — worklet may become active later } @@ -157,6 +160,16 @@ _dest = dest; }, disconnect: disconnect, - isReady: function () { return _initialized; } + isReady: function () { return _initialized; }, + getDiag: function () { + return { + initialized: _initialized, + useWorklet: _useWorklet, + hasWorkletNode: !!_workletNode, + preBufferLen: _preBuffer.length, + chunksLen: _chunks.length, + gain: _gainNode ? _gainNode.gain.value : -1 + }; + } }; })(); diff --git a/app/static/js/services/bridgeAudioWorklet.js b/app/static/js/services/bridgeAudioWorklet.js index 35e3104..8934de4 100644 --- a/app/static/js/services/bridgeAudioWorklet.js +++ b/app/static/js/services/bridgeAudioWorklet.js @@ -1,6 +1,8 @@ // app/static/js/services/bridgeAudioWorklet.js // VSTi crackle fix: audio sink chay tren audio thread (khong jank main thread). -// - Ring block 256 mau, toi da 64 block (~371ms @48k) — hap thu jitter pump. +// - Ring block 256 mau, toi da 8 block (~42ms @48k) — hap thu jitter pump +// nhung gioi han backlog: 64 block (~371ms) tao tre ~0.3-0.4s khi play/ +// keyboard preview (audio moi phai cho ring cu chay het). // - Underrun: fade ve 0 thay vi click (hard silence). // - Resume sau silence: fade vao tu 0. // - Overrun: drop block cu nhat + crossfade 64 mau tai diem drop (het click). @@ -8,7 +10,7 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor { constructor() { super(); this._ring = []; - this._MAX = 64; + this._MAX = 8; this._gain = 1.0; this._lastOutL = 0.0; this._lastOutR = 0.0; diff --git a/app/static/js/services/unifiedMidiRouter.js b/app/static/js/services/unifiedMidiRouter.js index 46342c1..b954710 100644 --- a/app/static/js/services/unifiedMidiRouter.js +++ b/app/static/js/services/unifiedMidiRouter.js @@ -5,6 +5,8 @@ (function () { var MELODIC_CHANNELS = [0, 1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13, 14]; // skip 9 (percussion); 15 reserved for Media Explorer preview + var _lastTimelineDbgTs = 0; + var router = { bridgeConnected: false, onFallback: null, // function(cmd, channel, pitch, velocity) — set by app.jsx @@ -51,11 +53,29 @@ * data2/data3 (A12): CC value / program / PB LSB|MSB — passed to bridge only. */ pushEvent: function (opts) { + // V28: midi bất kỳ (keyboard/pad/timeline) → restore bridge gain đang + // giữ ~0.0001 sau stop (ding fix — V27 restore 800ms không đủ tail dài). + try { if (window.__restorePendingFadeSession) window.__restorePendingFadeSession(); } catch (e) {} + // V31: live note (ARM keyboard preview) — clear master FX outQueue backlog + // (idle block ~341ms) truoc khi note moi render; timeline note (live=false) + // KHONG clear — tranh drop audio dang phat. + if (opts.live && opts.cmd === 'NOTE_ON') { try { if (window.__clearMasterFxQueue) window.__clearMasterFxQueue(); } catch (e) {} } var cmd = opts.cmd, ch = opts.channel, pitch = opts.pitch; var vel = (opts.velocity === undefined ? 1.0 : opts.velocity); var sampleOffset = opts.sampleOffset || 0; if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion); if (ch === undefined || ch === null) ch = 0; + try { + // V36: midi_push log = 1 Tauri invoke sync (dbg_log -> disk I/O tren main + // thread Rust) moi event. Timeline phat ~380 event/s -> main thread nghet + // -> stall 110-170ms -> crackle. Chi log live (ARM preview), timeline + // gioi han 1/200ms. + var _nowMs = (window.performance && performance.now) ? performance.now() : Date.now(); + if (window.__dbgLog && (opts.live || _nowMs - _lastTimelineDbgTs > 200)) { + if (!opts.live) _lastTimelineDbgTs = _nowMs; + window.__dbgLog('midi_push', 'cmd=' + cmd + ' ch=' + ch + ' live=' + (opts.live ? 1 : 0) + ' vel=' + Math.round((vel || 0) * 100) + (window.__dbgBridgeGain ? ' ' + window.__dbgBridgeGain() : '') + ' q=' + (window.__fxRtQueueLen ? window.__fxRtQueueLen() : 'n/a')); + } + } catch (e) {} if (this.isBridgeActive()) { window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live); return; diff --git a/app/static/js/sf-fx-realtime.js b/app/static/js/sf-fx-realtime.js index eb2039a..4c55eeb 100644 --- a/app/static/js/sf-fx-realtime.js +++ b/app/static/js/sf-fx-realtime.js @@ -9,10 +9,10 @@ // ~5.5ms/call > budget 5.33ms → 179/s < 187.5/s → engine drop input → // crackle). Gửi burst 4 block → bridge take=4 → 1 call process 1024 mẫu // (amortize overhead: 14-19ms/4 block ≈ 3.5-4.75ms/block) → đủ 187.5/s. -// Latency thêm ~4 block (~21ms) — worklet outQueue 64 block hấp thụ. +// Latency thêm ~4 block (~21ms) — worklet outQueue 32 block hấp thụ. // // FILL (hết crackle do jitter): không phát ngay khi có block đầu — chờ queue -// ≥ FILL (24 block ~128ms @48k) rồi mới phát. Engine gửi ASAP (không pace +// ≥ FILL (8 block ~43ms @48k) rồi mới phát. Engine gửi ASAP (không pace // realtime) → client tích lũy buffer an toàn; mọi jitter (bridge process VST // biến thiên, GC, WS, main thread rAF) bị hấp thụ bởi FILL thay vì queue ≈1 // → cạn → ngắt quãng. @@ -29,8 +29,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { super(); this.BLOCK = 256; // FXRT_BLOCK this.BURST = 4; // số block gom 1 frame (bridge batch) - this.FILL = 24; // 24 block ~128ms @48k: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel). PDC 7936 = (7+24)*256 (fxRtApplyPdc). - this.CAP = 64; // cap queue chặn overflow (~341ms) + this.FILL = 8; // V40: 8 block ~43ms @48k: buffer khởi đầu — hấp thụ jitter main-thread (GUI decode, rAF, VST panel). PDC 3840 = (7+8)*256 (fxRtApplyPdc). Lưu ý: cushion mỏng hơn (24→8) → jitter main-thread mạnh → nguy cơ underrun fade-out. + this.RESUME_FILL = 8; // V26: re-buffer khi resume SAU STOP (flush_for_stop) — 8 block ~43ms (V40: = FILL) → play kế tiếp bắt đầu nhanh; underrun-stall vẫn dùng FILL (crackle/wobble). + this.resumeFill = this.FILL; // mức re-buffer khi resume sau flush — stop: RESUME_FILL, underrun-stall: FILL + this.CAP = 32; // V40: cap queue chặn overflow (~170ms) this.HOLD_FADE = 4; // 4 block ~21ms: fade 1→0 sau HOLD hết (hết click cắt cứng) this.holdFadeLeft = 0; // mẫu còn lại của fade-out khi underrun this.silenced = false; // đã fade về 0 → im lặng; resume phải fade-in, không hold lại @@ -47,23 +49,96 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.FADE = 512; // fade-in 512 mẫu (~11ms @48k) khi bắt đầu phát/resume — hết click this.outGain = 0; // gain phát hiện tại (0→1 trong fade) this.fadeLeft = 0; // số mẫu còn lại của fade - this.skipFade = 0; // >0: crossfade 64 mẫu đang chạy sau jump (cap-drop/resume) — hết click + this.skipFade = 0; // >0: crossfade đang chạy sau jump (cap-drop/resume/wrap) — hết click + this.skipFadeTotal = 64; // V42: độ dài crossfade hiện tại (64 mẫu thường; WRAP_FADE 256 tại seam wrap) this.skipFromL = 0.0; // mẫu output tại điểm jump — điểm bắt đầu crossfade this.skipFromR = 0.0; this.lastOutL = 0.0; // mẫu output cuối — nguồn crossfade + điểm bắt đầu fade-out this.lastOutR = 0.0; + this.wrapFadeBlocks = -1; // V42: số block tail pass cũ còn lại sau clear_queue('loop') — đếm ngược để crossfade đúng seam tail→head + this.WRAP_FADE = 256; // V42: crossfade 256 mẫu (~5.3ms @48k) tại seam wrap — che click jump nội dung end→start (FX path) this.fadeFromL = 0.0; // mẫu cố định khi fade-out underrun (fade tuyến tính tới 0) this.fadeFromR = 0.0; this.port.onmessage = (e) => { const d = e.data; if (d && d.type === 'set_param') { this.port.postMessage(d); return; } if (d && d.type === 'flush_done') { this.flushPending = false; return; } + if (d && d.type === 'flush_for_stop') { + // V26: user stop — main thread báo xóa audio tail trong queue (audio đã + // xử lí còn phát ~0.3-0.5s sau stop = tiếng "ding"). Fade-out 21ms từ + // mẫu cuối (hết click) → im lặng; báo main flush engine ring (audio + // stale trong WS/SHM không chơi tiếp). Resume lần sau: RESUME_FILL + // (8 block ~43ms, V40: = FILL) — play kế tiếp không chờ trễ. + this.outQueue.length = 0; + this.outPos = 0; + this.playing = false; + this.wrapFadeBlocks = -1; // V42: stop — hủy wrap crossfade chờ + this.flushPending = true; + this.resumeFadeIn = true; + this.resumeFill = this.RESUME_FILL; + this.silenced = false; + // V30: fade-out tu mau output CUOI (lastOut) — truoc day dung fadeFrom + // cu (mac dinh 0) -> jump mau -> click/crackle ngay khi stop. + this.fadeFromL = this.lastOutL; + this.fadeFromR = this.lastOutR; + this.holdFadeLeft = this.HOLD_FADE * this.BLOCK; + this.port.postMessage({ type: 'flush' }); + return; + } + if (d && d.type === 'clear_queue') { + // V34: backlog guard o DAU handler (V33 dat sau mutation -> outQueue.length=0 + // chay truoc -> guard luon return, clear van drop queue + fade + re-buffer moi + // lan = V31). Queue nho (< FILL 8 block) -> bo qua hoan toan (khong mutation, + // khong re-buffer) -> het ARM crackle (queue ARM chi 0-8 block, engine ~realtime). + // Queue day (idle lau, engine chay truoc ~32 block) -> clear dung (fix lag 170ms). + // V35: mode:'play' (start/section/pianoroll) LUON clear - queue < FILL luc + // play-start con stale block idle/ARM (~16) -> bo qua => phat audio cu roi + // can queue ~500ms (cold start). mode:'live' (ARM) giu guard het crackle. + // V37 ARM probe: reset dbg flags BEFORE guard so live notes (small queue) also log fx_hop + this.dbgClearAt = currentTime * 1000; + this.dbgInFirst = true; + this.dbgOutFirst = true; + this.dbgPlayFirst = true; + // V39: loop wrap — KHONG wipe: giu queue chay lien tuc (tail pass cu + // pre-roll pass moi qua VST lookahead) -> het wobble dau bai. Chi reset + // dbg flags. mode 'play' van wipe (bao ve audio cu khi seek/cold start). + // V42: dem so block tail pass cu con lai trong queue -> wrap crossfade + // trigger dung luc block dau pass moi len front (seam tail->head). + if (d.mode === 'loop') { + this.wrapFadeBlocks = this.outQueue.length; + return; + } + if (d.mode !== 'play' && this.outQueue.length < this.FILL) return; + // V31: play/ARM live note — main thread bao xoa backlog block idle + // (engine render lien tuc khi STOP -> outQueue day ~32 block ~170ms + // audio cu; play moi phai cho phia sau -> lag 0.3-0.5s). KHONG post + // 'flush' (khong chaym engine WS — note-on play vua day xuong se bi + // drop). Resume sau 8 block (~43ms) + fade-out/fade-in tu mau cuoi. + this.outQueue.length = 0; + this.outPos = 0; + this.playing = false; + this.wrapFadeBlocks = -1; // V42: wipe — hủy wrap crossfade chờ (không có tail cũ) + this.resumeFadeIn = true; + // V34: mode phan biet — play re-buffer FILL 8 (~43ms, cushion du hap thu + // engine ramp/jitter dau play); ARM live note giu RESUME_FILL 8 (~43ms) — preview + // nhanh, khong cho FILL 8. + this.resumeFill = (d.mode === 'play') ? this.FILL : this.RESUME_FILL; + this.silenced = false; + this.fadeFromL = this.lastOutL; + this.fadeFromR = this.lastOutR; + this.holdFadeLeft = this.HOLD_FADE * this.BLOCK; + return; + } if (!d || d.type !== 'out' || !d.l || !d.r) return; // V24: đang chờ flush (resume sau underrun/stall) → drop block stale. // Backlog engine gửi khi worklet im là audio CŨ (chơi lại → lag cộng dồn // ~1s) — chỉ nhận block MỚI từ sau flush_done. if (this.flushPending) return; this.outQueue.push({ l: d.l, r: d.r }); + if (this.dbgOutFirst) { + this.dbgOutFirst = false; + this.port.postMessage({ type: 'dbg', tag: 'fx_out_first', t: Math.round(currentTime * 1000), q: this.outQueue.length }); + } while (this.outQueue.length > this.CAP) { this.outQueue.shift(); this.statCapDrop++; @@ -72,6 +147,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { // block mới (front) phải phát từ đầu, không tiếp tục vị trí block cũ. if (this.skipFade <= 0) { this.skipFade = 64; + this.skipFadeTotal = 64; this.skipFromL = this.lastOutL; this.skipFromR = this.lastOutR; } @@ -84,6 +160,12 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.statUnderrun = 0; this.statHold = 0; this.statFade = 0; this.statSilence = 0; this.statCapDrop = 0; this.statFadeIn = 0; this.statCalls = 0; + // V32: do latency master FX path — danh dau clear_queue, bao main thread hop dau + // sau clear (in_first / out_first / play_first) → dbg.log delta vs play_start. + this.dbgClearAt = 0; + this.dbgInFirst = false; + this.dbgOutFirst = false; + this.dbgPlayFirst = false; } process(inputs, outputs) { const input = inputs[0]; @@ -111,6 +193,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { } this.port.postMessage({ type: 'in', data: inter.buffer }, [inter.buffer]); this.inCount = 0; + if (this.dbgInFirst) { + this.dbgInFirst = false; + this.port.postMessage({ type: 'dbg', tag: 'fx_in_first', t: Math.round(currentTime * 1000) }); + } } } } @@ -136,7 +222,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.fadeLeft--; } if (this.skipFade > 0) { - const t = 1 - this.skipFade / 64; + const t = 1 - this.skipFade / this.skipFadeTotal; outL[i] = (this.skipFromL * (1 - t) + blk.l[this.outPos] * t) * this.outGain; if (outR) outR[i] = (this.skipFromR * (1 - t) + blk.r[this.outPos] * t) * this.outGain; this.skipFade--; @@ -150,6 +236,19 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { if (this.outPos >= this.BLOCK) { this.outQueue.shift(); this.outPos = 0; + // V42: seam tail→head — sau khi shift hết N block tail pass cũ, block + // đầu pass mới lên front → crossfade WRAP_FADE từ mẫu cuối (che click + // jump nội dung end→start, FX path). Trigger 1 lần mỗi wrap. + if (this.wrapFadeBlocks > 0) { + this.wrapFadeBlocks--; + if (this.wrapFadeBlocks === 0) { + this.skipFade = this.WRAP_FADE; + this.skipFadeTotal = this.WRAP_FADE; + this.skipFromL = this.lastOutL; + this.skipFromR = this.lastOutR; + this.port.postMessage({ type: 'dbg', tag: 'fx_wrap_xfade', t: Math.round(currentTime * 1000), q: this.outQueue.length }); + } + } } continue; } @@ -158,6 +257,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { // mẫu output cuối tới 0 (21ms) — vào liền mạch (fadeFrom = lastOut), // hết click cắt cứng. Resume → crossfade/fade-in (xem dưới). this.playing = false; + this.wrapFadeBlocks = -1; // V42: underrun — hủy wrap crossfade chờ (resume qua fade-in/crossfade thường) this.statUnderrun++; if (!this.silenced) { // đã im lặng (fade xong) → giữ im lặng this.fadeFromL = this.lastOutL; @@ -170,7 +270,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { // block — hold che khoảng chờ). V25: sau flush (resumeFadeIn=true) chờ // re-buffer đủ FILL mới resume — chặn loop underrun→flush→resume-1-block // →underrun (wobble): 1 block phát hết ~5ms → cạn → flush lại. - if (this.outQueue.length >= (this.hasStarted && !this.resumeFadeIn ? 1 : this.FILL)) { + if (this.outQueue.length >= (this.hasStarted ? (this.resumeFadeIn ? this.resumeFill : 1) : this.FILL)) { // V24: interruption (underrun sau stall) — backlog trong queue là audio // LẠC HẬU (chơi lại → lag cộng dồn ~1s). KHÔNG chơi backlog: clear // queue + báo main thread flush (engine xóa input/output ring, main @@ -186,6 +286,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.outPos = 0; this.flushPending = true; this.resumeFadeIn = true; + this.resumeFill = this.FILL; // underrun-stall: giữ cushion FILL đầy đủ (V25 wobble) this.port.postMessage({ type: 'flush' }); i--; continue; @@ -193,6 +294,10 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { this.playing = true; this.outPos = 0; this.holdFadeLeft = 0; + if (this.dbgPlayFirst) { + this.dbgPlayFirst = false; + this.port.postMessage({ type: 'dbg', tag: 'fx_play_first', t: Math.round(currentTime * 1000), q: this.outQueue.length }); + } if (this.resumeFadeIn) { this.resumeFadeIn = false; if (this.silenced) { @@ -203,6 +308,7 @@ class SfFxRealtimeProcessor extends AudioWorkletProcessor { } else { // Giữa fade-out → crossfade ngắn từ mẫu hiện tại (không jump về 0). this.skipFade = 64; + this.skipFadeTotal = 64; this.skipFromL = this.lastOutL; this.skipFromR = this.lastOutR; } diff --git a/app/templates/index.html b/app/templates/index.html index 589d623..afe9fdf 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -41,7 +41,7 @@ - + @@ -51,7 +51,7 @@ - +