From c084ac8ba79e3526c2fdf2c02678c5e3f3d69f81 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Sat, 29 Aug 2026 16:39:02 +0700 Subject: [PATCH] V45: D17 seamless loop wrap + D18 stop solo note-off (Qin) + D18b/c guard + engine VST load fixes - D17: wrap_info/wrap-skip, fx wrap xfade, playhead-derived scheduling - D18: solo stop -> NOTE_OFF all sounding timeline pitches + CC123/CC120 - D18b: track audible gain guard, tracksRef fallback - D18c: timelinePitchesRef per-track Set, kill at applyAllTrackMuteSolo - bridge ring _MAX 16, index v=202608290928 - plugins.py/vst_engine.py/fx_realtime.py/JuceFxEngine.cpp: VSTi load + Qin fixes (uncommitted 12:18) --- app/api/v1/plugins.py | 10 +- app/core/fx_realtime.py | 7 +- app/core/vst_engine.py | 4 +- app/static/js/app.jsx | 242 +++++++++++++++---- app/static/js/app.precompiled.js | 133 ++++++---- app/static/js/services/bridgeAudioNode.js | 2 +- app/static/js/services/bridgeAudioWorklet.js | 5 +- app/templates/index.html | 4 +- native_bridge/juce_fx/JuceFxEngine.cpp | 131 +++++++++- 9 files changed, 434 insertions(+), 104 deletions(-) diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 2cc0ca4..5b0e655 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -1,13 +1,10 @@ import os, sys, uuid, json, tempfile, subprocess, time as _time, threading, asyncio, logging -import numpy as np -import soundfile as sf from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, BackgroundTasks, Query, WebSocket, WebSocketDisconnect from fastapi.responses import FileResponse from pydantic import BaseModel from typing import Optional, Any from app.config import settings from app.core.vst_engine import PluginManager, HAS_PYFLUIDSYNTH, render_soundfont_midi_to_audio, RENDER_LOCK, _render_locked -from app.core.render_engine import PythonRenderEngine, _find_sf2_path, _find_default_sf2 from app.core.soundfont_inspector import SoundFontInspector from app.core.soundfont_converter import SoundFontConverter from app.core.soundfont_scanner import SoundFontAutoScanner @@ -570,6 +567,7 @@ async def delete_soundfont(sf_id: str, current_user: dict = Depends(get_current_ @router.get("/soundfonts/download/{sf_id}") async def download_soundfont_asset(sf_id: str): + from app.core.render_engine import _find_sf2_path, _find_default_sf2 clean_id = sf_id.replace("sf_", "") if sf_id.startswith("sf_") else sf_id # Cũng tìm trong static/soundfonts (font bundled theo deployment) — trước # đây chỉ UPLOAD + SYSTEM → font bundled 404 → incognito (IndexedDB rỗng) @@ -963,6 +961,7 @@ class FxRenderRequest(BaseModel): @router.post("/fx-render") async def fx_render(req: FxRenderRequest, current_user: dict = Depends(get_current_user)): """Áp fx_chain lên WAV đầu vào → WAV mới trong PROCESSED_DIR.""" + import soundfile as sf enforce_password_changed(current_user) src = (req.input_path or "").strip() if not src: @@ -1233,6 +1232,8 @@ async def soundfont_render(req: SoundfontRenderRequest, current_user: dict = Dep Khác FluidSynthWASM (docker): âm render server-side bằng pyfluidsynth, trả WAV để client play. Dùng cho: preview/keyboard, play MIDI item khi chạy transport ở môi trường standalone.""" + from app.core.render_engine import _find_sf2_path, _find_default_sf2 + import soundfile as sf enforce_password_changed(current_user) if not req.notes: raise HTTPException(status_code=400, detail="Chưa có nốt nhạc để render") @@ -1288,6 +1289,9 @@ def render_project( current_user: dict = Depends(get_current_user) ): enforce_password_changed(current_user) + from app.core.render_engine import PythonRenderEngine + import soundfile as sf + import numpy as np engine = PythonRenderEngine() # Prevent path traversal: strip directory components; force .wav/.ogg. safe_name = os.path.basename((req.output_filename or "render_output.wav").replace("\\", "/")) diff --git a/app/core/fx_realtime.py b/app/core/fx_realtime.py index 83b2db2..048c97c 100644 --- a/app/core/fx_realtime.py +++ b/app/core/fx_realtime.py @@ -23,10 +23,7 @@ import threading import time import uuid -import numpy as np - from app.config import settings -from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe FXRT_MAGIC = 0x46585254 FXRT_BLOCK = 256 @@ -91,6 +88,7 @@ class FxRealtimeSession: """Một session realtime: SHM + subprocess fx_vst_bridge + I/O block.""" def __init__(self, fx_chain, sample_rate, block_size=FXRT_BLOCK): + import numpy as np from multiprocessing import shared_memory self.fx_chain = fx_chain or [] self.sample_rate = int(sample_rate) @@ -163,6 +161,7 @@ class FxRealtimeSession: return out def _spawn(self): + from app.core.native_render import find_fx_bridge_exe, find_juce_fx_bridge_exe juce_exe = os.getenv("SF_JUCE_FX_BRIDGE_PATH", "").strip() # G3: mặc định dùng JUCE bridge (latency graph thật + processed samples); # fallback bridge cũ khi không có exe JUCE (regression an toàn). @@ -265,6 +264,7 @@ class FxRealtimeSession: """Gom FXRT_IN_SLOTS frame vào pending, đủ 4 → publish atomic vào ring (ghi data toàn bộ slot trước, sau đó in_write += n 1 lần). Ring đầy khi publish → drop batch cũ nhất (giữ latency thấp).""" + import numpy as np arr = np.frombuffer(interleaved, dtype=np.float32) if arr.size != self.block_size * 2: return @@ -333,6 +333,7 @@ class FxRealtimeSession: self.stats['flush'] += 1 def read_output(self): + import numpy as np """Drain output ring → list bytes (mỗi block 2048 bytes interleaved).""" out = [] with self._lock: diff --git a/app/core/vst_engine.py b/app/core/vst_engine.py index 992406e..a6a2632 100644 --- a/app/core/vst_engine.py +++ b/app/core/vst_engine.py @@ -1,9 +1,9 @@ # SonicForge Studio VST / VSTi Engine Service +from __future__ import annotations import os import json import re import time as _time -import numpy as np import functools import threading from ctypes import c_char_p @@ -52,6 +52,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = Xử lí đúng note CHỒNG nhau + đuôi release (event-stream noteon/noteoff). Trả stereo float32 (2, N). Raise RuntimeError nếu pyfluidsynth thiếu / SF không load được.""" + import numpy as np import fluidsynth as _fs settings = _fs.new_fluid_settings() _fs.fluid_settings_setnum(settings, b'synth.sample-rate', float(sr)) @@ -112,6 +113,7 @@ def render_soundfont_midi_to_audio(midi_events: list, sf_path: str, bank: int = @_render_locked def render_midi_events_to_audio(midi_events: list, sr: int = 44100, bpm: float = 120.0, instrument: str = 'synth') -> np.ndarray: + import numpy as np beat_duration_sec = 60.0 / max(30.0, bpm) max_duration_sec = 2.0 for event in midi_events: diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index da87ae7..266e3c0 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -7307,6 +7307,7 @@ const loadVstToBridge = async (track) => { if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {}; const tId = track.id || track.trackId; const bch = window.SonicMidiRouter.allocateChannel(tId, false); + markTrackLoad(tId, { state: 'loading', channel: bch }); try { // V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename — // KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu @@ -7331,11 +7332,12 @@ const loadVstToBridge = async (track) => { } // Đã load ĐÚNG path vào channel này rồi → skip (không LOAD trùng). Bỏ // early-return mù theo flag — flag sai khi C++ restore instrument khác. - if (knownPath && window.__bridgeLoadedPaths[bch] === knownPath) return true; + if (knownPath && window.__bridgeLoadedPaths[bch] === knownPath) { markTrackLoad(tId, { state: 'done' }); return true; } // Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS). if (knownPath) { const ok = await window.NativeBridgeService.loadInstrument(knownPath, btype, bch); if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = knownPath; if (!window.__bridgePluginPaths) window.__bridgePluginPaths = {}; window.__bridgePluginPaths[instrumentId] = knownPath; } + if (!ok) markTrackLoad(tId, { state: 'failed' }); console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', knownPath); return !!ok; } @@ -7355,12 +7357,13 @@ const loadVstToBridge = async (track) => { } } catch (e2) { console.warn('[Bridge] listPlugins fallback fail:', e2); } } - if (!p) return false; + if (!p) { markTrackLoad(tId, { state: 'failed' }); return false; } const ok = await window.NativeBridgeService.loadInstrument(p, btype, bch); if (ok) { window.__bridgeLoadedChannels[bch] = true; window.__bridgeLoadedPaths[bch] = p; } + if (!ok) markTrackLoad(tId, { state: 'failed' }); console.log('[Bridge] auto-load track', track.id, '->', btype, 'ch', bch, ok ? 'OK' : 'FAIL', p); return !!ok; - } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); return false; } + } catch (e) { console.warn('[Bridge] loadVstToBridge fail:', track.id, e); markTrackLoad(tId, { state: 'failed' }); return false; } }; // GM font dự phòng khi soundfont track không có trong thư viện (VD id // sf_generaluser_gs do AI sinh nhưng user chưa tải font) — ưu tiên font GM @@ -7385,6 +7388,7 @@ const loadAnyTrackToBridge = async (track) => { const se = track.synth_engine || {}; const bank = se.soundfont_bank !== undefined ? se.soundfont_bank : (track.soundfont_bank !== undefined ? track.soundfont_bank : 0); const bch = window.SonicMidiRouter.allocateChannel(track.id || track.trackId, bank === 128); + markTrackLoad(track.id || track.trackId, { state: 'loading', channel: bch }); let pl = null; try { const sfId = inst.replace('sf_', ''); @@ -7408,15 +7412,37 @@ const loadAnyTrackToBridge = async (track) => { p = r2 && r2.path ? r2.path : null; } } - if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); return false; } + if (!p) { console.warn('[Bridge] SF2 resolve fail:', track.id, inst); markTrackLoad(track.id || track.trackId, { state: 'failed' }); return false; } const ok = await window.NativeBridgeService.loadInstrument(p, 'SF2', bch); if (ok) { if (!window.__bridgeLoadedChannels) window.__bridgeLoadedChannels = {}; window.__bridgeLoadedChannels[bch] = true; if (!window.__bridgeLoadedPaths) window.__bridgeLoadedPaths = {}; window.__bridgeLoadedPaths[bch] = p; } + if (!ok) markTrackLoad(track.id || track.trackId, { state: 'failed' }); console.log('[Bridge] preload SF2 track', track.id, '-> ch', bch, ok ? 'OK' : 'FAIL', p); return !!ok; - } catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); return false; } + } catch (e) { console.warn('[Bridge] preload SF2 fail:', track.id, e); markTrackLoad(track.id || track.trackId, { state: 'failed' }); return false; } } try { return await loadVstToBridge(track); } catch (e) { return false; } }; +// D16: per-track loading ruler (TCP). window.__trackLoadState[trackId] = +// { state:'pending'|'loading'|'done'|'failed', channel, startedTs, doneTs }. +// Poller trong App (loadTick) chuyển loading→done khi channel xuất hiện trong +// loaded_channels (queryStatus) — đúng cho cả C++ restore (bridge_state.json, +// chạy từ spawn) lẫn JS load — không đánh dấu done sớm khi C++ guard skip. +const markTrackLoad = (trackId, patch) => { + if (!trackId) return; + if (!window.__trackLoadState) window.__trackLoadState = {}; + const cur = window.__trackLoadState[trackId] || { state: 'pending', channel: null, startedTs: 0, doneTs: 0 }; + const next = Object.assign({}, cur, patch); + if (patch.state === 'loading' && cur.state !== 'loading') next.startedTs = Date.now(); + if (patch.state === 'done' && !patch.doneTs) next.doneTs = Date.now(); + window.__trackLoadState[trackId] = next; + if (window.__onTrackLoadState) { try { window.__onTrackLoadState(trackId); } catch (e) {} } +}; +if (typeof document !== 'undefined' && !document.getElementById('sf-ruler-style')) { + const _st = document.createElement('style'); + _st.id = 'sf-ruler-style'; + _st.textContent = '@keyframes sf-ruler { 0% { transform: translateX(-110%); } 100% { transform: translateX(340%); } } .sf-ruler-bar { animation: sf-ruler 1.1s ease-in-out infinite; } @keyframes sf-ruler-done { 0% { opacity: 1; } 100% { opacity: 0; } } .sf-ruler-done { animation: sf-ruler-done 1.4s ease-out forwards; }'; + document.head.appendChild(_st); +} const ensureAndOpenVstGui = async (track) => { const trackId = track.trackId || track.id; const instrumentId = track.instrumentId || (track.synth_engine && track.synth_engine.plugin_id); @@ -7522,6 +7548,28 @@ const wheelSteps = (key, deltaY, deltaMode, pxPerStep = 100) => { // the soundfonts; the per-track channel re-selection happens at note time. const preloadTrackInstruments = async (tracks) => { const list = tracks || []; + // D13: bridge load TRƯỚC vòng WASM — vòng selectInstrument (parse font WASM + // từng track, có thể ~20s) trước đây chặn vòng bridge → user bấm play khi + // app vừa load: NOTE_ON vào channel bridge chưa có instrument → im lặng tới + // khi bridge load xong (~27s từ bootstrap — dbg.log play_first_audible). + if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { + // D16b: pre-mark TẤT CẢ track có instrument thành loading TRƯỚC loop — loop + // nối tiếp (C++ g_loadMutex) khiến track 2-9 không có ruler tới khi track 1 + // xong → UI lâu không có loading item. markTrackLoad channel=null hiện ruler + // ngay; loadAnyTrackToBridge set channel khi tới lượt; poller chuyển done. + for (let i = 0; i < list.length; i++) { + const t = list[i]; + const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id); + if (!inst) continue; + markTrackLoad(t.id || t.trackId, { state: 'loading', channel: null }); + } + for (let i = 0; i < list.length; i++) { + const t = list[i]; + const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id); + if (!inst) continue; + try { await loadAnyTrackToBridge(t); } catch (e) {} + } + } if (window.SonicSF && window.SonicSF.selectInstrument) { for (let i = 0; i < list.length; i++) { const t = list[i]; @@ -7536,17 +7584,7 @@ const preloadTrackInstruments = async (tracks) => { } catch (e) {} } } - // Lỗi 3: mở lại project → VST3 đã lưu phải load lại vào bridge (không mở GUI). - if (window.NativeBridgeService && window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { - for (let i = 0; i < list.length; i++) { - const t = list[i]; - const inst = t.instrumentId || (t.synth_engine && t.synth_engine.plugin_id); - if (!inst) continue; - try { await loadAnyTrackToBridge(t); } catch (e) {} - } - } }; - const ProfileModal = ({ isOpen, onClose, @@ -18741,6 +18779,8 @@ const App = () => { const activeAudioRecordersRef = useRef({}); const pianoRollRecorderRef = useRef(null); const activeMidiPitchesRef = useRef(new Set()); + const timelinePitchesRef = useRef({}); // D18c: per-track Set timeline MIDI pitches dang keu (NOTE_ON fired) + const tracksRef = useRef([]); // D18c: tracks state moi nhat (guard mute/solo fire-time) const [activeMidiPitches, setActiveMidiPitches] = useState(new Set()); const recordingPCMDataRef = useRef({}); const recordingStartTimeRef = useRef(0); @@ -18922,6 +18962,32 @@ const App = () => { if (isMidiSoundTrack(t)) { try { sendMidiVolumeCC7(t, effective, audible); } catch (e) {} } + // D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0 + // (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume) + // → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang + // giữ + CC123/CC120 all-notes-off trên channel track đó. + if (!audible && wasAudible && isMidiSoundTrack(t)) { + try { + const _isPerc = !!(t.synth_engine && t.synth_engine.soundfont_bank === 128) || !!t.is_percussion; + const _held = activeMidiPitchesRef.current || new Set(); + // D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch; + // truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note + // timeline (Qin VST2) cu ngan/loop (tremolo) khi solo track khac. + const _tSet = (timelinePitchesRef.current || {})[t.id]; + if (_tSet && _tSet.size) { + _tSet.forEach(function (_p) { + window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: t.id, pitch: _p, velocity: 0, percussion: _isPerc, live: true }); + }); + } + _held.forEach(function (_p) { + window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: t.id, pitch: _p, velocity: 0, percussion: _isPerc, live: true }); + }); + window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 123, velocity: 0, data2: 0, percussion: _isPerc }); + window.SonicMidiRouter.pushEvent({ cmd: 'CC', channel: t.id, pitch: 120, velocity: 0, data2: 0, percussion: _isPerc }); + if (heldMidiNotesRef.current) delete heldMidiNotesRef.current[t.id]; + if (timelinePitchesRef.current) delete timelinePitchesRef.current[t.id]; + } catch (e) {} + } if (audible && !wasAudible) becameAudible = true; trackAudibleRef.current[t.id] = audible; }); @@ -19114,6 +19180,31 @@ const App = () => { const [editingTrackName, setEditingTrackName] = useState(null); // trackId being edited const [editingClipName, setEditingClipName] = useState(null); // { trackId, clipId } const [editNameInput, setEditNameInput] = useState(''); + // D16: per-track loading ruler — ticker buộc re-render khi có track đang + // loading; poller queryStatus chuyển loading→done khi channel sẵn sàng + // (đúng cho cả C++ restore từ spawn lẫn JS load — không phụ thuộc thứ tự). + const [loadTick, setLoadTick] = useState(0); + useEffect(() => { + window.__onTrackLoadState = () => setLoadTick(t => t + 1); + const iv = setInterval(async () => { + try { + if (!window.NativeBridgeService || !window.SonicMidiRouter || !window.SonicMidiRouter.isBridgeActive()) return; + const m = window.__trackLoadState || {}; + const loadingKeys = Object.keys(m).filter(k => m[k].state === 'loading'); + if (!loadingKeys.length) return; + const st = await window.NativeBridgeService.queryStatus(); + const lc = (st && Array.isArray(st.loaded_channels)) ? st.loaded_channels : null; + let changed = false; + loadingKeys.forEach(k => { + const s = m[k]; + if (lc && s.channel != null && lc.some(x => x.channel === s.channel)) { m[k] = Object.assign({}, s, { state: 'done', doneTs: Date.now() }); changed = true; } + else if (!s.startedTs || Date.now() - s.startedTs > 90000) { m[k] = Object.assign({}, s, { state: 'failed' }); changed = true; } + }); + if (changed) setLoadTick(t => t + 1); + } catch (e) {} + }, 400); + return () => { delete window.__onTrackLoadState; clearInterval(iv); }; + }, []); // ── Context Menu & Clipboard ── const [contextMenu, setContextMenu] = useState(null); // { x, y, trackId } @@ -19582,6 +19673,7 @@ const App = () => { }, [activeTab, sessionTabs, subTabs, tracks]); const activeTracksRef = useRef([]); activeTracksRef.current = activeTracks; + tracksRef.current = tracks; // D18c: sync tracks state moi nhat (guard fire-time) const sessionTabsRef = useRef(sessionTabs); sessionTabsRef.current = sessionTabs; @@ -23883,12 +23975,14 @@ 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) {} + // D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua + // tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap + // trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu. + const _wrapRestart = fxRtWrapRestart; + const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38 + try { if (!_wrapRestart && 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); @@ -24349,6 +24443,7 @@ const App = () => { const end = Math.max(st.selectionStart, st.selectionEnd); if (bufferPos >= end) { fxRtWrapRestart = true; // V38: loop wrap restart + try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {} fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); setSubTabs(prev => prev.map(s => s.id === activeTabRef.current ? { @@ -24377,6 +24472,7 @@ const App = () => { })() : st.buffer.duration; if (bufferPos >= effectiveDuration) { fxRtWrapRestart = true; // V38: loop wrap restart + try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {} fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); if (st.isLooping) { @@ -24423,6 +24519,7 @@ const App = () => { if (selRight > selLeft && updatedTime >= selRight) { if (hasAnySolo || selectionMode === 'local') { fxRtWrapRestart = true; // V38: loop wrap restart + try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {} fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); startOffsetTimeRef.current = selLeft; @@ -24437,6 +24534,7 @@ const App = () => { setIsPlaying(true); } else { fxRtWrapRestart = true; // V38: loop wrap restart + try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {} fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); startOffsetTimeRef.current = selLeft; @@ -24460,6 +24558,7 @@ const App = () => { } if (isLoopingSelection) { fxRtWrapRestart = true; // V38: loop wrap restart + try { _dbgLog('wrap_info', 'solo=' + (hasAnySolo ? 1 : 0) + ' local=' + (selectionMode === 'local' ? 1 : 0) + ' tracks=' + (tracks ? tracks.length : -1) + ' nl=' + nativeLoopSrcsRef.current.size); } catch (e) {} fxRtKeepForRestart = true; // restart/resume — giữ session FX realtime stopAllPlayback(); startOffsetTimeRef.current = 0; @@ -25321,6 +25420,14 @@ const App = () => { setTimeout(function () { if (epoch !== bridgeNoteEpochRef.current) return; if (!guardPlay()) return; + // D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track + // vua tro nen inaudible (solo track khac / mute) khong duoc phat ra. + const _list = (activeTracksRef.current && activeTracksRef.current.length) ? activeTracksRef.current : (tracksRef.current && tracksRef.current.length ? tracksRef.current : activeTracks); + const _trk = _list.find(t => t.id === trkId) || track; + if (!_trk || computeTrackAudibleGain(_list, _trk) <= 0) return; + // D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill + // dung note khi track thanh inaudible giua chung (Qin VST2 tremolo). + try { var _tps = timelinePitchesRef.current; (_tps[trkId] || (_tps[trkId] = new Set())).add(pitch || 60); } catch (e) {} var _rt = window.SonicMidiRouter; try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: trkId, pitch: pitch || 60, velocity: velocity || 0.8, percussion: isPerc }); } catch (e) { console.warn('[Midi] pushEvent error:', e && e.message); } }, delayMs); @@ -25328,6 +25435,8 @@ const App = () => { if (epoch !== bridgeNoteEpochRef.current) return; if (!guardPlay()) return; try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: trkId, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {} + // D18c: xoa khoi per-track set (note da tat - khong con can kill). + try { var _tps2 = timelinePitchesRef.current; if (_tps2[trkId]) _tps2[trkId].delete(pitch || 60); } catch (e) {} }, delayMs, durMs || 1000); return; } @@ -25344,12 +25453,14 @@ 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) {} + // D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua + // tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap + // trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu. + const _wrapRestart = fxRtWrapRestart; + const _wrapMode = _wrapRestart ? 'loop' : 'play'; fxRtWrapRestart = false; // V38 + try { if (!_wrapRestart && 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; @@ -25852,12 +25963,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) {} + // D18: piano-roll wrap (fxRtWrapRestart set o loop restart) — KHONG flush + // ring + clear_queue mode 'loop' (giu queue + wrapFade) nhu main path; truoc + // day luon mode 'play' → wipe + re-buffer moi pass = giat. KHONG reset flag + // (startSubTabPlayback goi sau van can doc de clear dung mode). + try { if (!fxRtWrapRestart && window.BridgeAudioNode && window.BridgeAudioNode.flush) window.BridgeAudioNode.flush(); } catch (e) {} + try { if (fxRt && fxRt.node && fxRt.node.port) fxRt.node.port.postMessage({ type: 'clear_queue', mode: fxRtWrapRestart ? 'loop' : '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). @@ -26057,18 +26168,11 @@ 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 + // D14: bỏ dip muteGain tại wrap (loop) — 5ms master→0.0001 mỗi vòng = + // âm beat 0:0 nhỏ hơn hẳn âm cuối pass (bug seam volume). Click seam giờ + // xử bằng release mượt (CC123, không CC120 hard-cut — D15) + worklet + // WRAP_FADE 256 (FX path) → không cần che bằng dropout master. + _dbgLog('stop_start', 'fade=' + !!fade + ' ' + _dbgBridgeGain()); isPlayingRef.current = false; // Gap 7: xoa automation schedule — tra gain master ve 1.0 (fader rieng). @@ -26100,6 +26204,7 @@ const App = () => { // cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref). bridgeNoteEpochRef.current++; try { clearAllPendingNoteOffs(); } catch (e) {} + try { timelinePitchesRef.current = {}; } catch (e) {} // D18c: moi note timeline da tat theo transport stop _cc11Epoch++; // CC11 timers lan play cu vo hieu (da clear o tren) try { clearAllCC11Timers(); } catch (e) {} try { if (subTabsRef.current) subTabsRef.current = subTabsRef.current.map(s => ({ ...s, isPlaying: false })); } catch (e) {} @@ -26189,22 +26294,36 @@ const App = () => { 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). + // D17: wrap (loop restart) — BỎ transport STOP. C++ STOP làm allNotesOff + // + CC64=0 trên cả 16 channel → cắt mọi note (kể cả note ngân qua seam) + // + có thể stall audio loop ~20ms (dbg.log: wrap play_start→first_audio + // δ17-32ms vs restart thường δ3-5ms) = "reload" tại beat 0:0. CC123 (D15) + // đã lo release JS-side; PLAY vẫn reset playhead (playheadSamples chỉ đổi + // qua transport); transportStopped giữ false → note-on pass mới không bị + // drop (V8 guard). Stop/seek thật vẫn gửi STOP. + const _wrapNoStop = !!(fxRtWrapRestart && !stale); if (window.NativeBridgeService && window.NativeBridgeService.isBridgeConnected) { - try { window.NativeBridgeService.transport('stop'); } catch (e) {} - _dbgLog('stop_transport', 'epoch=' + stopEpoch); + if (!_wrapNoStop) { try { window.NativeBridgeService.transport('stop'); } catch (e) {} } + _dbgLog('stop_transport', 'epoch=' + stopEpoch + (_wrapNoStop ? ' wrap-skip' : '')); // V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay. + // D15: wrap (loop) → chỉ CC123 (all-notes-off, release mượt); bỏ CC120 + // (all-sound-off, hard cut → click + âm beat 0:0 tụt). Release chồng + // lên re-noteon pass mới → seam như cùng khuôn nhạc. Stop thật giữ CC120. try { + const _wrapRelease = !!(fxRtWrapRestart && !stale); 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); + if (!_wrapRelease) 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); } + // D18: wrap (_wrapNoStop) — KHÔNG flush (giữ dòng liên tục qua seam); + // flush ở đây + ở startTrackPlayback = ring rỗng → gap giật beat 0:0. + if (!_wrapNoStop) { 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 @@ -34001,7 +34120,38 @@ STRICT CONSTRAINTS: }, /*#__PURE__*/React.createElement("i", { "data-lucide": "trash-2", className: "w-3 h-3" - }))))), /*#__PURE__*/React.createElement("div", { + }))))), (function () { + const _ls = window.__trackLoadState && window.__trackLoadState[track.id]; + if (_ls && _ls.state === 'loading') { + return /*#__PURE__*/React.createElement("div", { + className: "flex items-center gap-1.5 mt-0.5", + title: "Đang nạp instrument vào bridge — track sẽ phát sau khi load xong" + }, /*#__PURE__*/React.createElement("div", { + className: "flex-1 h-1 rounded bg-zinc-800 overflow-hidden" + }, /*#__PURE__*/React.createElement("div", { + className: "h-full rounded sf-ruler-bar", + style: { + background: '#06b6d4', + width: '45%' + } + })), /*#__PURE__*/React.createElement("span", { + className: "text-[9px] text-cyan-400 font-mono shrink-0" + }, "Loading…")); + } + if (_ls && _ls.state === 'failed') { + return /*#__PURE__*/React.createElement("div", { + className: "mt-0.5 text-[9px] text-red-500 font-mono" + }, "Load failed"); + } + if (_ls && _ls.state === 'done' && _ls.doneTs && Date.now() - _ls.doneTs < 2500) { + return /*#__PURE__*/React.createElement("div", { + className: "mt-0.5 h-1 rounded bg-green-600/30 overflow-hidden" + }, /*#__PURE__*/React.createElement("div", { + className: "h-full bg-green-500 sf-ruler-done" + })); + } + return null; + })(), /*#__PURE__*/React.createElement("div", { className: "flex flex-col gap-0.5 text-xs", onClick: e => e.stopPropagation() }, /*#__PURE__*/React.createElement("div", { diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 2289a04..4b7ed5c 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -339,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=202608281631`);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=202608290928`);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 @@ -416,7 +416,7 @@ if(!_fxStopNode)fxRtApplyPdc();if(sid){try{window.SonicAPI.fxRealtimeStop({sessi // ≈ 1792) + FILL buffer 16 block (4096) = 23 block ≈ 5888 (~123ms @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=5888;const FX_RT_START_EST_MS=450;// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr +const FX_RT_ROUNDTRIP_SAMPLES=7936;const FX_RT_START_EST_MS=450;// G3: anchor processed — playhead = startOffset + (processed - anchor - latency)/sr // (da tru latency -> bam vi tri mau NGHE DUOC; pre-roll tu giu tai startOffset — // thay freeze/snap V40). Anchor moi play/wrap restart qua __fxRtPlayheadFreeze // (moi play/resume path deu goi); cold start (chua active) -> null -> WS message @@ -669,7 +669,7 @@ sfToDelete&&React.createElement('div',{className:'fixed inset-0 z-[60] flex item // V9: track.plugin_path/cache path có thể là plugin KHÁC instrumentId (VD // WaveObserver trong khi track là Nexus) → basename không chứa tên id → trả // null để engine resolve theo name / listPlugins — tránh LOAD nhầm VSTi. -const _bridgeSafeVstPath=(path,instrumentId)=>{if(!path||!instrumentId)return path;const base=String(path).split(/[\\/]/).pop().toLowerCase().replace(/\s+/g,'');const want=String(instrumentId).toLowerCase().replace(/\s+/g,'');return base.indexOf(want)!==-1?path:null;};const loadVstToBridge=async track=>{if(!window.NativeBridgeService||!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return false;const instrumentId=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return false;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};if(!window.__bridgeLoadedPaths)window.__bridgeLoadedPaths={};const tId=track.id||track.trackId;const bch=window.SonicMidiRouter.allocateChannel(tId,false);try{// V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename — +const _bridgeSafeVstPath=(path,instrumentId)=>{if(!path||!instrumentId)return path;const base=String(path).split(/[\\/]/).pop().toLowerCase().replace(/\s+/g,'');const want=String(instrumentId).toLowerCase().replace(/\s+/g,'');return base.indexOf(want)!==-1?path:null;};const loadVstToBridge=async track=>{if(!window.NativeBridgeService||!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return false;const instrumentId=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return false;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};if(!window.__bridgeLoadedPaths)window.__bridgeLoadedPaths={};const tId=track.id||track.trackId;const bch=window.SonicMidiRouter.allocateChannel(tId,false);markTrackLoad(tId,{state:'loading',channel:bch});try{// V8 bug 5: engine _resolve_bridge_asset chi match FILE theo filename — // KHONG walk folder VST3 ("X.vst3/X.vst3.dll") → 404. Dung path da luu // (synth_engine.plugin_path) hoac cache tu listPlugins. // VST2 (.dll) scan type 'VST2' - khong hardcode VST3 (se LOAD sai loai). @@ -679,20 +679,25 @@ knownPath=_bridgeSafeVstPath(knownPath,instrumentId);// V10: C++ đã load/resto // đó load thẳng; C++ double-load guard skip nên không LOAD trùng. if(!knownPath&&window.__bridgeLoadedState){const lc=window.__bridgeLoadedState.find(x=>x.channel===bch&&_bridgeSafeVstPath(x.path,instrumentId));if(lc)knownPath=lc.path;}// Đã load ĐÚNG path vào channel này rồi → skip (không LOAD trùng). Bỏ // early-return mù theo flag — flag sai khi C++ restore instrument khác. -if(knownPath&&window.__bridgeLoadedPaths[bch]===knownPath)return true;// Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS). -if(knownPath){const ok=await window.NativeBridgeService.loadInstrument(knownPath,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=knownPath;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=knownPath;}console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',knownPath);return!!ok;}// Không có path → nhờ engine resolve theo name (POST). Fail (auth/CORS) → +if(knownPath&&window.__bridgeLoadedPaths[bch]===knownPath){markTrackLoad(tId,{state:'done'});return true;}// Có path đáng tin → load thẳng qua Tauri invoke (không cần POST/auth/CORS). +if(knownPath){const ok=await window.NativeBridgeService.loadInstrument(knownPath,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=knownPath;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=knownPath;}if(!ok)markTrackLoad(tId,{state:'failed'});console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',knownPath);return!!ok;}// Không có path → nhờ engine resolve theo name (POST). Fail (auth/CORS) → // fallback listPlugins bên dưới. -const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:null,instrumentType:btype,channel:bch});let p=r&&r.path?r.path:null;if(!p&&window.SonicAPI.listPlugins){try{const pl=await window.SonicAPI.listPlugins();const v=(pl&&pl.vst_instruments||[]).find(x=>x.id===instrumentId||x.name===instrumentId);if(v&&v.path){p=v.path;if(v&&v.type)btype=v.type;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=v.path;}}catch(e2){console.warn('[Bridge] listPlugins fallback fail:',e2);}}if(!p)return false;const ok=await window.NativeBridgeService.loadInstrument(p,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=p;}console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] loadVstToBridge fail:',track.id,e);return false;}};// GM font dự phòng khi soundfont track không có trong thư viện (VD id +const r=await window.SonicAPI.bridgeLoad({name:instrumentId,path:null,instrumentType:btype,channel:bch});let p=r&&r.path?r.path:null;if(!p&&window.SonicAPI.listPlugins){try{const pl=await window.SonicAPI.listPlugins();const v=(pl&&pl.vst_instruments||[]).find(x=>x.id===instrumentId||x.name===instrumentId);if(v&&v.path){p=v.path;if(v&&v.type)btype=v.type;if(!window.__bridgePluginPaths)window.__bridgePluginPaths={};window.__bridgePluginPaths[instrumentId]=v.path;}}catch(e2){console.warn('[Bridge] listPlugins fallback fail:',e2);}}if(!p){markTrackLoad(tId,{state:'failed'});return false;}const ok=await window.NativeBridgeService.loadInstrument(p,btype,bch);if(ok){window.__bridgeLoadedChannels[bch]=true;window.__bridgeLoadedPaths[bch]=p;}if(!ok)markTrackLoad(tId,{state:'failed'});console.log('[Bridge] auto-load track',track.id,'->',btype,'ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] loadVstToBridge fail:',track.id,e);markTrackLoad(tId,{state:'failed'});return false;}};// GM font dự phòng khi soundfont track không có trong thư viện (VD id // sf_generaluser_gs do AI sinh nhưng user chưa tải font) — ưu tiên font GM // đầy đủ patch, fallback font đầu tiên liệt kê được. const pickFallbackSoundfont=list=>{if(!list||!list.length)return null;const hay=s=>(String(s.id||'')+' '+String(s.file||'')+' '+String(s.name||'')).toLowerCase();for(const w of['sgm','generaluser','tgsf21x','timgm']){const hit=list.find(s=>hay(s).indexOf(w)!==-1);if(hit)return hit;}return list[0];};// D12: load track instrument (VST *hoặc* SF2) vào bridge. preloadTrackInstruments // trước đây chỉ load VST → mở project có SF2 track + bridge active → MIDI tới // channel bridge rỗng → C++ silent (không qua mastering chain native). -const loadAnyTrackToBridge=async track=>{if(!window.NativeBridgeService||!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return false;const inst=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!inst)return false;if(typeof inst==='string'&&inst.startsWith('sf_')){const se=track.synth_engine||{};const bank=se.soundfont_bank!==undefined?se.soundfont_bank:track.soundfont_bank!==undefined?track.soundfont_bank:0;const bch=window.SonicMidiRouter.allocateChannel(track.id||track.trackId,bank===128);let pl=null;try{const sfId=inst.replace('sf_','');let bpath=null;try{pl=await window.SonicAPI.listPlugins();const s0=(pl&&pl.soundfonts||[]).find(s=>String(s.id).replace('sf_','')===sfId);bpath=s0&&(s0.file||s0.path);}catch(e){}let r=await window.SonicAPI.bridgeLoad({name:inst,path:bpath||null,instrumentType:'SF2',channel:bch});let p=r&&r.path?r.path:null;if(!p){// FALLBACK: soundfont id KHÔNG tồn tại trong thư viện (AI prompt mặc +const loadAnyTrackToBridge=async track=>{if(!window.NativeBridgeService||!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return false;const inst=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!inst)return false;if(typeof inst==='string'&&inst.startsWith('sf_')){const se=track.synth_engine||{};const bank=se.soundfont_bank!==undefined?se.soundfont_bank:track.soundfont_bank!==undefined?track.soundfont_bank:0;const bch=window.SonicMidiRouter.allocateChannel(track.id||track.trackId,bank===128);markTrackLoad(track.id||track.trackId,{state:'loading',channel:bch});let pl=null;try{const sfId=inst.replace('sf_','');let bpath=null;try{pl=await window.SonicAPI.listPlugins();const s0=(pl&&pl.soundfonts||[]).find(s=>String(s.id).replace('sf_','')===sfId);bpath=s0&&(s0.file||s0.path);}catch(e){}let r=await window.SonicAPI.bridgeLoad({name:inst,path:bpath||null,instrumentType:'SF2',channel:bch});let p=r&&r.path?r.path:null;if(!p){// FALLBACK: soundfont id KHÔNG tồn tại trong thư viện (AI prompt mặc // định generaluser_gs nhưng user chưa tải font) → track câm im lặng. // Load GM font có sẵn (SGM/GeneralUser/tgsf21x) vào cùng channel — // __ensureBridgeProgram vẫn gửi CC0/CC32/PROGRAM → GM patch đúng. -const fb=pickFallbackSoundfont(pl&&pl.soundfonts);if(fb){console.warn('[Bridge] SF2 not found:',inst,'→ fallback',fb.file||fb.id,'@ch',bch);const r2=await window.SonicAPI.bridgeLoad({name:inst,path:fb.file||fb.path||null,instrumentType:'SF2',channel:bch});p=r2&&r2.path?r2.path:null;}}if(!p){console.warn('[Bridge] SF2 resolve fail:',track.id,inst);return false;}const ok=await window.NativeBridgeService.loadInstrument(p,'SF2',bch);if(ok){if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};window.__bridgeLoadedChannels[bch]=true;if(!window.__bridgeLoadedPaths)window.__bridgeLoadedPaths={};window.__bridgeLoadedPaths[bch]=p;}console.log('[Bridge] preload SF2 track',track.id,'-> ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] preload SF2 fail:',track.id,e);return false;}}try{return await loadVstToBridge(track);}catch(e){return false;}};const ensureAndOpenVstGui=async track=>{const trackId=track.trackId||track.id;const instrumentId=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};let bch=window.SonicMidiRouter&&window.SonicMidiRouter.allocateChannel?window.SonicMidiRouter.allocateChannel(trackId,false):0;// LAN/standalone (browser không Tauri): NativeBridgeService chỉ log, không +const fb=pickFallbackSoundfont(pl&&pl.soundfonts);if(fb){console.warn('[Bridge] SF2 not found:',inst,'→ fallback',fb.file||fb.id,'@ch',bch);const r2=await window.SonicAPI.bridgeLoad({name:inst,path:fb.file||fb.path||null,instrumentType:'SF2',channel:bch});p=r2&&r2.path?r2.path:null;}}if(!p){console.warn('[Bridge] SF2 resolve fail:',track.id,inst);markTrackLoad(track.id||track.trackId,{state:'failed'});return false;}const ok=await window.NativeBridgeService.loadInstrument(p,'SF2',bch);if(ok){if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};window.__bridgeLoadedChannels[bch]=true;if(!window.__bridgeLoadedPaths)window.__bridgeLoadedPaths={};window.__bridgeLoadedPaths[bch]=p;}if(!ok)markTrackLoad(track.id||track.trackId,{state:'failed'});console.log('[Bridge] preload SF2 track',track.id,'-> ch',bch,ok?'OK':'FAIL',p);return!!ok;}catch(e){console.warn('[Bridge] preload SF2 fail:',track.id,e);markTrackLoad(track.id||track.trackId,{state:'failed'});return false;}}try{return await loadVstToBridge(track);}catch(e){return false;}};// D16: per-track loading ruler (TCP). window.__trackLoadState[trackId] = +// { state:'pending'|'loading'|'done'|'failed', channel, startedTs, doneTs }. +// Poller trong App (loadTick) chuyển loading→done khi channel xuất hiện trong +// loaded_channels (queryStatus) — đúng cho cả C++ restore (bridge_state.json, +// chạy từ spawn) lẫn JS load — không đánh dấu done sớm khi C++ guard skip. +const markTrackLoad=(trackId,patch)=>{if(!trackId)return;if(!window.__trackLoadState)window.__trackLoadState={};const cur=window.__trackLoadState[trackId]||{state:'pending',channel:null,startedTs:0,doneTs:0};const next=Object.assign({},cur,patch);if(patch.state==='loading'&&cur.state!=='loading')next.startedTs=Date.now();if(patch.state==='done'&&!patch.doneTs)next.doneTs=Date.now();window.__trackLoadState[trackId]=next;if(window.__onTrackLoadState){try{window.__onTrackLoadState(trackId);}catch(e){}}};if(typeof document!=='undefined'&&!document.getElementById('sf-ruler-style')){const _st=document.createElement('style');_st.id='sf-ruler-style';_st.textContent='@keyframes sf-ruler { 0% { transform: translateX(-110%); } 100% { transform: translateX(340%); } } .sf-ruler-bar { animation: sf-ruler 1.1s ease-in-out infinite; } @keyframes sf-ruler-done { 0% { opacity: 1; } 100% { opacity: 0; } } .sf-ruler-done { animation: sf-ruler-done 1.4s ease-out forwards; }';document.head.appendChild(_st);}const ensureAndOpenVstGui=async track=>{const trackId=track.trackId||track.id;const instrumentId=track.instrumentId||track.synth_engine&&track.synth_engine.plugin_id;if(!instrumentId||typeof instrumentId==='string'&&instrumentId.startsWith('sf_'))return;if(!window.__bridgeLoadedChannels)window.__bridgeLoadedChannels={};let bch=window.SonicMidiRouter&&window.SonicMidiRouter.allocateChannel?window.SonicMidiRouter.allocateChannel(trackId,false):0;// LAN/standalone (browser không Tauri): NativeBridgeService chỉ log, không // invoke được open_vst_gui → nhờ engine đẩy LOAD+OPEN_GUI vào SHM qua route // /bridge/gui (E6). GUI hiện trên desktop máy chạy server. if(!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive()||!window.NativeBridgeService){try{if(!window.SonicAPI||!window.SonicAPI.bridgeOpenGui)return;const r=await window.SonicAPI.bridgeOpenGui({name:instrumentId,path:null,instrumentType:null,channel:bch});if(r&&r.success)console.log('[Bridge] GUI open via API',instrumentId,'ch',bch,r.path);else console.warn('[Bridge] GUI via API response:',r);}catch(e){console.warn('[Bridge] GUI via API fail:',trackId,e);}return;}// V9: load (nếu chưa có ĐÚNG instrument) rồi CHỈ mở GUI khi load OK — không @@ -735,8 +740,15 @@ const _wheelAcc=new Map();const wheelSteps=(key,deltaY,deltaMode,pxPerStep=100)= const c=(_wheelAcc.get(key)||0)-norm;const steps=Math.trunc(c/pxPerStep);_wheelAcc.set(key,c-steps*pxPerStep);return steps;};// Module-level so both ProfileModal (open project) and App (auto-restore) can // warm the FluidSynth font cache for each track's instrument. This only loads // the soundfonts; the per-track channel re-selection happens at note time. -const preloadTrackInstruments=async tracks=>{const list=tracks||[];if(window.SonicSF&&window.SonicSF.selectInstrument){for(let i=0;i{if(!isOpen)return null;const[activeTab,setActiveTab]=useState('account');const[profile,setProfile]=useState(null);const[oldPassword,setOldPassword]=useState('');const[newPassword,setNewPassword]=useState('');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);const[dragOfs,setDragOfs]=useState({x:0,y:0});const dragRef=useRef({active:false,startX:0,startY:0,ofsX:0,ofsY:0});// Projects list state +const preloadTrackInstruments=async tracks=>{const list=tracks||[];// D13: bridge load TRƯỚC vòng WASM — vòng selectInstrument (parse font WASM +// từng track, có thể ~20s) trước đây chặn vòng bridge → user bấm play khi +// app vừa load: NOTE_ON vào channel bridge chưa có instrument → im lặng tới +// khi bridge load xong (~27s từ bootstrap — dbg.log play_first_audible). +if(window.NativeBridgeService&&window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){// D16b: pre-mark TẤT CẢ track có instrument thành loading TRƯỚC loop — loop +// nối tiếp (C++ g_loadMutex) khiến track 2-9 không có ruler tới khi track 1 +// xong → UI lâu không có loading item. markTrackLoad channel=null hiện ruler +// ngay; loadAnyTrackToBridge set channel khi tới lượt; poller chuyển done. +for(let i=0;i{if(!isOpen)return null;const[activeTab,setActiveTab]=useState('account');const[profile,setProfile]=useState(null);const[oldPassword,setOldPassword]=useState('');const[newPassword,setNewPassword]=useState('');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);const[dragOfs,setDragOfs]=useState({x:0,y:0});const dragRef=useRef({active:false,startX:0,startY:0,ofsX:0,ofsY:0});// Projects list state const[projectsList,setProjectsList]=useState([]);const[loadingProjects,setLoadingProjects]=useState(false);// Files list state const[filesList,setFilesList]=useState([]);const[loadingFiles,setLoadingFiles]=useState(false);// Confirmation modal state const[confirmModal,setConfirmModal]=useState(null);// Backup state @@ -1282,7 +1294,9 @@ const cc=msg.data[1];const val=msg.data[2];if(window.SonicSF&&window.SonicSF.con const lsb=msg.data[1];const msb=msg.data[2];const bendVal=msb<<7|lsb;if(window.SonicSF&&window.SonicSF.pitchBend){var pbTracks=activeTracksRef.current||[];var hasArmedPB=pbTracks.some(function(t){return t.isArmed;});if(hasArmedPB){pbTracks.forEach(function(pt){if(!pt.isArmed)return;var ptCh=assignTrackMidiChannel(pt,pbTracks);window.SonicSF.pitchBend(ptCh,bendVal);});}else{window.SonicSF.pitchBend(midiCh,bendVal);}}}// Forward to active MIDI recorders if(activeMIDIRecordersRef.current){for(let trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){rec.handleMIDIMessage(msg,input.id);}}}};}for(let input of access.inputs.values()){inputs.push(input);attachMidiHandler(input);}setMidiDevices(inputs);access.onstatechange=()=>{const inputs=[];for(let input of access.inputs.values()){inputs.push(input);// Re-attach handler to ensure new devices get it if(!input.onmidimessage){attachMidiHandler(input);}}setMidiDevices(inputs);};}).catch(err=>console.log('MIDI access error:',err));}},[]);const[showAIConfig,setShowAIConfig]=useState(false);const[recordingState,setRecordingState]=useState('IDLE');// 'IDLE' | 'COUNT_IN' | 'RECORDING' -const[recTempMidiNotes,setRecTempMidiNotes]=useState([]);const[recTempAudioBuffer,setRecTempAudioBuffer]=useState(null);const[recStartTimelineTime,setRecStartTimelineTime]=useState(0);const[canvasRedrawCount,setCanvasRedrawCount]=useState(0);const[lastMidiNote,setLastMidiNote]=useState(null);const lastMidiNoteRef=useRef(null);const activeMIDIRecordersRef=useRef({});const activeAudioRecordersRef=useRef({});const pianoRollRecorderRef=useRef(null);const activeMidiPitchesRef=useRef(new Set());const[activeMidiPitches,setActiveMidiPitches]=useState(new Set());const recordingPCMDataRef=useRef({});const recordingStartTimeRef=useRef(0);const recordingSyncRef=useRef(null);const recordingStateRef=useRef(recordingState);recordingStateRef.current=recordingState;const lastTempCompileTimeRef=useRef(0);const nextMetronomeBeatRef=useRef(0);const[showExportPanel,setShowExportPanel]=useState(false);const[showExportProgress,setShowExportProgress]=useState(false);// modal tiến trình export RIÊNG +const[recTempMidiNotes,setRecTempMidiNotes]=useState([]);const[recTempAudioBuffer,setRecTempAudioBuffer]=useState(null);const[recStartTimelineTime,setRecStartTimelineTime]=useState(0);const[canvasRedrawCount,setCanvasRedrawCount]=useState(0);const[lastMidiNote,setLastMidiNote]=useState(null);const lastMidiNoteRef=useRef(null);const activeMIDIRecordersRef=useRef({});const activeAudioRecordersRef=useRef({});const pianoRollRecorderRef=useRef(null);const activeMidiPitchesRef=useRef(new Set());const timelinePitchesRef=useRef({});// D18c: per-track Set timeline MIDI pitches dang keu (NOTE_ON fired) +const tracksRef=useRef([]);// D18c: tracks state moi nhat (guard mute/solo fire-time) +const[activeMidiPitches,setActiveMidiPitches]=useState(new Set());const recordingPCMDataRef=useRef({});const recordingStartTimeRef=useRef(0);const recordingSyncRef=useRef(null);const recordingStateRef=useRef(recordingState);recordingStateRef.current=recordingState;const lastTempCompileTimeRef=useRef(0);const nextMetronomeBeatRef=useRef(0);const[showExportPanel,setShowExportPanel]=useState(false);const[showExportProgress,setShowExportProgress]=useState(false);// modal tiến trình export RIÊNG React.useEffect(()=>{if(showExportPanel)setTimeout(()=>{try{window.lucide&&window.lucide.createIcons();}catch(e){}},80);},[showExportPanel]);// ── Virtual MIDI Keyboard (Phase 5) ───────────────────────────────────── const[vkbOpen,setVkbOpen]=useState(false);const vkbOpenRef=useRef(false);vkbOpenRef.current=vkbOpen;const[chordsPanelOpen,setChordsPanelOpen]=useState(false);const vkbHeldRef=useRef(new Set());const[vkbSettings,setVkbSettings]=useState(function(){try{const saved=JSON.parse(localStorage.getItem('sonic_vkb_settings_v1')||'null');if(saved&&typeof saved==='object')return Object.assign({x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'},saved);}catch(e){}return{x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'};}());const vkbSettingsRef=useRef(vkbSettings);vkbSettingsRef.current=vkbSettings;const vkbNoteOnRef=useRef(null);const vkbNoteOffRef=useRef(null);const vkbUpdateSettings=patch=>{setVkbSettings(prev=>{const next=Object.assign({},prev,patch);try{localStorage.setItem('sonic_vkb_settings_v1',JSON.stringify(next));}catch(e){}return next;});};// Global keydown: F2 toggle + QWERTY notes khi VKB mở. capture:true + đăng ký // TRƯỚC handler transport (~19402) → stopPropagation chặn phím tắt trùng @@ -1313,7 +1327,14 @@ const applyAllTrackMuteSolo=(trackId,patch)=>{const list=activeTracksRef.current Object.keys(activeTrackNodesRef.current).forEach(function(k){if(k.endsWith('_sub_'+t.id)){const sn=activeTrackNodesRef.current[k];if(sn&&sn.gainNode)setTrackNodeGain(sn,audibleGain);}});if(node&&node.sfEntry){try{node.sfEntry.gain.setTargetAtTime(audibleGain,getAudioContext().currentTime,0.02);}catch(e){}}// MIDI/bridge tracks: CC7 (channel volume) realtime — even for notes // already sounding. Bridge active → qua SonicMidiRouter (bridge); bridge // down → SonicSF (WASM). CC7 = VOLUME của track (không chỉ 100/0). -if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,effective,audible);}catch(e){}}if(audible&&!wasAudible)becameAudible=true;trackAudibleRef.current[t.id]=audible;});// A track that just became audible may have had its audio sources dropped +if(isMidiSoundTrack(t)){try{sendMidiVolumeCC7(t,effective,audible);}catch(e){}}// D18: track vừa MẤT tiếng (mute/solo) — dừng note đang giữ ngay. CC7=0 +// (sendMidiVolumeCC7) không ăn với VST như Qin (bỏ qua channel volume) +// → note kêu tiếp (tremolo) khi solo track khác. NOTE_OFF từng pitch đang +// giữ + CC123/CC120 all-notes-off trên channel track đó. +if(!audible&&wasAudible&&isMidiSoundTrack(t)){try{const _isPerc=!!(t.synth_engine&&t.synth_engine.soundfont_bank===128)||!!t.is_percussion;const _held=activeMidiPitchesRef.current||new Set();// D18c: timeline MIDI notes (per-track) dang keu - NOTE_OFF tung pitch; +// truoc day chi kill preview keyboard (activeMidiPitchesRef) -> note +// timeline (Qin VST2) cu ngan/loop (tremolo) khi solo track khac. +const _tSet=(timelinePitchesRef.current||{})[t.id];if(_tSet&&_tSet.size){_tSet.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});}_held.forEach(function(_p){window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:_p,velocity:0,percussion:_isPerc,live:true});});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:123,velocity:0,data2:0,percussion:_isPerc});window.SonicMidiRouter.pushEvent({cmd:'CC',channel:t.id,pitch:120,velocity:0,data2:0,percussion:_isPerc});if(heldMidiNotesRef.current)delete heldMidiNotesRef.current[t.id];if(timelinePitchesRef.current)delete timelinePitchesRef.current[t.id];}catch(e){}}if(audible&&!wasAudible)becameAudible=true;trackAudibleRef.current[t.id]=audible;});// A track that just became audible may have had its audio sources dropped // by a loop restart while it was inaudible (startTrackPlayback skips // muted/soloed tracks, so unmute alone cannot resurrect them). Re-schedule // from the current playhead — the same approach toggleTrackSoloEvaluate @@ -1339,7 +1360,10 @@ useEffect(()=>{try{if(exportSettings.rememberExportDir&&exportSettings.exportDir const[stretchedClip,setStretchedClip]=useState(null);// { trackId, clipId, originalDuration, startTime, originalSpeed, beforeSnap } const[draggedSectionItem,setDraggedSectionItem]=useState(null);const[resizedSectionItem,setResizedSectionItem]=useState(null);const[editingTrackName,setEditingTrackName]=useState(null);// trackId being edited const[editingClipName,setEditingClipName]=useState(null);// { trackId, clipId } -const[editNameInput,setEditNameInput]=useState('');// ── Context Menu & Clipboard ── +const[editNameInput,setEditNameInput]=useState('');// D16: per-track loading ruler — ticker buộc re-render khi có track đang +// loading; poller queryStatus chuyển loading→done khi channel sẵn sàng +// (đúng cho cả C++ restore từ spawn lẫn JS load — không phụ thuộc thứ tự). +const[loadTick,setLoadTick]=useState(0);useEffect(()=>{window.__onTrackLoadState=()=>setLoadTick(t=>t+1);const iv=setInterval(async()=>{try{if(!window.NativeBridgeService||!window.SonicMidiRouter||!window.SonicMidiRouter.isBridgeActive())return;const m=window.__trackLoadState||{};const loadingKeys=Object.keys(m).filter(k=>m[k].state==='loading');if(!loadingKeys.length)return;const st=await window.NativeBridgeService.queryStatus();const lc=st&&Array.isArray(st.loaded_channels)?st.loaded_channels:null;let changed=false;loadingKeys.forEach(k=>{const s=m[k];if(lc&&s.channel!=null&&lc.some(x=>x.channel===s.channel)){m[k]=Object.assign({},s,{state:'done',doneTs:Date.now()});changed=true;}else if(!s.startedTs||Date.now()-s.startedTs>90000){m[k]=Object.assign({},s,{state:'failed'});changed=true;}});if(changed)setLoadTick(t=>t+1);}catch(e){}},400);return()=>{delete window.__onTrackLoadState;clearInterval(iv);};},[]);// ── Context Menu & Clipboard ── const[contextMenu,setContextMenu]=useState(null);// { x, y, trackId } // Gợi ý động PIANO ROLL trên status bar (user 09:40) — piano roll set qua // window.__setPrHint (mouse in/out + Shift/Ctrl state) @@ -1358,7 +1382,8 @@ const[activeTab,setActiveTab]=useState('main');const[subTabSelectedNodeTime,setS const[pianoRollClipboard,setPianoRollClipboard]=useState(null);// notes copy giữa các PIANO ROLL TAB const[sessionTabs,setSessionTabs]=useState([]);// [{id, name, tracks}, ...] const[tabContextMenu,setTabContextMenu]=useState(null);// { x, y, tabId, tabType } -const handleSetTabColor=(tabId,tabType,color)=>{if(tabType==='session'){setSessionTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));const tab=sessionTabs.find(s=>s.id===tabId);if(tab){setTracks(prev=>prev.map(t=>{if(!t.sections||t.sections.length===0)return t;return{...t,sections:t.sections.map(s=>{if(s.sectionId===tab.sectionId){return{...s,color:color};}return s;})};}));}}else{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));}};const activeTracks=useMemo(()=>{var st=sessionTabs.find(s=>s.id===activeTab);if(st)return st.tracks;var pr=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(pr&&pr.parent_tab_id&&pr.parent_tab_id.startsWith('session_')){var parentSt=sessionTabs.find(s=>s.id===pr.parent_tab_id);if(parentSt)return parentSt.tracks;}return tracks;},[activeTab,sessionTabs,subTabs,tracks]);const activeTracksRef=useRef([]);activeTracksRef.current=activeTracks;const sessionTabsRef=useRef(sessionTabs);sessionTabsRef.current=sessionTabs;// Vị trí MIDI item (trong context của piano roll) — dùng để đồng bộ playhead +const handleSetTabColor=(tabId,tabType,color)=>{if(tabType==='session'){setSessionTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));const tab=sessionTabs.find(s=>s.id===tabId);if(tab){setTracks(prev=>prev.map(t=>{if(!t.sections||t.sections.length===0)return t;return{...t,sections:t.sections.map(s=>{if(s.sectionId===tab.sectionId){return{...s,color:color};}return s;})};}));}}else{setSubTabs(prev=>prev.map(s=>s.id===tabId?{...s,color:color}:s));}};const activeTracks=useMemo(()=>{var st=sessionTabs.find(s=>s.id===activeTab);if(st)return st.tracks;var pr=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(pr&&pr.parent_tab_id&&pr.parent_tab_id.startsWith('session_')){var parentSt=sessionTabs.find(s=>s.id===pr.parent_tab_id);if(parentSt)return parentSt.tracks;}return tracks;},[activeTab,sessionTabs,subTabs,tracks]);const activeTracksRef=useRef([]);activeTracksRef.current=activeTracks;tracksRef.current=tracks;// D18c: sync tracks state moi nhat (guard fire-time) +const sessionTabsRef=useRef(sessionTabs);sessionTabsRef.current=sessionTabs;// Vị trí MIDI item (trong context của piano roll) — dùng để đồng bộ playhead // 2 chiều SECTION-TAB ↔ PIANO ROLL: item.startTime (local section / main) + // st.currentTime (vị trí TRONG item). Dùng REFS (activeTracksRef/ // sessionTabsRef) — an toàn với closure cũ (updatePlayhead/rAF loop). @@ -1739,11 +1764,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 -// 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 +// D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua +// tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap +// trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu. +const _wrapRestart=fxRtWrapRestart;const _wrapMode=_wrapRestart?'loop':'play';fxRtWrapRestart=false;// V38 +try{if(!_wrapRestart&&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);}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;i=end){fxRtWrapRestart=true;// V38: loop wrap restart -fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}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){fxRtWrapRestart=true;// V38: loop wrap restart -fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}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;// G3: playhead display engine-derived khi FX active (wrap/loop trigger giu @@ -1849,13 +1875,13 @@ if(st.type==='PIANO_ROLL')schedulePianoRollMidi(st,0);startSubTabPlayback(st,0); const _playheadSec=fxRt&&fxRt.active&&typeof fxRt.processedSamples==='number'&&fxRt.processedAnchor!=null?Math.max(startOffsetTimeRef.current,startOffsetTimeRef.current+(fxRt.processedSamples-fxRt.processedAnchor-(fxRt.latencyTotal||0))/(context.sampleRate||44100)):updatedTime;// 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'){fxRtWrapRestart=true;// V38: loop wrap restart -fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}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 +try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}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){fxRtWrapRestart=true;// V38: loop wrap restart -fxRtKeepForRestart=true;// restart/resume — giữ session FX realtime +try{_dbgLog('wrap_info','solo='+(hasAnySolo?1:0)+' local='+(selectionMode==='local'?1:0)+' tracks='+(tracks?tracks.length:-1)+' nl='+nativeLoopSrcsRef.current.size);}catch(e){}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(_playheadSec);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 @@ -1904,7 +1930,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=202608281631`);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=202608290928`);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 @@ -2026,15 +2052,21 @@ const guardPlay=startTime!=null?function(){return isPlayingRef.current||subTabsR const isPerc=!!(synthEngine&&synthEngine.soundfont_bank===128);// D8: track đổi instrument → gửi CC0/CC32 (bank THẬT từ synthEngine) + // program change qua bridge (A12) — helper dedupe theo track+bank+program. if(program!==undefined&&program!==null){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(trkId,program,synthEngine);}catch(e){}}// V11: epoch guard — hủy timer cũ khi stop/loop restart (xem ref). -const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;var _rt=window.SonicMidiRouter;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){console.warn('[Midi] pushEvent error:',e&&e.message);}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì +const epoch=bridgeNoteEpochRef.current;setTimeout(function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;// D18b: mute/solo guard tai thoi diem fire — note-on dang cho cua track +// vua tro nen inaudible (solo track khac / mute) khong duoc phat ra. +const _list=activeTracksRef.current&&activeTracksRef.current.length?activeTracksRef.current:tracksRef.current&&tracksRef.current.length?tracksRef.current:activeTracks;const _trk=_list.find(t=>t.id===trkId)||track;if(!_trk||computeTrackAudibleGain(_list,_trk)<=0)return;// D18c: track pitch dang keu (timeline) - de applyAllTrackMuteSolo kill +// dung note khi track thanh inaudible giua chung (Qin VST2 tremolo). +try{var _tps=timelinePitchesRef.current;(_tps[trkId]||(_tps[trkId]=new Set())).add(pitch||60);}catch(e){}var _rt=window.SonicMidiRouter;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:trkId,pitch:pitch||60,velocity:velocity||0.8,percussion:isPerc});}catch(e){console.warn('[Midi] pushEvent error:',e&&e.message);}},delayMs);scheduleNoteOff('B:'+trkId+':'+(pitch||60),function(){if(epoch!==bridgeNoteEpochRef.current)return;if(!guardPlay())return;try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:trkId,pitch:pitch||60,velocity:0,percussion:isPerc});}catch(e){}// D18c: xoa khoi per-track set (note da tat - khong con can kill). +try{var _tps2=timelinePitchesRef.current;if(_tps2[trkId])_tps2[trkId].delete(pitch||60);}catch(e){}},delayMs,durMs||1000);return;}// Native VSTi (standalone browser, bridge inactive) → midi-render thay vì // 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 -// 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 +// D18: loop WRAP (fxRtWrapRestart) — KHONG flush ring bridge: ring dang chua +// tail pass cu + block moi (bridge render lien tuc); flush tai seam = gap +// trong → giat beat 0:0. Play that van flush (V30) de bo block idle cu. +const _wrapRestart=fxRtWrapRestart;const _wrapMode=_wrapRestart?'loop':'play';fxRtWrapRestart=false;// V38 +try{if(!_wrapRestart&&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 @@ -2123,11 +2155,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 -// 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 → +// D18: piano-roll wrap (fxRtWrapRestart set o loop restart) — KHONG flush +// ring + clear_queue mode 'loop' (giu queue + wrapFade) nhu main path; truoc +// day luon mode 'play' → wipe + re-buffer moi pass = giat. KHONG reset flag +// (startSubTabPlayback goi sau van can doc de clear dung mode). +try{if(!fxRtWrapRestart&&window.BridgeAudioNode&&window.BridgeAudioNode.flush)window.BridgeAudioNode.flush();}catch(e){}try{if(fxRt&&fxRt.node&&fxRt.node.port)fxRt.node.port.postMessage({type:'clear_queue',mode:fxRtWrapRestart?'loop':'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 @@ -2159,12 +2191,10 @@ stopAllPlayback();const startOffset=st.currentTime||0;if(st.type==='PIANO_ROLL') 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);// 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 +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{// D14: bỏ dip muteGain tại wrap (loop) — 5ms master→0.0001 mỗi vòng = +// âm beat 0:0 nhỏ hơn hẳn âm cuối pass (bug seam volume). Click seam giờ +// xử bằng release mượt (CC123, không CC120 hard-cut — D15) + worklet +// WRAP_FADE 256 (FX path) → không cần che bằng dropout master. _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 @@ -2178,7 +2208,8 @@ try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}}e try{fxRtTrackStopAll();}catch(e){console.warn('[FX RT track] stop error:',e);}clearTimeout(fxRtWarmIdleTimer);fxRtWarmIdleTimer=setTimeout(()=>{fxRtWarmIdleTimer=null;try{fxRtStop();}catch(e){console.warn('[FX RT] idle stop error:',e);}},FX_RT_WARM_IDLE_MS);}_nativePlayEpoch++;// hủy mọi render/decode pending của lần play cũ // V11: hủy mọi note timer của lần play cũ (stop/seek/loop restart) — timer // cũ bắn vào vòng mới = NOTE_OFF cắt note đang kêu → crack (xem ref). -bridgeNoteEpochRef.current++;try{clearAllPendingNoteOffs();}catch(e){}_cc11Epoch++;// CC11 timers lan play cu vo hieu (da clear o tren) +bridgeNoteEpochRef.current++;try{clearAllPendingNoteOffs();}catch(e){}try{timelinePitchesRef.current={};}catch(e){}// D18c: moi note timeline da tat theo transport stop +_cc11Epoch++;// CC11 timers lan play cu vo hieu (da clear o tren) try{clearAllCC11Timers();}catch(e){}try{if(subTabsRef.current)subTabsRef.current=subTabsRef.current.map(s=>({...s,isPlaying:false}));}catch(e){}// Fade-out khi user nhấn Stop/Pause (fade=true): ramp gain track + bridge // 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). @@ -2205,10 +2236,22 @@ if(stale){if(_pendingFadeSession&&_pendingFadeSession.stopEpoch===stopEpoch){res // 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 +// D17: wrap (loop restart) — BỎ transport STOP. C++ STOP làm allNotesOff +// + CC64=0 trên cả 16 channel → cắt mọi note (kể cả note ngân qua seam) +// + có thể stall audio loop ~20ms (dbg.log: wrap play_start→first_audio +// δ17-32ms vs restart thường δ3-5ms) = "reload" tại beat 0:0. CC123 (D15) +// đã lo release JS-side; PLAY vẫn reset playhead (playheadSamples chỉ đổi +// qua transport); transportStopped giữ false → note-on pass mới không bị +// drop (V8 guard). Stop/seek thật vẫn gửi STOP. +const _wrapNoStop=!!(fxRtWrapRestart&&!stale);if(window.NativeBridgeService&&window.NativeBridgeService.isBridgeConnected){if(!_wrapNoStop){try{window.NativeBridgeService.transport('stop');}catch(e){}}_dbgLog('stop_transport','epoch='+stopEpoch+(_wrapNoStop?' wrap-skip':''));// V39: MIDI panic CC123/CC120 tai wrap — VSTi note treo (khong nhan note-off) phai tat ngay. +// D15: wrap (loop) → chỉ CC123 (all-notes-off, release mượt); bỏ CC120 +// (all-sound-off, hard cut → click + âm beat 0:0 tụt). Release chồng +// lên re-noteon pass mới → seam như cùng khuôn nhạc. Stop thật giữ CC120. +try{const _wrapRelease=!!(fxRtWrapRestart&&!stale);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);if(!_wrapRelease)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 +// D18: wrap (_wrapNoStop) — KHÔNG flush (giữ dòng liên tục qua seam); +// flush ở đây + ở startTrackPlayback = ring rỗng → gap giật beat 0:0. +if(!_wrapNoStop){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). @@ -2595,7 +2638,7 @@ if(aiPromptMgrRef.current&&window.__aiPresetVersion!==aiPresetVersion){window.__ // (quét 8 bars hiển thị "0 đến 8"): nhập 8 → selection tới vạch 8. setSelectionEnd(t);setNumberBar(Math.max(0,b-beginBar));},className:"w-full bg-[#242424] text-white text-center font-mono text-xs rounded py-0.5 border border-zinc-700 focus:outline-none"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col justify-center"},/*#__PURE__*/React.createElement("span",{className:"text-[7px] text-zinc-500 font-bold uppercase"},"# Bars"),/*#__PURE__*/React.createElement("span",{className:"text-zinc-200 font-mono text-xs font-semibold mt-0.5"},numberBar))));if(panelId==='fx_rack')return/*#__PURE__*/React.createElement("div",{className:"flex flex-col h-full gap-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",onMouseDown:e=>startPanelDrag('fx_rack',e)},/*#__PURE__*/React.createElement("h3",{className:"font-bold text-xs text-rose-300 flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-vertical",className:"w-3 h-3 text-zinc-500"})),/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-3.5 h-3.5 text-rose-400"}))," Plugin FX Rack"),/*#__PURE__*/React.createElement("button",{onClick:()=>closePanel('fx_rack'),className:"text-zinc-600 hover:text-zinc-300"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 text-xs text-zinc-500 italic flex items-center justify-center"},"No FX plugins loaded"));if(panelId==='midi_events')return/*#__PURE__*/React.createElement("div",{className:"flex flex-col h-full gap-1.5"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between shrink-0 cursor-grab active:cursor-grabbing select-none",onMouseDown:e=>startPanelDrag('midi_events',e)},/*#__PURE__*/React.createElement("h3",{className:"font-bold text-xs text-sky-300 flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-vertical",className:"w-3 h-3 text-zinc-500"})),/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5 text-sky-400"}))," MIDI Event List"),/*#__PURE__*/React.createElement("button",{onClick:()=>closePanel('midi_events'),className:"text-zinc-600 hover:text-zinc-300"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 text-xs text-zinc-500 italic flex items-center justify-center"},"No MIDI events selected"));return null;};const renderDock=(pos,title)=>{const panels=dockPanels[pos];if(panels.length===0)return null;const isSide=pos==='left'||pos==='right';const borderClass=pos==='left'?'border-r':pos==='right'?'border-l':pos==='top'?'border-b':'border-t';const bgClass='bg-[#1e1e1e]';const highlight=panelDragRef.current&&panelDropZone===pos;if(pos==='right')return/*#__PURE__*/React.createElement("div",{id:"right-sidebar",className:`${borderClass} ${bgClass} flex flex-col overflow-hidden select-none h-full`,style:{width:`${rightSidebarWidth}px`,minWidth:'200px',maxWidth:'600px',flexShrink:0}},/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col gap-2 p-2 overflow-hidden h-full"},panels.map((p,idx)=>/*#__PURE__*/React.createElement(React.Fragment,{key:p},/*#__PURE__*/React.createElement("div",{className:'flex flex-col flex-1 min-h-0 border border-zinc-700 rounded-lg bg-[#262626] shadow-sm p-3'},renderPanelContent(p)),idx/*#__PURE__*/React.createElement("div",{key:p,className:`${isSide?'w-full':'flex-none w-[320px]'} border border-zinc-700 rounded-lg bg-[#262626] shadow-sm p-2`},renderPanelContent(p))));};return/*#__PURE__*/React.createElement("div",{ref:workspaceRef,className:"flex-1 flex flex-col overflow-hidden select-none daw-bg relative"},panelDragRef.current&&panelDropZone&&/*#__PURE__*/React.createElement("div",{className:"absolute inset-0 z-50 pointer-events-none"},panelDropZone==='top'&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 left-0 right-0 h-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"}),panelDropZone==='bottom'&&/*#__PURE__*/React.createElement("div",{className:"absolute bottom-0 left-0 right-0 h-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"}),panelDropZone==='left'&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 left-0 w-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"}),panelDropZone==='right'&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 right-0 w-1 bg-cyan-400 shadow-[0_0_8px_#06b6d4]"})),dragGhostPanel&&dragGhostPos&&/*#__PURE__*/React.createElement("div",{className:"fixed z-[100] pointer-events-none opacity-80 border border-cyan-500 rounded-lg bg-[#262626] shadow-2xl p-3 w-56",style:{left:dragGhostPos.x,top:dragGhostPos.y}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 text-xs text-zinc-200 font-bold"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"move",className:"w-3.5 h-3.5 text-cyan-400"})),dragGhostPanel==='export'?'Export Panel':dragGhostPanel==='ai'?'AI Panel':dragGhostPanel==='python_tools'?'Audio Processing Panel':'Selection Panel'),/*#__PURE__*/React.createElement("div",{className:"text-xs text-zinc-500 mt-1"},"Drop at edge to dock")),renderDock('top','Top'),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden"},renderDock('left','Left'),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col overflow-hidden min-w-0"},/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-hidden"},activeTab==='main'||sessionTabs.some(s=>s.id===activeTab)?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{ref:tcpContainerRef,onScroll:handleTCPScroll,className:"shrink-0 z-20 bg-[#262626] overflow-y-auto flex flex-col border-r border-zinc-900 no-scrollbar",style:{width:tcpWidth+'px',scrollbarWidth:'none',msOverflowStyle:'none'}},/*#__PURE__*/React.createElement("div",{className:"sticky top-0 z-50 flex items-center justify-between h-10 border-b border-zinc-900 bg-[#242424] px-2 shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-zinc-300 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-3.5 h-3.5 text-cyan-400"})),"TRACKS (",activeTracks.length,")"),/*#__PURE__*/React.createElement("button",{onClick:addNewTrack,className:"px-2.5 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1 shadow transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3 h-3"}))," Add Track")),/*#__PURE__*/React.createElement("div",{className:"sticky top-10 z-40 flex h-[40px] border-b border-purple-500/30 bg-[#1a1a2e] p-2 border-l-4 border-purple-500 shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between w-full"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-purple-400 font-mono"},"TM"),/*#__PURE__*/React.createElement("span",{className:"text-xs font-semibold text-zinc-300"},"Tempo")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("input",{type:"number",value:bpm,onChange:e=>{setBpm(e.target.value);},onBlur:e=>{const v=e.target.value;if(v&&String(bpm)!==v)setBpmWithUndo(v);localStorage.setItem('studio_bpm',bpm);},onKeyDown:e=>{if(e.key==='Enter'){e.target.blur();}},className:"w-12 bg-zinc-800 border border-zinc-700 rounded text-xs text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500",min:"40",max:"300"}),/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500"},"BPM")))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col divide-y divide-[#141414] bg-[#262626]"},activeTracks.length===0?/*#__PURE__*/React.createElement("div",{className:"p-6 text-center text-zinc-400 flex flex-col items-center justify-center space-y-3"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus-circle",className:"w-8 h-8 text-cyan-400 opacity-80"})),/*#__PURE__*/React.createElement("p",{className:"text-xs font-medium"},"Chưa có Track nào trong dự án."),/*#__PURE__*/React.createElement("button",{onClick:addNewTrack,className:"px-3 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white rounded text-xs font-semibold flex items-center gap-1.5 shadow"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus-circle",className:"w-3.5 h-3.5"}))," Thêm Track Mới")):activeTracks.map((track,idx)=>{const isSelected=selectedTrackId===track.id;const autoHeight=track.height||(track.isArmed?164:140);return/*#__PURE__*/React.createElement("div",{key:track.id,style:{height:`${autoHeight}px`},className:`shrink-0 relative flex flex-col justify-between p-2.5 bg-[#1e1e1e] border-r border-zinc-900 cursor-pointer border-l-4 border-b border-[#141414] overflow-hidden ${isSelected?'border-cyan-500 bg-[#252525]':'border-transparent hover:bg-zinc-800/20'}`,onClick:()=>{// Chỉ CHỌN track — KHÔNG tự dừng main play + chuyển sang piano roll // tab (trước đây gây "mất âm thanh khi click track number"). -setSelectedTrackId(track.id);}},/*#__PURE__*/React.createElement("div",{className:"flex items-start justify-between"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-zinc-500 font-mono"},(idx+1).toString().padStart(2,'0')),/*#__PURE__*/React.createElement("label",{className:"cursor-pointer relative block"},/*#__PURE__*/React.createElement("input",{type:"color",value:track.color||'#0f766e',onClick:e=>e.stopPropagation(),onMouseDown:e=>e.stopPropagation(),onChange:e=>{e.stopPropagation();updateTrackColor(track.id,e.target.value);},className:"absolute inset-0 w-full h-full opacity-0 cursor-pointer"}),/*#__PURE__*/React.createElement("div",{className:"w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",style:{backgroundColor:track.color}})),editingTrackName===track.id?/*#__PURE__*/React.createElement("input",{type:"text",value:editNameInput,autoFocus:true,onChange:e=>setEditNameInput(e.target.value),onBlur:()=>{updateTrackName(track.id,editNameInput||track.name);setEditingTrackName(null);},onKeyDown:e=>{if(e.key==='Enter'){updateTrackName(track.id,editNameInput||track.name);setEditingTrackName(null);}if(e.key==='Escape')setEditingTrackName(null);},onClick:e=>e.stopPropagation(),className:"text-xs font-semibold bg-black text-zinc-200 border border-cyan-500 rounded px-1 py-0 w-24 outline-none"}):/*#__PURE__*/React.createElement("span",{className:"text-xs font-semibold text-zinc-300 truncate max-w-[80px] cursor-text hover:text-cyan-300 transition-colors",title:"Click to rename",onClick:e=>{e.stopPropagation();setEditingTrackName(track.id);setEditNameInput(track.name);}},track.name)),/*#__PURE__*/React.createElement("div",{className:"flex flex-wrap gap-0.5 max-w-[100px] mb-0.5"},(track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',name:track.name,startTime:track.startTime}]:[]).slice(0,3).map(c=>/*#__PURE__*/React.createElement("span",{key:c.id,className:"text-[9px] font-mono text-zinc-500 bg-zinc-800/60 rounded px-0.5 truncate max-w-[90px] cursor-pointer hover:text-cyan-400 hover:bg-zinc-700",title:c.name||track.name,onClick:e=>{e.stopPropagation();setSelectedTrackId(track.id);clearLocalSelection();setSelectionMode('global');const start=c.startTime||0;const end=start+(c.buffer?c.buffer.duration:2);setSelectionStart(start);setSelectionEnd(end);showToast(`Selected: ${c.name||track.name}`,'info');}},c.name||track.name),editingClipName&&editingClipName.trackId===track.id&&editingClipName.clipId===c.id?/*#__PURE__*/React.createElement("input",{type:"text",value:editNameInput,autoFocus:true,onChange:e=>setEditNameInput(e.target.value),onBlur:()=>{if(editNameInput.trim())updateClipName(track.id,c.id,editNameInput.trim());setEditingClipName(null);},onKeyDown:e=>{if(e.key==='Enter'){if(editNameInput.trim())updateClipName(track.id,c.id,editNameInput.trim());setEditingClipName(null);}if(e.key==='Escape')setEditingClipName(null);},onClick:e=>e.stopPropagation(),className:"w-20 text-[9px] font-mono bg-black text-cyan-300 border border-cyan-500 rounded px-0.5 py-0 outline-none"}):/*#__PURE__*/React.createElement("button",{className:"text-[9px] text-zinc-600 hover:text-cyan-400 ml-0.5 shrink-0",title:"Sửa tên clip",onClick:e=>{e.stopPropagation();setEditingClipName({trackId:track.id,clipId:c.id});setEditNameInput(c.name||track.name);}},/*#__PURE__*/React.createElement("i",{"data-lucide":"pencil",className:"w-2.5 h-2.5"})))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackMute(track.id);if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(track.id,{muted:!track.muted});},title:"Mute",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.muted?'bg-red-950 text-red-400 border-red-700':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":track.muted?"volume-x":"volume-2",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackSoloEvaluate(track.id);if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(track.id,{solo:!track.solo});},title:"Solo",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.solo?'bg-amber-950 text-amber-400 border-amber-600':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":track.solo?"headphones":"headphone-off",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackDrum(track.id);},title:track.is_percussion?"Drum Channel (CH 10) - Click to disable":"Toggle Drum Channel (CH 10)",className:`px-1.5 py-0.5 text-[9px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.is_percussion?'bg-rose-900 text-rose-300 border-rose-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},/*#__PURE__*/React.createElement("span",{className:"text-[11px]"},"🥁"),track.is_percussion?/*#__PURE__*/React.createElement("span",{className:"text-[9px]"},"D"):null),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackArm(track.id);},title:"ARM (Record)",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.isArmed?'bg-red-600 text-white border-red-500 hover:bg-red-500':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:`w-2.5 h-2.5 ${track.isArmed?'fill-white':''}`})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackMonitor(track.id);},title:"Input Monitor",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.monitoringEnabled?'bg-amber-600 text-white border-amber-500 hover:bg-amber-500':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-305'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":track.monitoringEnabled?"mic":"mic-off",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();deleteTrack(track.id);},className:"p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3 h-3"}))))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-0.5 text-xs",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1","data-sf-wheel":"1",onWheel:e=>{const steps=wheelSteps('vol:'+track.id,e.deltaY,e.deltaMode,100);if(!steps)return;const nv=Math.max(-50,Math.min(7,(track.volumeDb??0)+steps*0.5));updateTrackVolumeDb(track.id,nv);},onDoubleClick:e=>{e.stopPropagation();updateTrackVolumeDb(track.id,0);}},/*#__PURE__*/React.createElement("span",{className:"w-8 text-right text-zinc-500 text-xs"},"Vol:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-50",max:"7",step:"0.5",value:track.volumeDb??0,onChange:e=>updateTrackVolumeDb(track.id,parseFloat(e.target.value)),className:"flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-cyan-500",style:{height:'4px'}}),/*#__PURE__*/React.createElement("span",{className:"w-12 text-right font-mono text-zinc-300 text-xs"},track.volumeDb??0,"dB")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1","data-sf-wheel":"1",onWheel:e=>{const steps=wheelSteps('pan:'+track.id,e.deltaY,e.deltaMode,100);if(!steps)return;const np=Math.max(-100,Math.min(100,(track.pan??0)+steps*5));updateTrackPan(track.id,np);},onDoubleClick:e=>{e.stopPropagation();updateTrackPan(track.id,0);}},/*#__PURE__*/React.createElement("span",{className:"w-8 text-right text-zinc-500 text-xs"},"Pan:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-100",max:"100",step:"1",value:track.pan??0,onChange:e=>updateTrackPan(track.id,parseInt(e.target.value)),className:"flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-purple-500",style:{height:'4px'}}),/*#__PURE__*/React.createElement("span",{className:"w-12 text-right font-mono text-zinc-300 text-xs"},track.pan>0?'R'+track.pan:track.pan<0?'L'+Math.abs(track.pan):'C')),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 mt-0.5"},/*#__PURE__*/React.createElement("span",{className:"w-8 text-right text-zinc-500 text-[10px]"},"In:"),/*#__PURE__*/React.createElement("select",{value:`${track.inputSource?.deviceType||'NONE'}:${track.inputSource?.deviceId||''}`,onChange:e=>{const val=e.target.value;const parts=val.split(':');const type=parts[0];const id=parts.slice(1).join(':');updateTrackInputSource(track.id,type,id);},className:"flex-1 bg-[#18181b] text-zinc-300 text-[10px] rounded border border-zinc-700 focus:outline-none py-0.5 px-1 truncate max-w-[120px]"},/*#__PURE__*/React.createElement("option",{value:"NONE:"},"No Input"),/*#__PURE__*/React.createElement("optgroup",{label:"Microphones"},audioDevices.map(d=>/*#__PURE__*/React.createElement("option",{key:d.deviceId,value:`MICROPHONE:${d.deviceId}`},d.label||`Microphone ${d.deviceId.slice(0,5)}`))),/*#__PURE__*/React.createElement("optgroup",{label:"MIDI Keyboards"},/*#__PURE__*/React.createElement("option",{value:"MIDI_KEYBOARD:ALL"},"Any MIDI Keyboard"),midiDevices.map(d=>/*#__PURE__*/React.createElement("option",{key:d.id,value:`MIDI_KEYBOARD:${d.id}`},d.name||`MIDI Input ${d.id.slice(0,5)}`)))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 w-16 shrink-0 ml-1"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 text-[9px]"},"VU"),/*#__PURE__*/React.createElement("canvas",{ref:el=>{// TÁCH VU MAIN vs SECTION-TAB (user 07:20): section clone id TRÙNG +setSelectedTrackId(track.id);}},/*#__PURE__*/React.createElement("div",{className:"flex items-start justify-between"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-zinc-500 font-mono"},(idx+1).toString().padStart(2,'0')),/*#__PURE__*/React.createElement("label",{className:"cursor-pointer relative block"},/*#__PURE__*/React.createElement("input",{type:"color",value:track.color||'#0f766e',onClick:e=>e.stopPropagation(),onMouseDown:e=>e.stopPropagation(),onChange:e=>{e.stopPropagation();updateTrackColor(track.id,e.target.value);},className:"absolute inset-0 w-full h-full opacity-0 cursor-pointer"}),/*#__PURE__*/React.createElement("div",{className:"w-3 h-3 rounded-full border border-zinc-600 hover:scale-110 transition-transform",style:{backgroundColor:track.color}})),editingTrackName===track.id?/*#__PURE__*/React.createElement("input",{type:"text",value:editNameInput,autoFocus:true,onChange:e=>setEditNameInput(e.target.value),onBlur:()=>{updateTrackName(track.id,editNameInput||track.name);setEditingTrackName(null);},onKeyDown:e=>{if(e.key==='Enter'){updateTrackName(track.id,editNameInput||track.name);setEditingTrackName(null);}if(e.key==='Escape')setEditingTrackName(null);},onClick:e=>e.stopPropagation(),className:"text-xs font-semibold bg-black text-zinc-200 border border-cyan-500 rounded px-1 py-0 w-24 outline-none"}):/*#__PURE__*/React.createElement("span",{className:"text-xs font-semibold text-zinc-300 truncate max-w-[80px] cursor-text hover:text-cyan-300 transition-colors",title:"Click to rename",onClick:e=>{e.stopPropagation();setEditingTrackName(track.id);setEditNameInput(track.name);}},track.name)),/*#__PURE__*/React.createElement("div",{className:"flex flex-wrap gap-0.5 max-w-[100px] mb-0.5"},(track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default',name:track.name,startTime:track.startTime}]:[]).slice(0,3).map(c=>/*#__PURE__*/React.createElement("span",{key:c.id,className:"text-[9px] font-mono text-zinc-500 bg-zinc-800/60 rounded px-0.5 truncate max-w-[90px] cursor-pointer hover:text-cyan-400 hover:bg-zinc-700",title:c.name||track.name,onClick:e=>{e.stopPropagation();setSelectedTrackId(track.id);clearLocalSelection();setSelectionMode('global');const start=c.startTime||0;const end=start+(c.buffer?c.buffer.duration:2);setSelectionStart(start);setSelectionEnd(end);showToast(`Selected: ${c.name||track.name}`,'info');}},c.name||track.name),editingClipName&&editingClipName.trackId===track.id&&editingClipName.clipId===c.id?/*#__PURE__*/React.createElement("input",{type:"text",value:editNameInput,autoFocus:true,onChange:e=>setEditNameInput(e.target.value),onBlur:()=>{if(editNameInput.trim())updateClipName(track.id,c.id,editNameInput.trim());setEditingClipName(null);},onKeyDown:e=>{if(e.key==='Enter'){if(editNameInput.trim())updateClipName(track.id,c.id,editNameInput.trim());setEditingClipName(null);}if(e.key==='Escape')setEditingClipName(null);},onClick:e=>e.stopPropagation(),className:"w-20 text-[9px] font-mono bg-black text-cyan-300 border border-cyan-500 rounded px-0.5 py-0 outline-none"}):/*#__PURE__*/React.createElement("button",{className:"text-[9px] text-zinc-600 hover:text-cyan-400 ml-0.5 shrink-0",title:"Sửa tên clip",onClick:e=>{e.stopPropagation();setEditingClipName({trackId:track.id,clipId:c.id});setEditNameInput(c.name||track.name);}},/*#__PURE__*/React.createElement("i",{"data-lucide":"pencil",className:"w-2.5 h-2.5"})))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackMute(track.id);if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(track.id,{muted:!track.muted});},title:"Mute",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.muted?'bg-red-950 text-red-400 border-red-700':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":track.muted?"volume-x":"volume-2",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackSoloEvaluate(track.id);if(window.__applyTrackMuteSolo)window.__applyTrackMuteSolo(track.id,{solo:!track.solo});},title:"Solo",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.solo?'bg-amber-950 text-amber-400 border-amber-600':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":track.solo?"headphones":"headphone-off",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackDrum(track.id);},title:track.is_percussion?"Drum Channel (CH 10) - Click to disable":"Toggle Drum Channel (CH 10)",className:`px-1.5 py-0.5 text-[9px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.is_percussion?'bg-rose-900 text-rose-300 border-rose-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},/*#__PURE__*/React.createElement("span",{className:"text-[11px]"},"🥁"),track.is_percussion?/*#__PURE__*/React.createElement("span",{className:"text-[9px]"},"D"):null),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackArm(track.id);},title:"ARM (Record)",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.isArmed?'bg-red-600 text-white border-red-500 hover:bg-red-500':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:`w-2.5 h-2.5 ${track.isArmed?'fill-white':''}`})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackMonitor(track.id);},title:"Input Monitor",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.monitoringEnabled?'bg-amber-600 text-white border-amber-500 hover:bg-amber-500':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-305'}`},/*#__PURE__*/React.createElement("i",{"data-lucide":track.monitoringEnabled?"mic":"mic-off",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();deleteTrack(track.id);},className:"p-0.5 rounded text-red-500 hover:bg-red-950/40 hover:text-red-400 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3 h-3"}))))),function(){const _ls=window.__trackLoadState&&window.__trackLoadState[track.id];if(_ls&&_ls.state==='loading'){return/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 mt-0.5",title:"Đang nạp instrument vào bridge — track sẽ phát sau khi load xong"},/*#__PURE__*/React.createElement("div",{className:"flex-1 h-1 rounded bg-zinc-800 overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-full rounded sf-ruler-bar",style:{background:'#06b6d4',width:'45%'}})),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-cyan-400 font-mono shrink-0"},"Loading…"));}if(_ls&&_ls.state==='failed'){return/*#__PURE__*/React.createElement("div",{className:"mt-0.5 text-[9px] text-red-500 font-mono"},"Load failed");}if(_ls&&_ls.state==='done'&&_ls.doneTs&&Date.now()-_ls.doneTs<2500){return/*#__PURE__*/React.createElement("div",{className:"mt-0.5 h-1 rounded bg-green-600/30 overflow-hidden"},/*#__PURE__*/React.createElement("div",{className:"h-full bg-green-500 sf-ruler-done"}));}return null;}(),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-0.5 text-xs",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1","data-sf-wheel":"1",onWheel:e=>{const steps=wheelSteps('vol:'+track.id,e.deltaY,e.deltaMode,100);if(!steps)return;const nv=Math.max(-50,Math.min(7,(track.volumeDb??0)+steps*0.5));updateTrackVolumeDb(track.id,nv);},onDoubleClick:e=>{e.stopPropagation();updateTrackVolumeDb(track.id,0);}},/*#__PURE__*/React.createElement("span",{className:"w-8 text-right text-zinc-500 text-xs"},"Vol:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-50",max:"7",step:"0.5",value:track.volumeDb??0,onChange:e=>updateTrackVolumeDb(track.id,parseFloat(e.target.value)),className:"flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-cyan-500",style:{height:'4px'}}),/*#__PURE__*/React.createElement("span",{className:"w-12 text-right font-mono text-zinc-300 text-xs"},track.volumeDb??0,"dB")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1","data-sf-wheel":"1",onWheel:e=>{const steps=wheelSteps('pan:'+track.id,e.deltaY,e.deltaMode,100);if(!steps)return;const np=Math.max(-100,Math.min(100,(track.pan??0)+steps*5));updateTrackPan(track.id,np);},onDoubleClick:e=>{e.stopPropagation();updateTrackPan(track.id,0);}},/*#__PURE__*/React.createElement("span",{className:"w-8 text-right text-zinc-500 text-xs"},"Pan:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-100",max:"100",step:"1",value:track.pan??0,onChange:e=>updateTrackPan(track.id,parseInt(e.target.value)),className:"flex-1 h-1 bg-zinc-700 rounded-lg appearance-none cursor-pointer accent-purple-500",style:{height:'4px'}}),/*#__PURE__*/React.createElement("span",{className:"w-12 text-right font-mono text-zinc-300 text-xs"},track.pan>0?'R'+track.pan:track.pan<0?'L'+Math.abs(track.pan):'C')),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 mt-0.5"},/*#__PURE__*/React.createElement("span",{className:"w-8 text-right text-zinc-500 text-[10px]"},"In:"),/*#__PURE__*/React.createElement("select",{value:`${track.inputSource?.deviceType||'NONE'}:${track.inputSource?.deviceId||''}`,onChange:e=>{const val=e.target.value;const parts=val.split(':');const type=parts[0];const id=parts.slice(1).join(':');updateTrackInputSource(track.id,type,id);},className:"flex-1 bg-[#18181b] text-zinc-300 text-[10px] rounded border border-zinc-700 focus:outline-none py-0.5 px-1 truncate max-w-[120px]"},/*#__PURE__*/React.createElement("option",{value:"NONE:"},"No Input"),/*#__PURE__*/React.createElement("optgroup",{label:"Microphones"},audioDevices.map(d=>/*#__PURE__*/React.createElement("option",{key:d.deviceId,value:`MICROPHONE:${d.deviceId}`},d.label||`Microphone ${d.deviceId.slice(0,5)}`))),/*#__PURE__*/React.createElement("optgroup",{label:"MIDI Keyboards"},/*#__PURE__*/React.createElement("option",{value:"MIDI_KEYBOARD:ALL"},"Any MIDI Keyboard"),midiDevices.map(d=>/*#__PURE__*/React.createElement("option",{key:d.id,value:`MIDI_KEYBOARD:${d.id}`},d.name||`MIDI Input ${d.id.slice(0,5)}`)))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 w-16 shrink-0 ml-1"},/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 text-[9px]"},"VU"),/*#__PURE__*/React.createElement("canvas",{ref:el=>{// TÁCH VU MAIN vs SECTION-TAB (user 07:20): section clone id TRÙNG // main id → key canvas trùng → VU main/section lẫn nhau. Section // strip dùng key '_sess_' — VU tick parse theo key. const _vuK=track._isSectionClone?'_sess_'+track.id:track.id;if(el)trackVuRefs.current[_vuK]=el;else delete trackVuRefs.current[_vuK];},width:100,height:4,className:"flex-1 bg-[#18181b] rounded h-1"})))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 mt-1",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("input",{type:"file",id:`upload-${track.id}`,accept:"audio/*",className:"hidden",onChange:e=>loadFileOnTrack(track.id,e.target.files[0])}),/*#__PURE__*/React.createElement("label",{htmlFor:`upload-${track.id}`,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded text-xs flex items-center gap-1 cursor-pointer border border-zinc-700 font-medium"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"upload",className:"w-3 h-3"}))," File"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();window.__openFxRack(track.id,track.name);},title:"Mở Track FX Chain",className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-purple-400 border border-purple-900 rounded text-xs font-bold flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"wand-2",className:"w-3 h-3"}))," FX: ",/*#__PURE__*/React.createElement("span",{className:"text-zinc-500 font-normal"},track.fxType||"None"),/*#__PURE__*/React.createElement("span",{title:"FX nhanh (cũ)",className:"ml-0.5 text-zinc-500 hover:text-purple-300 cursor-pointer",onClick:e=>{e.stopPropagation();setFxSelectorTrackId(track.id);}},"▾")),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();openInstrumentSelector(track.id);},className:"px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-amber-400 border border-amber-800 rounded text-xs font-bold flex items-center gap-1 max-w-[90px] shrink-0"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-2.5 h-2.5 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"truncate text-xs min-w-0"},track.instrumentName||track.instrumentId||"Synth"),/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-down",className:"w-3 h-3 shrink-0"})),track.instrumentId&&typeof track.instrumentId==='string'&&!track.instrumentId.startsWith('sf_')?/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();ensureAndOpenVstGui(track);},title:"Mở lại GUI VSTi",className:"px-1.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-violet-300 border border-violet-800 rounded text-xs font-bold flex items-center gap-1 shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders-horizontal",className:"w-3 h-3 shrink-0"}),/*#__PURE__*/React.createElement("span",{className:"truncate text-xs min-w-0"},"GUI")):null),/*#__PURE__*/React.createElement("div",{onMouseDown:e=>handleTrackResizeMouseDown(e,track.id),className:"absolute bottom-0 left-0 right-0 h-1 cursor-ns-resize z-30 hover:bg-cyan-500/50 transition-colors",onClick:e=>e.stopPropagation()}));})),/*#__PURE__*/React.createElement("div",{className:"h-[48px] border-r border-zinc-900 bg-[#1e1e1e]/50 shrink-0"})),/*#__PURE__*/React.createElement("div",{className:"w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",onMouseDown:startTcpResize}),/*#__PURE__*/React.createElement("div",{ref:handleTimelineWrapperRef,onScroll:handleTimelineScroll,className:"flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${timelineWidth}px`},className:"relative flex flex-col min-h-full"},/*#__PURE__*/React.createElement("div",{className:"sticky top-0 z-30 bg-[#1a1a1a]"},/*#__PURE__*/React.createElement(TimelineRuler,{bpm:parseInt(bpm)||120,zoom:zoom,timelineWidth:timelineWidth,viewportWidth:viewportWidth,onPlayheadSet:handlePlayheadSet,snapValue:snapValue,onRulerMouseDown:e=>{setSelectionFollowsTempo(false);handleRulerMouseDown(e);},scrollLeft:scrollLeft,canvasRedrawCount:canvasRedrawCount}),/*#__PURE__*/React.createElement(TempoTrackLane,{bpm:parseInt(bpm)||120,zoom:zoom,timelineWidth:timelineWidth,viewportWidth:viewportWidth,onPlayheadSet:handlePlayheadSet,snapValue:snapValue,onRulerMouseDown:e=>{setSelectionFollowsTempo(true);handleRulerMouseDown(e);},scrollLeft:scrollLeft,canvasRedrawCount:canvasRedrawCount})),selectionMode==='global'&&selLeft!==null&&selRight!==null&&selRight>selLeft&&/*#__PURE__*/React.createElement("div",{className:"absolute inset-0 pointer-events-none z-20",style:{left:`${selLeft*zoom}px`,width:`${(selRight-selLeft)*zoom}px`,top:'80px'}},/*#__PURE__*/React.createElement("div",{className:"w-full h-full bg-amber-500/10",style:{borderLeft:'1px solid #f59e0b',borderRight:'1px solid #f59e0b'}})),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col divide-y divide-[#141414] relative bg-[#111111] min-h-full",onDragOver:e=>e.preventDefault(),onDrop:async e=>{e.preventDefault();// Media Explorer drag (client FS handle / server file_id / local path) diff --git a/app/static/js/services/bridgeAudioNode.js b/app/static/js/services/bridgeAudioNode.js index 7cd01f2..f558859 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=202608272301'; + var url = '/static/js/services/bridgeAudioWorklet.js?v=202608291200'; _ctx.audioWorklet.addModule(url).then(function () { if (!_initialized) return; // disconnect() happened while loading if (_useWorklet) return; // already set up (init retry) diff --git a/app/static/js/services/bridgeAudioWorklet.js b/app/static/js/services/bridgeAudioWorklet.js index 8934de4..fea9eae 100644 --- a/app/static/js/services/bridgeAudioWorklet.js +++ b/app/static/js/services/bridgeAudioWorklet.js @@ -1,6 +1,7 @@ // 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 8 block (~42ms @48k) — hap thu jitter pump +// - Ring block 256 mau, toi da 16 block (~85ms @48k) — hap thu jitter pump +// (option 2: 8->16 de dung tremolo crackle; backlog lon hon ~85ms) // 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). @@ -10,7 +11,7 @@ class BridgeAudioSinkProcessor extends AudioWorkletProcessor { constructor() { super(); this._ring = []; - this._MAX = 8; + this._MAX = 16; this._gain = 1.0; this._lastOutL = 0.0; this._lastOutR = 0.0; diff --git a/app/templates/index.html b/app/templates/index.html index 6369095..d1e438d 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -40,7 +40,7 @@ - + @@ -51,7 +51,7 @@ - +