From 371b1ca665c8914c937b4d82deba5f982b79b485 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 11 Aug 2026 18:04:34 +0700 Subject: [PATCH] =?UTF-8?q?T16:=20autosample=20VST2=20via=20native=20bridg?= =?UTF-8?q?e=20+=20SF2/SF3=20export=20ho=C3=A0n=20ch=E1=BB=89nh,=20UI=20ch?= =?UTF-8?q?ip=20size/note=5Fcount,=20re-sample=20on=20preset=20change,=20t?= =?UTF-8?q?ests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/plugins.py | 14 ++-- app/core/native_audio_service.py | 8 ++- app/core/soundfont_converter.py | 56 +++++++++------ app/static/js/app.jsx | 23 ++++++- app/static/js/app.precompiled.js | 10 +-- app/static/js/services/vstiAutosample.js | 16 +++++ tests/test_autosampler.py | 21 ++++++ tools/autosample_vsti.py | 86 +++++++++++++++++++----- 8 files changed, 184 insertions(+), 50 deletions(-) diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index ca2b76c..a7f1ad3 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -1144,17 +1144,22 @@ async def autosample_vsti(req: AutosampleRequest, current_user: dict = Depends(g Tra sf_id (bare uuid) de client tai SF2 va phat qua SonicSF nhu soundfont thuong. Chi phi autosample mot lan -> cache o client (localStorage).""" enforce_password_changed(current_user) - if not HAS_PEDALBOARD: - raise HTTPException(status_code=501, detail="pedalboard khong kha dung tren may nay") + _dirs = _effective_dirs().get("plugin_dirs") or [] # tools/ nam ngoai package app - import voi fallback path try: - from tools.autosample_vsti import autosample_sf2 as _autosample_sf2 + from tools.autosample_vsti import autosample_sf2 as _autosample_sf2, _is_vst2_path except ImportError: _root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) if _root not in sys.path: sys.path.insert(0, _root) - from tools.autosample_vsti import autosample_sf2 as _autosample_sf2 + from tools.autosample_vsti import autosample_sf2 as _autosample_sf2, _is_vst2_path + + if not HAS_PEDALBOARD: + # VST2 native bridge khong can pedalboard — chi chan khi plugin la VST3 + _p = PluginManager(extra_vst_dirs=_dirs)._scan_plugins().get(req.instrument_id) + if not _p or not _is_vst2_path(_p): + raise HTTPException(status_code=501, detail="pedalboard khong kha dung tren may nay") file_uuid = str(uuid.uuid4()) dest_path = os.path.join(UPLOAD_SF_DIR, file_uuid + ".sf2") @@ -1174,6 +1179,7 @@ async def autosample_vsti(req: AutosampleRequest, current_user: dict = Depends(g duration=req.duration, release=req.release, velocity=req.velocity, sample_rate=req.sample_rate, log=_log, + extra_vst_dirs=_dirs, ) except FileNotFoundError as e: raise HTTPException(status_code=404, detail=str(e)) diff --git a/app/core/native_audio_service.py b/app/core/native_audio_service.py index de3c2e2..d8a6b4e 100644 --- a/app/core/native_audio_service.py +++ b/app/core/native_audio_service.py @@ -445,8 +445,10 @@ class NativeAudioService: def render_vst2_offline(self, plugin_path, notes, bpm=120.0, sr=44100, gain_db=0.0, pan=0.0, lim_active=False, - threshold_db=-1.0, master_gain_db=0.0): - """Offline VST2: SF_VST2_Process (raw) + mixer_math (Python mirror).""" + threshold_db=-1.0, master_gain_db=0.0, tail_sec=0.5): + """Offline VST2: SF_VST2_Process (raw) + mixer_math (Python mirror). + + tail_sec: duoi im lang sau note cuoi (autosample muon release tail).""" with _LOCK: dll = self._load_vst2_dll() ctx = _Vst2Ctx(dll) @@ -466,7 +468,7 @@ class NativeAudioService: events.append((int((start_s + dur_s) * sr), "off", pitch, 0)) total = max(total, start_s + dur_s) events.sort(key=lambda e: e[0]) - n = int((max(total, 0.25) + 0.5) * sr) + n = int((max(total, 0.25) + float(tail_sec)) * sr) out = np.zeros((2, n), dtype=np.float32) block = 256 pos = 0 diff --git a/app/core/soundfont_converter.py b/app/core/soundfont_converter.py index 0475a99..9ef2ce9 100644 --- a/app/core/soundfont_converter.py +++ b/app/core/soundfont_converter.py @@ -5,6 +5,27 @@ import logging import wave logger = logging.getLogger(__name__) +def _ensure_fluidsynth_runtime() -> None: + """Dua thu muc chua libfluidsynth DLL vao PATH de pyfluidsynth import duoc + (find_library do theo PATH tren Windows). Khong lam gi tren non-Windows.""" + if os.name != "nt": + return + root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + candidates = [ + os.path.join(root, "native_host", "build", "Release", "fluidsynth_runtime"), + os.path.join(root, "native_host", "fluidsynth_runtime"), + ] + path = os.environ.get("PATH", "") + parts = [os.path.normcase(p) for p in path.split(os.pathsep)] + for d in candidates: + if os.path.isdir(d) and any( + f.lower().startswith("libfluidsynth") and f.lower().endswith(".dll") + for f in os.listdir(d) + ): + if os.path.normcase(d) not in parts: + os.environ["PATH"] = d + os.pathsep + path + return + SF_TARGET_DIRS = [ "/opt/daw_engine/soundfonts", @@ -193,8 +214,8 @@ class SoundFontConverter: # OGG loop pointers are relative to the individual decompressed sample new_sloop = (startloop - start) if (startloop > start and startloop <= end) else 0 new_eloop = (endloop - start) if (endloop > start and endloop <= end) else 0 - # Mark the sample as Ogg Vorbis compressed (FLUID_SAMPLETYPE_OGG_VORBIS = 0x20) - new_stype = sampletype | 0x20 + # Mark the sample as Ogg Vorbis compressed (FLUID_SAMPLETYPE_OGG_VORBIS = 0x10) + new_stype = sampletype | 0x10 new_shdr += data[base:base + 20] # sample name new_shdr += struct.pack(" 1e-4 finally: try: - _fs.delete_fluid_synth(_fl) + _synth.delete() except Exception: pass except Exception: @@ -453,7 +467,7 @@ class SoundFontConverter: new_loopstart = loopstart + new_start if (loopstart or loopend) else 0 new_loopend = loopend + new_start if (loopstart or loopend) else 0 # Clear the Ogg Vorbis flag; keep mono/left/right/linked flags - new_stype = sampletype & ~0x20 + new_stype = sampletype & ~0x10 new_shdr += name new_shdr += struct.pack(" { try { localStorage.setItem('sf_pr_zoom', String(rollZoom)); } catch (e) { } }, [rollZoom]); + // Autosample VSTi xong → re-render chip size/note_count (subtab header) + React.useEffect(function () { + const _h = () => setRenderTick(t => t + 1); + window.addEventListener('sf:autosample-update', _h); + return () => window.removeEventListener('sf:autosample-update', _h); + }, []); const [aiBarStart, setAiBarStart] = React.useState(0); const [aiBarEnd, setAiBarEnd] = React.useState(4); @@ -9915,7 +9921,10 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); onClick: () => onInstrumentSelect && onInstrumentSelect(st.trackId), title: st.instrumentName || "Synth", className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName ? 'bg-violet-900 text-violet-300 border-violet-700' : 'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}` - }, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, activeParentTrackName ? ('(' + activeParentTrackName + ') ' + (st.instrumentName || 'Synth')) : (st.instrumentName || 'Synth'))), React.createElement("div", { + }, React.createElement("i", { "data-lucide": "music", className: "w-3 h-3 shrink-0" }), React.createElement("span", { className: "truncate text-[9px]" }, activeParentTrackName ? ('(' + activeParentTrackName + ') ' + (st.instrumentName || 'Synth')) : (st.instrumentName || 'Synth'))), (function() { + var _e = (typeof window.SonicVstiAutosample !== 'undefined') ? window.SonicVstiAutosample.entryFor(st.synth_engine) : null; + return _e ? /*#__PURE__*/React.createElement("span", { className: "px-1 py-0.5 bg-zinc-900 border border-zinc-800 text-emerald-400/90 rounded text-[9px] font-mono whitespace-nowrap shrink-0", title: "Autosampled: " + (_e.note_count || 0) + " notes" }, window.SonicVstiAutosample.formatSize(_e.size_bytes) + " \u00b7 " + (_e.note_count || 0) + "n") : null; + })(), React.createElement("div", { className: "flex items-center gap-1 ml-1 text-xs" }, React.createElement("span", { className: "text-zinc-500" }, "AI:"), React.createElement("input", { type: "number", value: aiBarStart, onChange: e => setAiBarStart(parseInt(e.target.value) || 0), @@ -15374,6 +15383,13 @@ const App = () => { }; const [instrumentSelectorData, setInstrumentSelectorData] = useState(null); const [instrumentRefreshKey, setInstrumentRefreshKey] = useState(0); + // Autosample VSTi xong → re-render chip size/note_count (track strip) + const [autosampleVersion, setAutosampleVersion] = React.useState(0); + React.useEffect(() => { + const _h = () => setAutosampleVersion(v => v + 1); + window.addEventListener('sf:autosample-update', _h); + return () => window.removeEventListener('sf:autosample-update', _h); + }, []); const openInstrumentSelector = trackId => { setInstrumentSelectorTrackId(trackId); setSfPresetSearchQuery(''); @@ -29708,7 +29724,10 @@ STRICT CONSTRAINTS: }, /*#__PURE__*/React.createElement("i", { "data-lucide": "music", className: "w-3 h-3" - })), /*#__PURE__*/React.createElement("span", { className: "truncate text-[10px]" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" }))), /*#__PURE__*/React.createElement("div", { + })), /*#__PURE__*/React.createElement("span", { className: "truncate text-[10px]" }, track.instrumentName || track.instrumentId || "Synth"), /*#__PURE__*/React.createElement("i", { "data-lucide": "chevron-down", className: "w-3 h-3 shrink-0" }))), (function() { + var _e = (typeof window.SonicVstiAutosample !== 'undefined') ? window.SonicVstiAutosample.entryFor(track.synth_engine) : null; + return _e ? /*#__PURE__*/React.createElement("span", { className: "px-1.5 py-0.5 bg-zinc-900 border border-zinc-800 text-emerald-400/90 rounded text-[9px] font-mono whitespace-nowrap shrink-0", title: "Autosampled: " + (_e.note_count || 0) + " notes" }, window.SonicVstiAutosample.formatSize(_e.size_bytes) + " \u00b7 " + (_e.note_count || 0) + "n") : 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() diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 53f0b3c..844f802 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -470,7 +470,8 @@ React.useEffect(()=>{const handleMessage=event=>{if(event.data&&event.data.type= const userDefined=newPresets.filter(p=>p.is_user_defined);if(window.SonicAPI&&userDefined.length>0){userDefined.forEach(p=>{window.SonicAPI.saveAIPreset(p).catch(()=>{});});}};const handleEdit=p=>{setEditingPreset(p);setFormName(p.name);setFormKeywords(p.keywords.join(', '));setFormCategory(p.category);setSelectedCategory(p.category);setFormBars(p.default_bars);setFormBpm(p.default_bpm);setFormScale(p.default_scale);setFormTemplate(p.system_instruction_template);setIsAddingCategory(false);setNewCategoryValue('');};const handleNew=()=>{setEditingPreset('new');setFormName('');setFormKeywords('');setFormCategory('Orchestral / Film Score');setSelectedCategory('Orchestral / Film Score');setFormBars(8);setFormBpm(120);setFormScale('C Minor');setFormTemplate('');setIsAddingCategory(false);setNewCategoryValue('');};const handleNewStructured=()=>{refreshPresets();setShowGeneratorModal(true);};const handleToggleFav=id=>{mgr.toggleFavorite(id);setPresets([...mgr.getPresets()]);const p=mgr.presets.find(x=>x.id===id);if(p&&p.is_user_defined&&window.SonicAPI){window.SonicAPI.saveAIPreset(p).catch(()=>{});}};const handleDelete=id=>{const p=presets.find(x=>x.id===id);if(p&&p.is_user_defined&&window.SonicAPI){window.SonicAPI.deleteAIPreset(id).catch(()=>{});}mgr.deletePreset(id);setPresets([...mgr.getPresets()]);showToast('Đã xóa preset.','info');};const handleSaveForm=e=>{e.preventDefault();if(!formName.trim()||!formTemplate.trim()){showToast('Vui lòng điền đầy đủ tên và mẫu gợi ý.','warning');return;}const keywordsArray=formKeywords.split(',').map(k=>k.trim()).filter(Boolean);const presetObj={id:editingPreset==='new'?'preset_'+Date.now():editingPreset.id,name:formName.trim(),keywords:keywordsArray,category:formCategory,default_bars:parseInt(formBars)||8,default_bpm:parseInt(formBpm)||120,default_scale:formScale,system_instruction_template:formTemplate.trim(),is_user_defined:true,is_favorite:editingPreset==='new'?false:editingPreset.is_favorite||false,created_at:editingPreset==='new'?new Date().toISOString():editingPreset.created_at};mgr.saveUserPreset(presetObj);setPresets([...mgr.getPresets()]);setEditingPreset(null);if(window.SonicAPI){window.SonicAPI.saveAIPreset(presetObj).catch(()=>{});}showToast('Đã lưu preset thành công!','success');};const categories=['ALL','★ Yêu thích','Người dùng',...new Set(presets.map(p=>p.category))];const filtered=presets.filter(p=>{const matchesSearch=p.name.toLowerCase().includes(search.toLowerCase())||p.keywords.some(k=>k.toLowerCase().includes(search.toLowerCase()));let matchesCategory;if(filterCategory==='ALL'){matchesCategory=true;}else if(filterCategory==='★ Yêu thích'){matchesCategory=p.is_favorite;}else if(filterCategory==='Người dùng'){matchesCategory=p.is_user_defined;}else{matchesCategory=p.category===filterCategory;}if(showFavoritesOnly)matchesCategory=matchesCategory&&p.is_favorite;return matchesSearch&&matchesCategory;});return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/75 backdrop-blur-sm p-4 animate-fade-in"},/*#__PURE__*/React.createElement("div",{className:"bg-[#18181b] border border-zinc-800 rounded-xl shadow-2xl w-full max-w-4xl max-h-[85vh] flex flex-col overflow-hidden text-zinc-100"},/*#__PURE__*/React.createElement("div",{className:"p-4 border-b border-zinc-800 flex items-center justify-between shrink-0 bg-[#202024]"},/*#__PURE__*/React.createElement("h2",{className:"text-sm font-bold tracking-wider uppercase text-purple-400 flex items-center gap-1.5"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders",className:"w-4 h-4"}),"AI Prompt Preset Manager",syncing&&/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-zinc-500 ml-2"},"đang đồng bộ...")),/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingPreset(null);onClose();},className:"text-zinc-400 hover:text-zinc-200 transition"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto p-4 flex gap-4 min-h-0"},!editingPreset?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col min-w-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 shrink-0"},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Tìm kiếm preset hoặc từ khóa...",value:search,onChange:e=>setSearch(e.target.value),className:"flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"}),/*#__PURE__*/React.createElement("select",{value:filterCategory,onChange:e=>setFilterCategory(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600"},categories.map(c=>/*#__PURE__*/React.createElement("option",{key:c,value:c},c==='ALL'?'Tất cả danh mục':c))),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowFavoritesOnly(!showFavoritesOnly),className:`px-2.5 py-1 rounded text-xs font-bold transition shrink-0 ${showFavoritesOnly?'bg-yellow-700 text-yellow-300':'bg-zinc-800 text-zinc-400 hover:bg-zinc-700'}`,title:"Chỉ hiện yêu thích"},/*#__PURE__*/React.createElement("i",{"data-lucide":"star",className:"w-3.5 h-3.5 inline-block mr-1"}),"★"),/*#__PURE__*/React.createElement("button",{onClick:handleNew,className:"px-3 py-1 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold flex items-center gap-1.5 shadow transition shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3.5 h-3.5"}),"Tạo mới")),/*#__PURE__*/React.createElement("div",{className:"flex-1 border border-zinc-800 rounded bg-[#0f0f12] overflow-y-auto"},filtered.length===0?/*#__PURE__*/React.createElement("div",{className:"p-8 text-center text-zinc-500 text-xs italic"},"Không tìm thấy preset nào."):/*#__PURE__*/React.createElement("table",{className:"w-full text-left text-xs border-collapse"},/*#__PURE__*/React.createElement("thead",{className:"bg-[#1f1f23] text-zinc-400 font-bold border-b border-zinc-800"},/*#__PURE__*/React.createElement("tr",null,/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-8"},""),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/4"},"Tên Preset"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/4"},"Từ khóa kích hoạt"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6"},"Số Bar"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6"},"BPM"),/*#__PURE__*/React.createElement("th",{className:"p-2.5 w-1/6 text-right"},"Hành động"))),/*#__PURE__*/React.createElement("tbody",null,filtered.map(p=>/*#__PURE__*/React.createElement("tr",{key:p.id,className:"border-b border-zinc-800/50 hover:bg-zinc-850"},/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-center"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleToggleFav(p.id),className:`transition ${p.is_favorite?'text-yellow-400':'text-zinc-600 hover:text-zinc-400'}`,title:p.is_favorite?'Bỏ yêu thích':'Đánh dấu yêu thích'},p.is_favorite?"★":"☆")),/*#__PURE__*/React.createElement("td",{className:"p-2.5 font-semibold text-purple-300"},p.name),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400 font-mono text-[11px] truncate max-w-[150px]"},p.keywords.join(', ')),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400"},p.default_bars," Bars"),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-zinc-400"},p.default_bpm," BPM"),/*#__PURE__*/React.createElement("td",{className:"p-2.5 text-right flex items-center justify-end gap-1.5 h-full"},/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>handleEdit(p),className:"px-2 py-0.5 bg-zinc-850 hover:bg-zinc-700 text-zinc-300 rounded border border-zinc-700 text-[10px]"},"Sửa"),p.is_user_defined&&/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>handleDelete(p.id),className:"px-2 py-0.5 bg-red-950/40 hover:bg-red-800 text-red-400 rounded border border-red-900 text-[10px]"},"Xóa"))))))))):/*#__PURE__*/React.createElement("form",{onSubmit:handleSaveForm,className:"flex-1 flex flex-col gap-3 min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-zinc-400 shrink-0 border-b border-zinc-800 pb-1"},editingPreset==='new'?"TẠO PRESET MỚI":`SỬA PRESET: ${editingPreset.name}`),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Tên Preset"),/*#__PURE__*/React.createElement("input",{type:"text",value:formName,onChange:e=>setFormName(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Danh mục"),isAddingCategory?/*#__PURE__*/React.createElement("div",{className:"flex gap-2"},/*#__PURE__*/React.createElement("input",{type:"text",value:newCategoryValue,onChange:e=>setNewCategoryValue(e.target.value),onBlur:()=>{if(newCategoryValue.trim()){addNewCategory(newCategoryValue.trim());}setIsAddingCategory(false);},onKeyDown:e=>{if(e.key==='Enter'){e.preventDefault();if(newCategoryValue.trim()){addNewCategory(newCategoryValue.trim());}setIsAddingCategory(false);}else if(e.key==='Escape'){setIsAddingCategory(false);}},autoFocus:true,className:"flex-1 bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200",placeholder:"Nhập danh mục mới..."})):/*#__PURE__*/React.createElement("select",{value:selectedCategory,onChange:e=>{if(e.target.value==='__add_new__'){setIsAddingCategory(true);setNewCategoryValue('');}else{setSelectedCategory(e.target.value);setFormCategory(e.target.value);}},className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"},presetCategories.map(c=>/*#__PURE__*/React.createElement("option",{key:c,value:c},c)),/*#__PURE__*/React.createElement("option",{value:'__add_new__'},"+ Nhập danh mục mới...")))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Từ khóa kích hoạt (ngăn cách bằng dấu phẩy)"),/*#__PURE__*/React.createElement("input",{type:"text",value:formKeywords,onChange:e=>setFormKeywords(e.target.value),placeholder:"Ví dụ: epic orchestra, hoành tráng, nhạc phim epic",className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-3"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Số Bars mặc định"),/*#__PURE__*/React.createElement("input",{type:"number",value:formBars,onChange:e=>setFormBars(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"BPM mặc định"),/*#__PURE__*/React.createElement("input",{type:"number",value:formBpm,onChange:e=>setFormBpm(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"Âm giai (Scale) mặc định"),/*#__PURE__*/React.createElement("input",{type:"text",value:formScale,onChange:e=>setFormScale(e.target.value),className:"bg-zinc-900 border border-zinc-700 rounded px-2.5 py-1 text-xs outline-none focus:border-purple-600 text-zinc-200"}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1 flex-1 min-h-0"},/*#__PURE__*/React.createElement("label",{className:"text-[10px] uppercase font-bold text-zinc-500"},"System Prompt Template / Luật soạn nhạc"),/*#__PURE__*/React.createElement("textarea",{value:formTemplate,onChange:e=>setFormTemplate(e.target.value),rows:6,className:"flex-1 bg-zinc-900 border border-zinc-700 rounded p-2.5 text-xs outline-none focus:border-purple-600 text-zinc-200 font-mono resize-none"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 shrink-0 pt-2 border-t border-zinc-800"},/*#__PURE__*/React.createElement("button",{type:"button",onClick:handleNewStructured,className:"px-3 py-1.5 bg-emerald-800 hover:bg-emerald-700 text-emerald-300 border border-emerald-700 rounded text-xs transition"},"Tạo preset với cấu trúc"),/*#__PURE__*/React.createElement("div",{className:"flex-1"}),/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>setEditingPreset(null),className:"px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border border-zinc-700 rounded text-xs transition"},"Quay lại"),/*#__PURE__*/React.createElement("button",{type:"submit",className:"px-3 py-1.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-xs font-bold shadow transition"},"Lưu Preset")))),/*#__PURE__*/React.createElement("div",{className:"p-4 border-t border-zinc-800 flex justify-end shrink-0 bg-[#202024]"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setEditingPreset(null);onClose();},className:"px-4 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs font-bold shadow transition"},"Đóng")))),showGeneratorModal?/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[60] flex items-center justify-center bg-black/80 p-4",onClick:e=>{if(e.target===e.currentTarget){refreshPresets();setShowGeneratorModal(false);}}},/*#__PURE__*/React.createElement("div",{className:"w-full h-full max-w-6xl max-h-[90vh] bg-[#13141a] border border-zinc-800 rounded-2xl shadow-2xl overflow-hidden flex flex-col"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between p-3 border-b border-zinc-800 shrink-0"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400"},"AI Prompt Generator"),/*#__PURE__*/React.createElement("button",{onClick:()=>{refreshPresets();setShowGeneratorModal(false);},className:"text-zinc-400 hover:text-white"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-4 h-4"}))),/*#__PURE__*/React.createElement("iframe",{src:"/ai-prompt-generator",className:"flex-1 w-full border-0 bg-white",title:"AI Prompt Generator"}))):null);};const PianoRollTabEditor=({st,zoom,bpm,viewportWidth,activeTracks,onClose,onUpdateNotes,onSaveNotes,setSubTabs,onPlayPause,onStop,isPlaying,playPreviewNote,showToast,midiDevices,recordingState,recTempMidiNotes,onRecord,selectedMidiInputId,onMidiInputSelect,activeMidiPitches,onInstrumentSelect,onRescheduleMidi,onSeekPlayhead,snapValue,onSnapChange,onRealtimePlay,onCopyNotes,clipboardNotes,onExportMidiAudio})=>{const[activeRollTool,setActiveRollTool]=React.useState('select');const[renderTick,setRenderTick]=React.useState(0);const[ccMode,setCcMode]=React.useState('velocity');const[rollZoom,setRollZoom]=React.useState(()=>{// Persist zoom piano roll qua phiên (localStorage) — user yêu cầu giữ kích // thước zoom ở phiên làm việc trước. try{const v=parseFloat(localStorage.getItem('sf_pr_zoom'));if(isFinite(v)&&v>=15&&v<=250)return v;}catch(e){}return 60;});// local horizontal zoom factor -React.useEffect(()=>{try{localStorage.setItem('sf_pr_zoom',String(rollZoom));}catch(e){}},[rollZoom]);const[aiBarStart,setAiBarStart]=React.useState(0);const[aiBarEnd,setAiBarEnd]=React.useState(4);const canvasRef=React.useRef(null);const ccCanvasRef=React.useRef(null);const ccWrapperRef=React.useRef(null);const gridScrollRef=React.useRef(null);const keybedRef=React.useRef(null);const keybedMouseDownRef=React.useRef(false);const rulerScrollRef=React.useRef(null);// Audio element phát preview VSTi render — dừng cái cũ khi play cái mới +React.useEffect(()=>{try{localStorage.setItem('sf_pr_zoom',String(rollZoom));}catch(e){}},[rollZoom]);// Autosample VSTi xong → re-render chip size/note_count (subtab header) +React.useEffect(function(){const _h=()=>setRenderTick(t=>t+1);window.addEventListener('sf:autosample-update',_h);return()=>window.removeEventListener('sf:autosample-update',_h);},[]);const[aiBarStart,setAiBarStart]=React.useState(0);const[aiBarEnd,setAiBarEnd]=React.useState(4);const canvasRef=React.useRef(null);const ccCanvasRef=React.useRef(null);const ccWrapperRef=React.useRef(null);const gridScrollRef=React.useRef(null);const keybedRef=React.useRef(null);const keybedMouseDownRef=React.useRef(false);const rulerScrollRef=React.useRef(null);// Audio element phát preview VSTi render — dừng cái cũ khi play cái mới const vstiPreviewAudioRef=React.useRef(null);React.useEffect(()=>{const up=()=>{keybedMouseDownRef.current=false;};window.addEventListener('mouseup',up);return()=>window.removeEventListener('mouseup',up);},[]);const NoteHeight=18;const PITCH_START=0;// C0 (render all 128 keys) const KeybedPixelHeight=(128-PITCH_START)*NoteHeight;const pixelsPerBeat=rollZoom;const timeSigNum=4;const noteMaxBeat=(st.notes||[]).reduce((max,n)=>Math.max(max,(n.start_beat||0)+(n.duration_beats||1)),0);const[selectionMarquee,setSelectionMarquee]=React.useState(null);// { startBeat, startPitch, currentBeat, currentPitch } const[draggedNote,setDraggedNote]=React.useState(null);// { mode: 'move'|'resize', idx, startOffsetBeat, originalStart } @@ -582,7 +583,7 @@ const[velocityTarget,setVelocityTarget]=React.useState(80);// compress target (s const snapToScaleRef=React.useRef(true);snapToScaleRef.current=st.snapToScale!==undefined?st.snapToScale:true;const[scaleMenuPos,setScaleMenuPos]=React.useState(null);const scaleMenuOriginRef=React.useRef(null);const snapPitchToScale=(pitch,scale)=>{if(!scale)return pitch;const octave=Math.floor(pitch/12);const noteInOctave=pitch%12;if(scale.includes(noteInOctave))return pitch;let best=noteInOctave;let minDist=12;scale.forEach(s=>{const dist=Math.abs(s-noteInOctave);if(dist{const sc=selectedScaleRef.current;if(!sc)return null;return sc.map(s=>(s+scaleRootRef.current)%12);};const commitNotes=updated=>{setNotes(updated);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updated);};const applyArpeggiate=p=>{const sc=scaleWithRoot();const beatSec=60.0/(parseInt(bpm)||120);const rateDiv=p.rate==='1/4'?1:p.rate==='1/8'?0.5:p.rate==='1/16'?0.25:p.rate==='1/32'?0.125:1;const rateBeats=rateDiv*(p.triplet?2/3:p.dotted?1.5:1);const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const chords=[];targets.forEach(n=>{const k=n.start_beat.toFixed(2);const g=chords.find(c=>c.k===k);if(g)g.notes.push(n);else chords.push({k,notes:[n]});});const result=[];const rangeNotes=[];// arpeggiated sequence (pitch per step) chords.forEach(ch=>{const sorted=ch.notes.slice().sort((a,b)=>a.pitch-b.pitch);const notesOut=[];for(let o=0;o{const octPitch=n.pitch+o*12;if(!rangeNotes.includes(octPitch))rangeNotes.push(octPitch);});}const seq=[];rangeNotes.splice(0,rangeNotes.length);chords.forEach(()=>{});const sortedAsc=ch.notes.slice().sort((a,b)=>a.pitch-b.pitch);const sortedDesc=sortedAsc.slice().reverse();const pool=p.pattern==='DOWN'?sortedDesc:p.pattern==='UP-DOWN'?[...sortedAsc,...sortedDesc.slice(1,-1)]:p.pattern==='RANDOM'?sortedAsc.slice().sort(()=>Math.random()-0.5):sortedAsc;// UP / CHORD -const steps=p.pattern==='CHORD'?1:pool.length*p.octaves;const seqPitches=[];for(let i=0;iseqPitches.push(n.pitch));break;}seqPitches.push(pool[i%pool.length].pitch+Math.floor(i/pool.length)*12);}const stepDur=p.pattern==='CHORD'?rateBeats*pool.length:rateBeats;const total=seqPitches.length*stepDur;seqPitches.forEach((pitch,i)=>{const dur=stepDur*(p.gate/100);const vel=ch.notes[0]?ch.notes[0].velocity:0.8;notesOut.push({id:'note_'+Math.random().toString(36).substr(2,9),pitch,start_beat:ch.notes[0].start_beat+i*stepDur,duration_beats:Math.max(0.05,dur),velocity:vel});});result.push(...notesOut);});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result].sort((a,b)=>a.start_beat-b.start_beat));setArpModal(null);showToast('Đã arpeggiate '+targets.length+' nốt.','success');};const applyStrum=p=>{const secPerBeat=60.0/(parseInt(bpm)||120);const strumBeats=p.ms/1000.0/secPerBeat;const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const groups=[];targets.forEach(n=>{const k=n.start_beat.toFixed(2);const g=groups.find(g2=>g2.k===k);if(g)g.notes.push(n);else groups.push({k,notes:[n]});});let alternateFlip=false;const result=targets.map(n=>({...n}));groups.forEach(g=>{const sorted=g.notes.slice().sort((a,b)=>a.pitch-b.pitch);const asc=p.direction==='UP'?sorted.slice().reverse():p.direction==='ALTERNATE'?alternateFlip?sorted.slice().reverse():sorted.slice():sorted;alternateFlip=!alternateFlip;const firstStart=sorted[0].start_beat;asc.forEach((note,index)=>{result.forEach(r=>{if(r.id===note.id){r.start_beat=parseFloat((firstStart+index*strumBeats).toFixed(3));r.velocity=Math.max(0.1,Math.min(1.0,parseFloat((r.velocity-index*0.03).toFixed(2))));}});});});commitNotes(result);setStrumModal(null);showToast('Đã strum '+targets.length+' nốt.','success');};const applyHumanizeModal=p=>{const spb=60.0/(parseInt(bpm)||120);const maxJitter=p.timingMs/1000.0/spb;const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const result=targets.map(n=>{const tj=(Math.random()-0.5)*2*maxJitter;const vj=(Math.random()-0.5)*2*(p.velRange/127);const dj=p.durRange?(Math.random()-0.5)*2*(p.durRange/100):0;return{...n,start_beat:Math.max(0,parseFloat((n.start_beat+tj).toFixed(3))),velocity:Math.max(0.05,Math.min(1.0,parseFloat((n.velocity+vj).toFixed(2)))),duration_beats:Math.max(0.05,parseFloat((n.duration_beats*(1+dj)).toFixed(3)))};});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);setHumanizeModal(null);showToast('Đã humanize '+targets.length+' nốt.','success');};const applyForceToScale=()=>{const sc=scaleWithRoot();if(!sc){showToast('Chưa chọn scale (nhấp chuột phải chọn Scale).','warning');return;}const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;const result=targets.map(n=>({...n,pitch:snapPitchToScale(n.pitch,sc)}));const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã force '+targets.length+' nốt về scale.','success');};const CHORD_SHAPES={triad:[0,4,7],min7:[0,3,7,10],sus4:[0,5,7],add9:[0,4,7,14]};const applyChordStamp=(beat,pitch)=>{const shape=CHORD_SHAPES[chordType]||CHORD_SHAPES.triad;const beatSec=60.0/(parseInt(bpm)||120);const barBeats=4;const newNotes=shape.map(iv=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:pitch+iv,start_beat:beat,duration_beats:1,velocity:0.8}));commitNotes([...notes,...newNotes]);showToast('Đã stamp chord ('+shape.length+' nốt).','success');};const applyHarmonize=interval=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const dups=targets.map(n=>({...n,id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch+interval}));commitNotes([...notes,...dups]);showToast('Đã harmonize +'+interval+' ('+dups.length+' nốt).','success');};const applyVelocityCompress=()=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const mean=targets.reduce((s,n)=>s+(n.velocity!==undefined?n.velocity:0.8),0)/targets.length;const target=velocityTarget/127;const result=targets.map(n=>{const v=n.velocity!==undefined?n.velocity:0.8;return{...n,velocity:Math.max(0.05,Math.min(1.0,v+(target-mean)))};});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã compress velocity về '+velocityTarget+' ('+targets.length+' nốt).','success');};const applyVelocityNormalize=()=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const maxV=Math.max(...targets.map(n=>n.velocity!==undefined?n.velocity:0.8));if(maxV<=0)return;const result=targets.map(n=>({...n,velocity:Math.max(0.05,Math.min(1.0,(n.velocity!==undefined?n.velocity:0.8)/maxV))}));const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã normalize velocity (max → 127).','success');};const renderScaleContextMenu=()=>{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subs=[];Object.keys(val).forEach(subKey=>{subs.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});var subH=subs.length*30+16;var subTop=origin.y+subH+20>window.innerHeight?origin.y-subH:origin.y;subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:subTop,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subs);}}});var menuH=items.length*30+16;var menuTop=origin.y+menuH+20>window.innerHeight?Math.max(10,origin.y-menuH):origin.y;return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:menuTop,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);const barOffset=Math.floor(sessionStartBar);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${displayBar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"},/* 1. TOOLBAR HEADER — 2 hàng (wrap tự nhiên; spacer 100% ép hàng mới) */React.createElement("div",{className:"bg-[#282828] border-b border-zinc-900 flex flex-wrap items-center gap-x-2 gap-y-1 px-4 py-1.5 shrink-0 text-slate-200"},React.createElement("div",{className:"flex items-center gap-4"},React.createElement("select",{value:st.trackId||'',onChange:function(e){var trkId=e.target.value;var trkSel=(activeTracks||[]).find(function(t){return t.id===trkId;});if(trkSel&&trkSel.midiItems&&trkSel.midiItems.length)handleSwitchMidiItem(trkSel.midiItems[0].id);},className:"bg-zinc-800 border border-zinc-700 text-yellow-500 font-bold text-xs rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[180px] uppercase"},function(){var seenTrackOpts={};var trackOpts=[];(activeTracks||[]).forEach(function(t){if(!t.midiItems||!t.midiItems.length)return;if(seenTrackOpts[t.id])return;seenTrackOpts[t.id]=true;trackOpts.push(React.createElement("option",{key:t.id,value:t.id},t.name||t.id));});return trackOpts;}()),activeParentTrackName?React.createElement("span",{className:"text-[9px] text-zinc-500 ml-1"},"(Belongs to: ",React.createElement("span",{className:"text-zinc-400 font-semibold"},activeParentTrackName),")"):null,React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),React.createElement("select",{value:snapValue,onChange:e=>{onSnapChange(e.target.value);setRenderTick(t=>t+1);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>React.createElement("option",{key:v,value:v},v)))),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},React.createElement("option",{value:""},"Input"),React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),React.createElement("button",{onClick:()=>onInstrumentSelect&&onInstrumentSelect(st.trackId),title:st.instrumentName||"Synth",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName?'bg-violet-900 text-violet-300 border-violet-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},React.createElement("i",{"data-lucide":"music",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[9px]"},activeParentTrackName?'('+activeParentTrackName+') '+(st.instrumentName||'Synth'):st.instrumentName||'Synth')),React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},React.createElement("span",{className:"text-zinc-500"},"AI:"),React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"-"),React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"bar")),React.createElement("select",{value:ccMode,onChange:e=>setCcMode(e.target.value),className:"bg-zinc-800 text-zinc-200 border border-zinc-700 rounded px-1.5 py-1 text-xs capitalize cursor-pointer"},React.createElement("option",{value:"velocity"},"Velocity"),React.createElement("option",{value:"sustain"},"Sustain"),React.createElement("option",{value:"modulation"},"Modulation"),React.createElement("option",{value:"pitch_bend"},"Pitch Bend"),React.createElement("option",{value:"pan"},"Pan"))),React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='velocity'?'Vel':ccMode==='sustain'?'Sus':ccMode==='modulation'?'Mod':ccMode==='pitch_bend'?'Bend':ccMode==='pan'?'Pan':'CC'),React.createElement("button",{onClick:function(){setSessionSyncMode(function(p){return!p;});},className:function(){var base='px-2 py-1 rounded text-xs ';return sessionSyncMode?base+'bg-cyan-900/60 text-cyan-300 border border-cyan-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:sessionSyncMode?"Session-synced mode (ghost visible)":"Isolated mode (bar 0, no ghost)"},sessionSyncMode?"\uD83C\uDF10 Session":"\uD83D\uDCCB Isolated"),React.createElement("button",{onClick:function(){setShowGhostNotes(function(p){return!p;});},disabled:!sessionSyncMode,className:function(){if(!sessionSyncMode)return'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';var base='px-2 py-1 rounded text-xs ';return showGhostNotes?base+'bg-purple-900/60 text-purple-300 border border-purple-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:"Toggle ghost notes visibility"},"👻 MIDI ghost notes"),React.createElement("div",{className:"flex items-center gap-1 ml-auto"},React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"L\u01B0u"),React.createElement("button",{onClick:()=>{const ppq=480;const bpmNum=parseInt(bpm)||120;const ticksPerBeat=ppq;const events=[];(notes||[]).forEach(n=>{const startTick=Math.round((n.start_beat||0)*ticksPerBeat);const durTick=Math.round((n.duration_beats||1)*ticksPerBeat);const pitch=n.pitch||60;const vel=Math.round((n.velocity||0.8)*127);events.push({tick:startTick,type:'note_on',pitch,velocity:vel});events.push({tick:startTick+durTick,type:'note_off',pitch,velocity:0});});events.sort((a,b)=>a.tick-b.tick||(a.type==='note_off'?-1:1));const writeVLQ=(bytes,v)=>{let val=Math.max(0,v);const buf=[];buf.push(val&0x7F);while(val>0x7F){val>>=7;buf.push(0x80|val&0x7F);}for(let i=buf.length-1;i>=0;i--)bytes.push(buf[i]);};const trackBytes=[];let lastTick=0;events.forEach(ev=>{const delta=Math.max(0,ev.tick-lastTick);writeVLQ(trackBytes,delta);trackBytes.push(ev.type==='note_on'?0x90:0x80,ev.pitch,ev.velocity);lastTick=ev.tick;});writeVLQ(trackBytes,0);trackBytes.push(0xFF,0x2F,0x00);const trackData=[0x4D,0x54,0x72,0x6B];const len=trackBytes.length;trackData.push(len>>24&0xFF,len>>16&0xFF,len>>8&0xFF,len&0xFF);trackData.push(...trackBytes);const header=[0x4D,0x54,0x68,0x64,0x00,0x00,0x00,0x06,0x00,0x01,0x00,0x01,ppq>>8&0xFF,ppq&0xFF];const all=header.concat(trackData);const blob=new Blob([new Uint8Array(all)],{type:'audio/midi'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=(st.label||'midi')+'.mid';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);showToast('Đã xuất file MIDI!','success');},className:"px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"file-down",className:"w-3 h-3"}),"Export MIDI"),React.createElement("button",{onClick:async()=>{// Preview/export MIDI notes với âm VSTi: midi-render (pedalboard) → +const steps=p.pattern==='CHORD'?1:pool.length*p.octaves;const seqPitches=[];for(let i=0;iseqPitches.push(n.pitch));break;}seqPitches.push(pool[i%pool.length].pitch+Math.floor(i/pool.length)*12);}const stepDur=p.pattern==='CHORD'?rateBeats*pool.length:rateBeats;const total=seqPitches.length*stepDur;seqPitches.forEach((pitch,i)=>{const dur=stepDur*(p.gate/100);const vel=ch.notes[0]?ch.notes[0].velocity:0.8;notesOut.push({id:'note_'+Math.random().toString(36).substr(2,9),pitch,start_beat:ch.notes[0].start_beat+i*stepDur,duration_beats:Math.max(0.05,dur),velocity:vel});});result.push(...notesOut);});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result].sort((a,b)=>a.start_beat-b.start_beat));setArpModal(null);showToast('Đã arpeggiate '+targets.length+' nốt.','success');};const applyStrum=p=>{const secPerBeat=60.0/(parseInt(bpm)||120);const strumBeats=p.ms/1000.0/secPerBeat;const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const groups=[];targets.forEach(n=>{const k=n.start_beat.toFixed(2);const g=groups.find(g2=>g2.k===k);if(g)g.notes.push(n);else groups.push({k,notes:[n]});});let alternateFlip=false;const result=targets.map(n=>({...n}));groups.forEach(g=>{const sorted=g.notes.slice().sort((a,b)=>a.pitch-b.pitch);const asc=p.direction==='UP'?sorted.slice().reverse():p.direction==='ALTERNATE'?alternateFlip?sorted.slice().reverse():sorted.slice():sorted;alternateFlip=!alternateFlip;const firstStart=sorted[0].start_beat;asc.forEach((note,index)=>{result.forEach(r=>{if(r.id===note.id){r.start_beat=parseFloat((firstStart+index*strumBeats).toFixed(3));r.velocity=Math.max(0.1,Math.min(1.0,parseFloat((r.velocity-index*0.03).toFixed(2))));}});});});commitNotes(result);setStrumModal(null);showToast('Đã strum '+targets.length+' nốt.','success');};const applyHumanizeModal=p=>{const spb=60.0/(parseInt(bpm)||120);const maxJitter=p.timingMs/1000.0/spb;const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const result=targets.map(n=>{const tj=(Math.random()-0.5)*2*maxJitter;const vj=(Math.random()-0.5)*2*(p.velRange/127);const dj=p.durRange?(Math.random()-0.5)*2*(p.durRange/100):0;return{...n,start_beat:Math.max(0,parseFloat((n.start_beat+tj).toFixed(3))),velocity:Math.max(0.05,Math.min(1.0,parseFloat((n.velocity+vj).toFixed(2)))),duration_beats:Math.max(0.05,parseFloat((n.duration_beats*(1+dj)).toFixed(3)))};});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);setHumanizeModal(null);showToast('Đã humanize '+targets.length+' nốt.','success');};const applyForceToScale=()=>{const sc=scaleWithRoot();if(!sc){showToast('Chưa chọn scale (nhấp chuột phải chọn Scale).','warning');return;}const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;const result=targets.map(n=>({...n,pitch:snapPitchToScale(n.pitch,sc)}));const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã force '+targets.length+' nốt về scale.','success');};const CHORD_SHAPES={triad:[0,4,7],min7:[0,3,7,10],sus4:[0,5,7],add9:[0,4,7,14]};const applyChordStamp=(beat,pitch)=>{const shape=CHORD_SHAPES[chordType]||CHORD_SHAPES.triad;const beatSec=60.0/(parseInt(bpm)||120);const barBeats=4;const newNotes=shape.map(iv=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:pitch+iv,start_beat:beat,duration_beats:1,velocity:0.8}));commitNotes([...notes,...newNotes]);showToast('Đã stamp chord ('+shape.length+' nốt).','success');};const applyHarmonize=interval=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const dups=targets.map(n=>({...n,id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch+interval}));commitNotes([...notes,...dups]);showToast('Đã harmonize +'+interval+' ('+dups.length+' nốt).','success');};const applyVelocityCompress=()=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const mean=targets.reduce((s,n)=>s+(n.velocity!==undefined?n.velocity:0.8),0)/targets.length;const target=velocityTarget/127;const result=targets.map(n=>{const v=n.velocity!==undefined?n.velocity:0.8;return{...n,velocity:Math.max(0.05,Math.min(1.0,v+(target-mean)))};});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã compress velocity về '+velocityTarget+' ('+targets.length+' nốt).','success');};const applyVelocityNormalize=()=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const maxV=Math.max(...targets.map(n=>n.velocity!==undefined?n.velocity:0.8));if(maxV<=0)return;const result=targets.map(n=>({...n,velocity:Math.max(0.05,Math.min(1.0,(n.velocity!==undefined?n.velocity:0.8)/maxV))}));const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã normalize velocity (max → 127).','success');};const renderScaleContextMenu=()=>{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subs=[];Object.keys(val).forEach(subKey=>{subs.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});var subH=subs.length*30+16;var subTop=origin.y+subH+20>window.innerHeight?origin.y-subH:origin.y;subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:subTop,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subs);}}});var menuH=items.length*30+16;var menuTop=origin.y+menuH+20>window.innerHeight?Math.max(10,origin.y-menuH):origin.y;return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:menuTop,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);const barOffset=Math.floor(sessionStartBar);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${displayBar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"},/* 1. TOOLBAR HEADER — 2 hàng (wrap tự nhiên; spacer 100% ép hàng mới) */React.createElement("div",{className:"bg-[#282828] border-b border-zinc-900 flex flex-wrap items-center gap-x-2 gap-y-1 px-4 py-1.5 shrink-0 text-slate-200"},React.createElement("div",{className:"flex items-center gap-4"},React.createElement("select",{value:st.trackId||'',onChange:function(e){var trkId=e.target.value;var trkSel=(activeTracks||[]).find(function(t){return t.id===trkId;});if(trkSel&&trkSel.midiItems&&trkSel.midiItems.length)handleSwitchMidiItem(trkSel.midiItems[0].id);},className:"bg-zinc-800 border border-zinc-700 text-yellow-500 font-bold text-xs rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[180px] uppercase"},function(){var seenTrackOpts={};var trackOpts=[];(activeTracks||[]).forEach(function(t){if(!t.midiItems||!t.midiItems.length)return;if(seenTrackOpts[t.id])return;seenTrackOpts[t.id]=true;trackOpts.push(React.createElement("option",{key:t.id,value:t.id},t.name||t.id));});return trackOpts;}()),activeParentTrackName?React.createElement("span",{className:"text-[9px] text-zinc-500 ml-1"},"(Belongs to: ",React.createElement("span",{className:"text-zinc-400 font-semibold"},activeParentTrackName),")"):null,React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),React.createElement("select",{value:snapValue,onChange:e=>{onSnapChange(e.target.value);setRenderTick(t=>t+1);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>React.createElement("option",{key:v,value:v},v)))),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},React.createElement("option",{value:""},"Input"),React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),React.createElement("button",{onClick:()=>onInstrumentSelect&&onInstrumentSelect(st.trackId),title:st.instrumentName||"Synth",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName?'bg-violet-900 text-violet-300 border-violet-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},React.createElement("i",{"data-lucide":"music",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[9px]"},activeParentTrackName?'('+activeParentTrackName+') '+(st.instrumentName||'Synth'):st.instrumentName||'Synth')),function(){var _e=typeof window.SonicVstiAutosample!=='undefined'?window.SonicVstiAutosample.entryFor(st.synth_engine):null;return _e?/*#__PURE__*/React.createElement("span",{className:"px-1 py-0.5 bg-zinc-900 border border-zinc-800 text-emerald-400/90 rounded text-[9px] font-mono whitespace-nowrap shrink-0",title:"Autosampled: "+(_e.note_count||0)+" notes"},window.SonicVstiAutosample.formatSize(_e.size_bytes)+" \u00b7 "+(_e.note_count||0)+"n"):null;}(),React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},React.createElement("span",{className:"text-zinc-500"},"AI:"),React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"-"),React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"bar")),React.createElement("select",{value:ccMode,onChange:e=>setCcMode(e.target.value),className:"bg-zinc-800 text-zinc-200 border border-zinc-700 rounded px-1.5 py-1 text-xs capitalize cursor-pointer"},React.createElement("option",{value:"velocity"},"Velocity"),React.createElement("option",{value:"sustain"},"Sustain"),React.createElement("option",{value:"modulation"},"Modulation"),React.createElement("option",{value:"pitch_bend"},"Pitch Bend"),React.createElement("option",{value:"pan"},"Pan"))),React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='velocity'?'Vel':ccMode==='sustain'?'Sus':ccMode==='modulation'?'Mod':ccMode==='pitch_bend'?'Bend':ccMode==='pan'?'Pan':'CC'),React.createElement("button",{onClick:function(){setSessionSyncMode(function(p){return!p;});},className:function(){var base='px-2 py-1 rounded text-xs ';return sessionSyncMode?base+'bg-cyan-900/60 text-cyan-300 border border-cyan-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:sessionSyncMode?"Session-synced mode (ghost visible)":"Isolated mode (bar 0, no ghost)"},sessionSyncMode?"\uD83C\uDF10 Session":"\uD83D\uDCCB Isolated"),React.createElement("button",{onClick:function(){setShowGhostNotes(function(p){return!p;});},disabled:!sessionSyncMode,className:function(){if(!sessionSyncMode)return'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';var base='px-2 py-1 rounded text-xs ';return showGhostNotes?base+'bg-purple-900/60 text-purple-300 border border-purple-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:"Toggle ghost notes visibility"},"👻 MIDI ghost notes"),React.createElement("div",{className:"flex items-center gap-1 ml-auto"},React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"L\u01B0u"),React.createElement("button",{onClick:()=>{const ppq=480;const bpmNum=parseInt(bpm)||120;const ticksPerBeat=ppq;const events=[];(notes||[]).forEach(n=>{const startTick=Math.round((n.start_beat||0)*ticksPerBeat);const durTick=Math.round((n.duration_beats||1)*ticksPerBeat);const pitch=n.pitch||60;const vel=Math.round((n.velocity||0.8)*127);events.push({tick:startTick,type:'note_on',pitch,velocity:vel});events.push({tick:startTick+durTick,type:'note_off',pitch,velocity:0});});events.sort((a,b)=>a.tick-b.tick||(a.type==='note_off'?-1:1));const writeVLQ=(bytes,v)=>{let val=Math.max(0,v);const buf=[];buf.push(val&0x7F);while(val>0x7F){val>>=7;buf.push(0x80|val&0x7F);}for(let i=buf.length-1;i>=0;i--)bytes.push(buf[i]);};const trackBytes=[];let lastTick=0;events.forEach(ev=>{const delta=Math.max(0,ev.tick-lastTick);writeVLQ(trackBytes,delta);trackBytes.push(ev.type==='note_on'?0x90:0x80,ev.pitch,ev.velocity);lastTick=ev.tick;});writeVLQ(trackBytes,0);trackBytes.push(0xFF,0x2F,0x00);const trackData=[0x4D,0x54,0x72,0x6B];const len=trackBytes.length;trackData.push(len>>24&0xFF,len>>16&0xFF,len>>8&0xFF,len&0xFF);trackData.push(...trackBytes);const header=[0x4D,0x54,0x68,0x64,0x00,0x00,0x00,0x06,0x00,0x01,0x00,0x01,ppq>>8&0xFF,ppq&0xFF];const all=header.concat(trackData);const blob=new Blob([new Uint8Array(all)],{type:'audio/midi'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=(st.label||'midi')+'.mid';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);showToast('Đã xuất file MIDI!','success');},className:"px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"file-down",className:"w-3 h-3"}),"Export MIDI"),React.createElement("button",{onClick:async()=>{// Preview/export MIDI notes với âm VSTi: midi-render (pedalboard) → // play wav ngay; nếu pedalboard không render được plugin (VST2/state // GUI) fallback carla-play-notes (realtime qua Carla bridge, OSC). const se=st.synth_engine||{};const instrumentId=se.plugin_id||st.instrumentId||null;if(!instrumentId||String(se.type||'').indexOf('vst')===-1){showToast('Chọn VST instrument (nút Synth) trước khi Preview VSTi','warning');return;}if(!notes||!notes.length){showToast('Chưa có nốt nhạc','warning');return;}const payload={instrument_id:instrumentId,notes:(notes||[]).map(n=>({pitch:n.pitch,start_beat:n.start_beat,duration_beats:n.duration_beats,velocity:n.velocity!=null?n.velocity:0.8})),bpm:parseFloat(bpm)||120,preset_id:se.preset_id||undefined,preset_path:se.preset_path||undefined,preset_data:se.preset_data||undefined};try{const res=await window.SonicAPI.midiRender(payload);if(vstiPreviewAudioRef.current){try{vstiPreviewAudioRef.current.pause();}catch(e){}}const audio=new Audio(res.url);vstiPreviewAudioRef.current=audio;audio.play().catch(()=>{});showToast(`Preview VSTi (${(res.duration_sec||0).toFixed(1)}s) — ${se.plugin_id||instrumentId}`,'success');}catch(err){// midi-render fail (404 plugin không load / 501 pedalboard) → Carla @@ -778,7 +779,8 @@ updateActiveTracks(prev=>prev.map(t=>({...t,midiItems:(t.midiItems||[]).map(m=>{ const bars=Math.max(0.25,Math.round(m.duration/secondsPerBar*4)/4);return{...m,length_bars:bars,duration:bars*secondsPerBar};}return m;}),sections:(t.sections||[]).map(s=>{if(s.length_bars)return{...s,duration:s.length_bars*secondsPerBar};if(s.duration){const bars=Math.max(0.25,Math.round(s.duration/secondsPerBar*4)/4);return{...s,length_bars:bars,duration:bars*secondsPerBar};}return s;})})));},[bpm]);// LAN access badge (standalone): server bind 0.0.0.0 — client browser ở máy // khác trên mạng mở app qua http://:port. Hiện URL trên máy chủ; // client LAN thấy origin đang dùng. -useEffect(()=>{let mounted=true;let bar=null;let iv=null;const tryShow=()=>{const caps=window.SonicRuntime&&window.SonicRuntime.capabilities;const lan=caps&&caps.lan;if(!lan||!lan.urls||!lan.urls.length)return false;if(document.getElementById('sf-lan-bar'))return true;const hostname=window.location.hostname;const isLocal=hostname==='localhost'||hostname==='127.0.0.1'||hostname==='::1';bar=document.createElement('div');bar.id='sf-lan-bar';bar.style.cssText='position:fixed;top:8px;right:12px;z-index:9999;background:rgba(16,185,129,0.12);border:1px solid #10b981;color:#a7f3d0;font-size:11px;line-height:1.5;padding:6px 10px;border-radius:8px;max-width:min(70vw,420px);cursor:default;';bar.title='Client browser ở máy khác trên LAN mở địa chỉ này để làm việc realtime với máy chạy SonicForge';bar.innerHTML=isLocal?'LAN: '+lan.urls.map(function(u){return''+u+'';}).join('  ·  '):'Kết nối: '+window.location.origin;document.body.appendChild(bar);return true;};if(!tryShow()){iv=setInterval(function(){if(mounted&&tryShow())clearInterval(iv);},700);setTimeout(function(){if(iv)clearInterval(iv);},20000);}return()=>{mounted=false;if(iv)clearInterval(iv);if(bar&&bar.parentNode)bar.parentNode.removeChild(bar);};},[]);const openPanel=id=>{if(id==='export')setShowExportPanel(true);else if(id==='ai')setShowAIPanel(true);else if(id==='python_tools')setShowPythonToolsPanel(true);else if(id==='selection')setShowSelectionPanel(true);};const[instrumentSelectorTrackId,setInstrumentSelectorTrackId]=useState(null);const[synthTrackDropdownId,setSynthTrackDropdownId]=useState(null);const[fxSelectorTrackId,setFxSelectorTrackId]=useState(null);const handleSetTrackFx=(trackId,fxType)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,fxType}:t));setFxSelectorTrackId(null);setTimeout(()=>lucide.createIcons(),50);};const[instrumentSelectorData,setInstrumentSelectorData]=useState(null);const[instrumentRefreshKey,setInstrumentRefreshKey]=useState(0);const openInstrumentSelector=trackId=>{setInstrumentSelectorTrackId(trackId);setSfPresetSearchQuery('');setInstrumentRefreshKey(k=>k+1);setSfPresets(null);// Fetch instruments fresh from API for all soundfonts +useEffect(()=>{let mounted=true;let bar=null;let iv=null;const tryShow=()=>{const caps=window.SonicRuntime&&window.SonicRuntime.capabilities;const lan=caps&&caps.lan;if(!lan||!lan.urls||!lan.urls.length)return false;if(document.getElementById('sf-lan-bar'))return true;const hostname=window.location.hostname;const isLocal=hostname==='localhost'||hostname==='127.0.0.1'||hostname==='::1';bar=document.createElement('div');bar.id='sf-lan-bar';bar.style.cssText='position:fixed;top:8px;right:12px;z-index:9999;background:rgba(16,185,129,0.12);border:1px solid #10b981;color:#a7f3d0;font-size:11px;line-height:1.5;padding:6px 10px;border-radius:8px;max-width:min(70vw,420px);cursor:default;';bar.title='Client browser ở máy khác trên LAN mở địa chỉ này để làm việc realtime với máy chạy SonicForge';bar.innerHTML=isLocal?'LAN: '+lan.urls.map(function(u){return''+u+'';}).join('  ·  '):'Kết nối: '+window.location.origin;document.body.appendChild(bar);return true;};if(!tryShow()){iv=setInterval(function(){if(mounted&&tryShow())clearInterval(iv);},700);setTimeout(function(){if(iv)clearInterval(iv);},20000);}return()=>{mounted=false;if(iv)clearInterval(iv);if(bar&&bar.parentNode)bar.parentNode.removeChild(bar);};},[]);const openPanel=id=>{if(id==='export')setShowExportPanel(true);else if(id==='ai')setShowAIPanel(true);else if(id==='python_tools')setShowPythonToolsPanel(true);else if(id==='selection')setShowSelectionPanel(true);};const[instrumentSelectorTrackId,setInstrumentSelectorTrackId]=useState(null);const[synthTrackDropdownId,setSynthTrackDropdownId]=useState(null);const[fxSelectorTrackId,setFxSelectorTrackId]=useState(null);const handleSetTrackFx=(trackId,fxType)=>{updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,fxType}:t));setFxSelectorTrackId(null);setTimeout(()=>lucide.createIcons(),50);};const[instrumentSelectorData,setInstrumentSelectorData]=useState(null);const[instrumentRefreshKey,setInstrumentRefreshKey]=useState(0);// Autosample VSTi xong → re-render chip size/note_count (track strip) +const[autosampleVersion,setAutosampleVersion]=React.useState(0);React.useEffect(()=>{const _h=()=>setAutosampleVersion(v=>v+1);window.addEventListener('sf:autosample-update',_h);return()=>window.removeEventListener('sf:autosample-update',_h);},[]);const openInstrumentSelector=trackId=>{setInstrumentSelectorTrackId(trackId);setSfPresetSearchQuery('');setInstrumentRefreshKey(k=>k+1);setSfPresets(null);// Fetch instruments fresh from API for all soundfonts window.SonicAPI.listPlugins().then(async data=>{var sfonts=data.soundfonts||[];if(sfonts.length===0){setSfPresets([]);return;}try{var results=await Promise.all(sfonts.map(function(sf){var sfId=sf.id.replace('sf_','');return window.SonicAPI.listSoundfontInstruments(sfId).then(function(r){return{sf:sf,presets:r.presets||[]};}).catch(function(){return{sf:sf,presets:[]};});}));var all=[];results.forEach(function(r){var sf=r.sf;var sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;var sfName=sf.display||sf.name||sf.id;(r.presets||[]).forEach(function(p){all.push({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)});});});setSfPresets(all.length>0?all:[]);}catch(e){setSfPresets([]);}}).catch(function(){setSfPresets([]);});};const closeInstrumentSelector=()=>{setInstrumentSelectorTrackId(null);setSynthCategory(null);setSelectedSoundFontId(null);setSfPresetSearchQuery('');};const[synthCategory,setSynthCategory]=useState(null);// 'vst' | 'soundfont' const[selectedSoundFontId,setSelectedSoundFontId]=useState(null);const[sfPresets,setSfPresets]=useState(null);// presets from SoundFont const[instrumentDropdownTrackId,setInstrumentDropdownTrackId]=useState(null);const[instrumentDropdownBtnRect,setInstrumentDropdownBtnRect]=useState(null);const[instrumentSearchQuery,setInstrumentSearchQuery]=useState('');const[sfPresetSearchQuery,setSfPresetSearchQuery]=useState('');const filteredInstruments=useMemo(()=>{if(!instrumentSelectorData||!instrumentSearchQuery)return{soundfonts:instrumentSelectorData?.soundfonts||[],vst:instrumentSelectorData?.vst_instruments||[]};const q=instrumentSearchQuery.toLowerCase();return{soundfonts:(instrumentSelectorData.soundfonts||[]).filter(sf=>(sf.display||sf.name||sf.id).toLowerCase().includes(q)),vst:(instrumentSelectorData.vst_instruments||[]).filter(v=>(v.name||v.id).toLowerCase().includes(q))};},[instrumentSearchQuery,instrumentSelectorData]);useEffect(()=>{window.SonicAPI.listPlugins().then(async data=>{try{const catResp=(await window.SonicAPI.getSoundfontCatalog?.())??(await fetch('/api/v1/plugins/soundfonts/catalog').then(r=>r.json()));const catalog=catResp.full_catalog||{};data.soundfonts=(data.soundfonts||[]).map(sf=>{const sfId=sf.id.replace('sf_','');const catEntry=catalog[sfId.toLowerCase()]||catalog[sfId];if(catEntry&&catEntry.instruments){return{...sf,presets:catEntry.instruments};}return sf;});}catch(e){console.warn('Catalog fetch error:',e);}setInstrumentSelectorData(data);}).catch(()=>{});},[instrumentRefreshKey]);useEffect(()=>{if(!instrumentDropdownTrackId)return;const handler=e=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setInstrumentSearchQuery('');};document.addEventListener('click',handler);return()=>document.removeEventListener('click',handler);},[instrumentDropdownTrackId]);const GM_INSTRUMENTS=["Acoustic Grand Piano","Bright Acoustic Piano","Electric Grand Piano","Honky-tonk Piano","Electric Piano 1","Electric Piano 2","Harpsichord","Clavi","Celesta","Glockenspiel","Music Box","Vibraphone","Marimba","Xylophone","Tubular Bells","Dulcimer","Drawbar Organ","Percussive Organ","Rock Organ","Church Organ","Reed Organ","Accordion","Harmonica","Tango Accordion","Acoustic Guitar (nylon)","Acoustic Guitar (steel)","Electric Guitar (jazz)","Electric Guitar (clean)","Electric Guitar (muted)","Overdriven Guitar","Distortion Guitar","Guitar harmonics","Acoustic Bass","Electric Bass (finger)","Electric Bass (pick)","Fretless Bass","Slap Bass 1","Slap Bass 2","Synth Bass 1","Synth Bass 2","Violin","Viola","Cello","Contrabass","Tremolo Strings","Pizzicato Strings","Orchestral Harp","Timpani","String Ensemble 1","String Ensemble 2","Synth Strings 1","Synth Strings 2","Choir Aahs","Voice Oohs","Synth Voice","Orchestra Hit","Trumpet","Trombone","Tuba","Muted Trumpet","French Horn","Brass Section","Synth Brass 1","Synth Brass 2","Soprano Sax","Alto Sax","Tenor Sax","Baritone Sax","Oboe","English Horn","Bassoon","Clarinet","Piccolo","Flute","Recorder","Pan Flute","Blown Bottle","Shakuhachi","Whistle","Ocarina","Lead 1 (square)","Lead 2 (sawtooth)","Lead 3 (calliope)","Lead 4 (chiff)","Lead 5 (charang)","Lead 6 (voice)","Lead 7 (fifths)","Lead 8 (bass+lead)","Pad 1 (new age)","Pad 2 (warm)","Pad 3 (polysynth)","Pad 4 (choir)","Pad 5 (bowed)","Pad 6 (metallic)","Pad 7 (halo)","Pad 8 (sweep)","FX 1 (rain)","FX 2 (soundtrack)","FX 3 (crystal)","FX 4 (atmosphere)","FX 5 (brightness)","FX 6 (goblins)","FX 7 (echoes)","FX 8 (sci-fi)","Sitar","Banjo","Shamisen","Koto","Kalimba","Bag pipe","Fiddle","Shanai","Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal","Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"];const setTrackInstrumentWithProgram=(trackId,instrumentId,programNumber,displayName,bankNumber)=>{const isSfInstrument=instrumentId&&typeof instrumentId==='string'&&instrumentId.startsWith('sf_');const sfBank=bankNumber!==undefined?bankNumber:isSfInstrument?0:undefined;const sfProg=programNumber!==undefined?programNumber:undefined;var mt=activeTracksRef.current||tracks;var curTrk=null;for(var ci=0;cie.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);},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);},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"},/*#__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"},/*#__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();setFxSelectorTrackId(track.id);},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("button",{onClick:e=>{e.stopPropagation();openInstrumentSelector(track.id);},className:"px-2.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-[120px]"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"truncate text-[10px]"},track.instrumentName||track.instrumentId||"Synth"),/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-down",className:"w-3 h-3 shrink-0"}))),/*#__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) +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();setFxSelectorTrackId(track.id);},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("button",{onClick:e=>{e.stopPropagation();openInstrumentSelector(track.id);},className:"px-2.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-[120px]"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"truncate text-[10px]"},track.instrumentName||track.instrumentId||"Synth"),/*#__PURE__*/React.createElement("i",{"data-lucide":"chevron-down",className:"w-3 h-3 shrink-0"}))),function(){var _e=typeof window.SonicVstiAutosample!=='undefined'?window.SonicVstiAutosample.entryFor(track.synth_engine):null;return _e?/*#__PURE__*/React.createElement("span",{className:"px-1.5 py-0.5 bg-zinc-900 border border-zinc-800 text-emerald-400/90 rounded text-[9px] font-mono whitespace-nowrap shrink-0",title:"Autosampled: "+(_e.note_count||0)+" notes"},window.SonicVstiAutosample.formatSize(_e.size_bytes)+" \u00b7 "+(_e.note_count||0)+"n"):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) if(window.__mediaExplorerDragFile){const f=await resolveMediaExplorerDropFile();window.__mediaExplorerDragFile=null;if(!f)return;if(/\.mid$|\.midi$/i.test(f.name||'')){handleDropMidiToNewTracks(f);}else if(selectedTrackId){loadFileOnTrack(selectedTrackId,f);}return;}const f=e.dataTransfer.files&&e.dataTransfer.files[0];if(!f)return;if(/\.mid$|\.midi$/i.test(f.name||'')){handleDropMidiToNewTracks(f);}else if(selectedTrackId){loadFileOnTrack(selectedTrackId,f);}},onMouseDown:e=>{if(e.ctrlKey&&!e.shiftKey&&!e.altKey&&e.button===0){const wrapper=timelineWrapperRef.current;if(wrapper){const rect=wrapper.getBoundingClientRect();const sl=wrapper.scrollLeft;const raw=Math.max(0,(e.clientX-rect.left+sl)/zoom-leadInMargin);const time=snapValue!=='free'?snapTime(raw,snapValue,bpm):raw;handleSweepSelectStart(null,time);}}}},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 border-b border-[#141414] hover:bg-zinc-850/5 transition-colors ${isSelected?'bg-zinc-800/10':''}`,onDragOver:e=>e.preventDefault(),onDrop:async e=>{e.preventDefault();e.stopPropagation();// Media Explorer drag (client FS handle / server file_id / local path) if(window.__mediaExplorerDragFile){const f=await resolveMediaExplorerDropFile();window.__mediaExplorerDragFile=null;if(f)loadFileOnTrack(track.id,f);return;}const f=e.dataTransfer.files&&e.dataTransfer.files[0];if(!f)return;loadFileOnTrack(track.id,f);},onMouseEnter:()=>{setHoveredTrackId(track.id);hoveredTrackIdRef.current=track.id;}},/*#__PURE__*/React.createElement(WaveformLane,{track:track,zoom:zoom,timelineWidth:timelineWidth,viewportWidth:viewportWidth,scrollLeft:scrollLeft,onSelectRange:handleSelectRange,onPlayheadSet:handlePlayheadSet,isSelected:isSelected,selectedItemIds:selectedItemIds,onSelectTrack:setSelectedTrackId,markers:track.markers,onTrackLaneMouseDown:handleTrackLaneMouseDown,onClearSelection:()=>{captureSelectionUndo();setSelectedItemIds(new Set());},onSweepSelectStart:handleSweepSelectStart,onDeselectItem:handleDeselectItem,onAddToSelection:handleAddToSelection,onSetPendingDrag:handleSetPendingDrag,onSetPendingDragMove:handleSetPendingDragMove,onContextMenu:handleContextMenu,onClipDragStart:handleClipDragStart,onClipStretchStart:handleClipStretchStart,onSectionItemDragStart:handleSectionItemDragStart,onSectionItemResizeStart:handleSectionItemResizeStart,onSelectionEdgeDragStart:handleSelectionEdgeDragStart,setSelectedClipId:setSelectedClipId,selectedClipId:selectedClipId,activeTool:activeTool,onSplitTrackAtTime:handleSplitTrackAtTime,onEditClipInSubTab:handleEditClipInSubTab,onEditSectionInTab:handleEditSectionInTab,onEditMidiInTab:handleEditMidiInTab,snapValue:snapValue,bpm:bpm,selectionMode:selectionMode,localSelectionTrackId:localSelectionTrackId,localSelectionStart:localSelectionStart,currentTime:currentTime,getLocalAnchor:()=>localSelectionAnchorRef.current,onClearLocalSelection:()=>{captureSelectionUndo();clearLocalSelection();},onSetSelectionMode:mode=>{captureSelectionUndo();setSelectionMode(mode);},onSetSelectionStart:val=>{captureSelectionUndo();setSelectionStart(val);},onSetSelectionEnd:val=>{captureSelectionUndo();setSelectionEnd(val);},onSetCurrentTime:setCurrentTime,onSetLocalSelectionTrackId:setLocalSelectionTrackId,onSetLocalSelectionStart:setLocalSelectionStart,onSetLocalSelectionEnd:setLocalSelectionEnd,localSelLeft:localSelectionStart!==null&&localSelectionEnd!==null?Math.min(localSelectionStart,localSelectionEnd):null,localSelRight:localSelectionStart!==null&&localSelectionEnd!==null?Math.max(localSelectionStart,localSelectionEnd):null,scrollLeft:scrollLeft,recordingState:recordingState,recTempMidiNotes:recTempMidiNotes,recTempAudioBuffer:recTempAudioBuffer,recStartTimelineTime:recStartTimelineTime,canvasRedrawCount:canvasRedrawCount}),selectionMode==='local'&&localSelectionTrackId===track.id&&localSelectionStart!==null&&localSelectionEnd!==null&&Math.abs(localSelectionEnd-localSelectionStart)>0&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 border-l border-r border-amber-400 bg-amber-500/20 z-20 pointer-events-none",style:{left:`${Math.min(localSelectionStart,localSelectionEnd)*zoom}px`,width:`${Math.abs(localSelectionEnd-localSelectionStart)*zoom}px`}},/*#__PURE__*/React.createElement("div",{className:"absolute -left-[1px] top-0 bottom-0 w-[2px] bg-amber-400 z-30 pointer-events-auto cursor-ew-resize",onMouseDown:e=>handleHandleDragStart(e,'left')}),/*#__PURE__*/React.createElement("div",{className:"absolute -right-[1px] top-0 bottom-0 w-[2px] bg-amber-400 z-30 pointer-events-auto cursor-ew-resize",onMouseDown:e=>handleHandleDragStart(e,'right')})),sweepSelect&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 border-l border-r border-amber-400 bg-amber-500/10 z-20 pointer-events-none",style:{left:`${Math.min(sweepSelect.startTime,sweepSelect.endTime)*zoom}px`,width:`${Math.abs(sweepSelect.endTime-sweepSelect.startTime)*zoom}px`}}),track.buffer&&/*#__PURE__*/React.createElement("div",{className:"absolute right-2 top-2 flex items-center gap-1 z-10 opacity-70 hover:opacity-100"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleSplitTrack(track.id),className:"px-1.5 py-0.5 bg-zinc-900/95 text-zinc-300 rounded text-xs flex items-center gap-1 border border-zinc-700/50"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-2.5 h-2.5 text-cyan-400"}))," Cắt")),/*#__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] flex items-center justify-center text-xs text-zinc-500 hover:bg-zinc-900/20 cursor-pointer select-none border-t border-dashed border-zinc-800",onMouseEnter:()=>{if(draggedClipRef.current||draggedSectionItemRef.current){setHoveredTrackId(addNewTrack());}},onClick:addNewTrack},/*#__PURE__*/React.createElement("span",{className:"flex items-center gap-1 text-zinc-400"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus",className:"w-3.5 h-3.5"}))," Kéo clip xuống hoặc Click tạo Track")),selectionMode==='global'&&selLeft!==null&&selRight!==null&&selRight>selLeft&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 selection-interactive-box cursor-grab active:cursor-grabbing",style:{left:`${selLeft*zoom}px`,width:`${(selRight-selLeft)*zoom}px`},onMouseDown:handleSelectionBodyDragStart},/*#__PURE__*/React.createElement("div",{className:"absolute -left-[1px] top-0 bottom-0 w-[1px] bg-amber-500 hover:bg-amber-400 cursor-ew-resize z-30",onMouseDown:e=>handleHandleDragStart(e,'left')}),/*#__PURE__*/React.createElement("div",{className:"absolute -right-[1px] top-0 bottom-0 w-[1px] bg-amber-500 hover:bg-amber-400 cursor-ew-resize z-30",onMouseDown:e=>handleHandleDragStart(e,'right')})),/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 w-[2px] bg-[#ef4444] z-20 pointer-events-none",style:{left:`${playheadLeftPos}px`}},/*#__PURE__*/React.createElement("div",{className:"w-3 h-3 bg-[#ef4444] rotate-45 transform -translate-x-1/2 -translate-y-1/2 absolute top-0"})))))):(()=>{const st=subTabs.find(s=>s.id===activeTab);if(!st)return null;if(st.type==='PIANO_ROLL'){return/*#__PURE__*/React.createElement(PianoRollTabEditor,{st:st,zoom:zoom,bpm:bpm,viewportWidth:viewportWidth,activeTracks:activeTracks,onClose:()=>closeSubTab(st.id),onUpdateNotes:handleUpdateMidiNotes,onSaveNotes:handleSaveMidiNotes,setSubTabs:setSubTabs,onPlayPause:handlePlayPause,onStop:stopAllPlayback,isPlaying:isPlaying,playPreviewNote:playMidiPreviewNote,showToast:showToast,midiDevices:midiDevices,recordingState:recordingState,recTempMidiNotes:recTempMidiNotes,onRecord:handleRecordClick,selectedMidiInputId:selectedMidiInputId,onMidiInputSelect:handleMidiInputSelect,activeMidiPitches:activeMidiPitches,onInstrumentSelect:trackId=>{openInstrumentSelector(trackId);},onRealtimePlay:handlePianoRollRealtimePlay,onCopyNotes:n=>setPianoRollClipboard(JSON.parse(JSON.stringify(n||[]))),clipboardNotes:pianoRollClipboard,onExportMidiAudio:handleExportMidiAudio,onRescheduleMidi:updatedNotes=>{const playingSub=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL'&&s.isPlaying);if(playingSub){const offset=playingSub.currentTime||0;const ctx=getAudioContext();const tNode=activeTrackNodesRef.current[playingSub.trackId];if(tNode&&tNode.gainNode){tNode.gainNode.gain.setValueAtTime(tNode.gainNode.gain.value||1,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(0.001,ctx.currentTime+0.04);}setTimeout(()=>{window.SonicSF.stopAll();_panicMidi();if(tNode&&tNode.gainNode){const trackData=activeTracksRef.current?activeTracksRef.current.find(t=>t.id===playingSub.trackId):null;const volDb=trackData?trackData.volumeDb??0:0;const volLinear=volDb<=-50?0:Math.pow(10,volDb/20);tNode.gainNode.gain.setValueAtTime(0.001,ctx.currentTime);tNode.gainNode.gain.linearRampToValueAtTime(volLinear||0.8,ctx.currentTime+0.015);}startOffsetTimeRef.current=offset;startAudioTimeRef.current=ctx.currentTime;startBufferOffsetRef.current=offset*(playingSub.speed||1.0);schedulePianoRollMidi(playingSub,offset,updatedNotes);},50);}},onSeekPlayhead:clickTime=>{const seekSt=subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');if(!seekSt)return;if(seekSt.isPlaying){stopAllPlayback();window.SonicSF.stopAll();_panicMidi();setSubTabs(prev=>prev.map(s=>s.id===seekSt.id?{...s,currentTime:clickTime,isPlaying:true}:s));const ctx=getAudioContext();startOffsetTimeRef.current=clickTime;startAudioTimeRef.current=ctx.currentTime;startBufferOffsetRef.current=clickTime*(seekSt.speed||1.0);schedulePianoRollMidi(seekSt,clickTime);startSubTabPlayback(seekSt,clickTime);}else{setSubTabs(prev=>prev.map(s=>s.id===seekSt.id?{...s,currentTime:clickTime}:s));}}});}const subTrack=tracks.find(t=>t.id===st.trackId);const vTrack=subTrack?{...subTrack,buffer:st.buffer,isSubTab:true}:null;const subTabDuration=st.type==='PIANO_ROLL'?(()=>{const notes=st.notes||[];const beatSec=60.0/(parseFloat(bpm)||120);let maxEnd=0;notes.forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});return maxEnd*beatSec+1.0;})():st.buffer&&'duration'in st.buffer?st.buffer.duration:4.0;const subTabTimelineWidth=Math.max(zoom*subTabDuration,viewportWidth);return/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"shrink-0 z-30 bg-[#262626] overflow-y-scroll no-scrollbar flex flex-col border-r border-zinc-900",style:{width:tcpWidth+'px',scrollbarWidth:'none',msOverflowStyle:'none'}},/*#__PURE__*/React.createElement("div",{className:"sticky top-0 z-50 flex items-center h-10 border-b border-zinc-900 bg-[#242424] px-2 justify-between shrink-0"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-zinc-500 uppercase"},"Sub-Tab"),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSubTab(st.id),className:"px-1.5 py-0.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400 rounded border border-zinc-700 flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))," Close")),vTrack?/*#__PURE__*/React.createElement("div",{key:vTrack.id,className:"flex-1 flex flex-col p-2.5 bg-[#1e1e1e] border-r border-zinc-900 border-l-4 border-l-cyan-500"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-2"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("label",{className:"cursor-pointer relative block"},/*#__PURE__*/React.createElement("input",{type:"color",value:vTrack.color||'#0f766e',onClick:e=>e.stopPropagation(),onMouseDown:e=>e.stopPropagation(),onChange:e=>{e.stopPropagation();updateTrackColor(vTrack.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:vTrack.color}})),editingTrackName===vTrack.id?/*#__PURE__*/React.createElement("input",{type:"text",value:editNameInput,autoFocus:true,onChange:e=>setEditNameInput(e.target.value),onBlur:()=>{updateTrackName(vTrack.id,editNameInput||vTrack.name);setEditingTrackName(null);},onKeyDown:e=>{if(e.key==='Enter'){updateTrackName(vTrack.id,editNameInput||vTrack.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(vTrack.id);setEditNameInput(vTrack.name);}},vTrack.name)),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackMute(vTrack.id);},className:`px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.muted?'bg-red-950 text-red-400 border-red-700':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},"M"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();toggleTrackSoloEvaluate(vTrack.id);},className:`px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.solo?'bg-amber-950 text-amber-400 border-amber-600':'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}`},"S"))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2.5 text-[14px] mb-3"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"w-10 text-right text-zinc-500 text-[14px]"},"Vol:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-50",max:"7",step:"0.5",id:`tcp-vol-${st.id}`,value:vTrack.volumeDb??0,onChange:e=>updateTrackVolumeDb(vTrack.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-16 text-right font-mono text-zinc-300 text-[14px]",id:`tcp-vol-label-${st.id}`},vTrack.volumeDb??0,"dB")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"w-10 text-right text-zinc-500 text-[14px]"},"Pan:"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-100",max:"100",step:"1",id:`tcp-pan-${st.id}`,value:vTrack.pan??0,onChange:e=>updateTrackPan(vTrack.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-16 text-right font-mono text-zinc-300 text-[14px]",id:`tcp-pan-label-detailed-${st.id}`},vTrack.pan>0?'R'+vTrack.pan:vTrack.pan<0?'L'+Math.abs(vTrack.pan):'C')),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 text-[14px]"},/*#__PURE__*/React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isLooping:!s.isLooping}:s)),className:`px-2 py-0.5 text-[14px] rounded border font-bold ${st.isLooping?'bg-amber-700 text-white border-amber-500':'bg-zinc-800 text-zinc-400 border-zinc-700'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"repeat",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>updateSubTabEffects(st.id,{reverse:!(st.effects||{}).reverse}),className:`px-2 py-0.5 rounded border text-[14px] font-bold ${(st.effects||{}).reverse?'bg-zinc-600 text-white border-zinc-500':'bg-zinc-800 text-zinc-400 border-zinc-700'} flex items-center justify-center`,title:"Reverse"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"arrow-left-right",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("span",{className:"w-10 text-right text-zinc-500 text-[14px]"},"Loop:"),/*#__PURE__*/React.createElement("input",{type:"number",min:"0",max:"999",value:st.loopCount||0,onChange:e=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,loopCount:Math.max(0,parseInt(e.target.value)||0)}:s)),className:"w-12 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono",title:"Loop count"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[14px] text-zinc-500 mb-1 font-semibold uppercase mt-2"},/*#__PURE__*/React.createElement("span",null,"DSP"),/*#__PURE__*/React.createElement("span",{className:"font-mono text-zinc-600 text-[11px] normal-case"},"áp dụng vùng chọn / cả clip")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-3 gap-1 mb-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTabEffect(st.id,'invert_phase',0),className:"py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"arrow-down-up",className:"w-3 h-3"})),"Phase Inv"),/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTabEffect(st.id,'swap_channels',0),className:"py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"shuffle",className:"w-3 h-3"})),"Swap L/R"),/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTabEffect(st.id,'reverse',0),className:"py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"arrow-left-right",className:"w-3 h-3"})),"Reverse")),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 justify-between my-2.5"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"},/*#__PURE__*/React.createElement("span",{className:"text-[14px] font-bold text-zinc-400 uppercase tracking-tighter",title:"Normalize"},"Norm"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"0",step:"0.1",value:subTabNormVal,onChange:e=>setSubTabNormVal(parseFloat(e.target.value)),style:{writingMode:'vertical-lr',direction:'rtl',height:'120px',width:'20px',accentColor:'#a1a1aa'},className:"my-2 cursor-pointer"}),/*#__PURE__*/React.createElement("span",{className:"text-[14px] font-mono text-zinc-300 font-bold"},subTabNormVal,"dB"),/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTabEffect(st.id,'normalize',subTabNormVal),className:"mt-1.5 px-1 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 rounded text-[14px] font-bold w-full uppercase border border-zinc-650"},"Apply")),/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"},/*#__PURE__*/React.createElement("span",{className:"text-[14px] font-bold text-zinc-400 uppercase tracking-tighter",title:"Pitch Shift"},"Pitch"),/*#__PURE__*/React.createElement("input",{type:"range",min:"-12",max:"12",step:"0.5",value:subTabPitchVal,onChange:e=>setSubTabPitchVal(parseFloat(e.target.value)),style:{writingMode:'vertical-lr',direction:'rtl',height:'120px',width:'20px',accentColor:'#a1a1aa'},className:"my-2 cursor-pointer"}),/*#__PURE__*/React.createElement("span",{className:"text-[14px] font-mono text-zinc-300 font-bold"},subTabPitchVal>0?'+':'',subTabPitchVal,"st"),/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTabEffect(st.id,'pitch',subTabPitchVal),className:"mt-1.5 px-1 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 rounded text-[14px] font-bold w-full uppercase border border-zinc-650"},"Apply")),/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"},/*#__PURE__*/React.createElement("span",{className:"text-[14px] font-bold text-zinc-400 uppercase tracking-tighter",title:"Gain Multiplier"},"Gain"),/*#__PURE__*/React.createElement("input",{type:"range",min:"0",max:"150",step:"1",value:subTabGainVal,onChange:e=>setSubTabGainVal(parseInt(e.target.value)),style:{writingMode:'vertical-lr',direction:'rtl',height:'120px',width:'20px',accentColor:'#a1a1aa'},className:"my-2 cursor-pointer"}),/*#__PURE__*/React.createElement("span",{className:"text-[14px] font-mono text-zinc-300 font-bold"},subTabGainVal,"%"),/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTabEffect(st.id,'gain',subTabGainVal),className:"mt-1.5 px-1 py-1 bg-zinc-700 hover:bg-zinc-600 text-zinc-100 rounded text-[14px] font-bold w-full uppercase border border-zinc-650"},"Apply"))),/*#__PURE__*/React.createElement("div",{className:"flex-1"}),/*#__PURE__*/React.createElement("div",{className:"mt-auto pt-2.5 border-t border-zinc-800"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[14px] text-zinc-500 mb-1 font-semibold uppercase"},/*#__PURE__*/React.createElement("span",null,"Duration:"),/*#__PURE__*/React.createElement("span",{className:"font-mono text-zinc-300"},st.buffer?formatTime(subTabDuration):'0s')),/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between text-[14px] text-zinc-500 mb-2.5 font-semibold uppercase"},/*#__PURE__*/React.createElement("span",null,"SR:"),/*#__PURE__*/React.createElement("span",{className:"font-mono text-zinc-300"},st.buffer?st.buffer.sampleRate:0," Hz")),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-1 mb-2"},/*#__PURE__*/React.createElement("button",{onClick:async e=>{subTabAiTrackIdRef.current=st.trackId;handleAIScan();},disabled:analysisState.isRunning,className:"py-1 bg-purple-900 hover:bg-purple-800 text-purple-100 font-bold rounded text-xs border border-purple-700 flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"map-pin",className:"w-3 h-3"}))," Scan"),/*#__PURE__*/React.createElement("button",{onClick:async e=>{subTabAiTrackIdRef.current=st.trackId;handleAICutToNewTrack();},disabled:analysisState.isRunning,className:"py-1 bg-fuchsia-800 hover:bg-fuchsia-700 text-white font-bold rounded text-xs flex items-center justify-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3 h-3"}))," Cut"),/*#__PURE__*/React.createElement("button",{onClick:async e=>{subTabAiTrackIdRef.current=st.trackId;if(st.buffer){setSelectionRangeOnBuffer(st.buffer,st.selectionStart||0,st.selectionEnd||st.buffer.duration);}handleAIAnalysicLoop();},disabled:analysisState.isRunning,className:"py-1 bg-violet-800 hover:bg-violet-700 text-violet-100 font-bold rounded text-xs border border-violet-600 flex items-center justify-center gap-1 col-span-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sparkles",className:"w-3 h-3"}))," AI Analysic Loop")),/*#__PURE__*/React.createElement("button",{onClick:()=>exportSubTabBuffer(st.id),className:"w-full py-1.5 mb-1.5 bg-blue-700 hover:bg-blue-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"download",className:"w-4 h-4"}))," Export"),/*#__PURE__*/React.createElement("button",{onClick:()=>applySubTab(st.id),className:"w-full py-1.5 bg-emerald-700 hover:bg-emerald-600 text-white font-bold rounded text-[14px] flex items-center justify-center gap-1 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-4 h-4"}))," Save"))):/*#__PURE__*/React.createElement("div",{className:"flex-1 flex items-center justify-center text-xs text-zinc-500"},"Track not found")),/*#__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,className:"flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0"},/*#__PURE__*/React.createElement("div",{style:{width:`${subTabTimelineWidth}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:subTabTimelineWidth,viewportWidth:viewportWidth,onPlayheadSet:setCurrentTime,snapValue:snapValue,onRulerMouseDown:e=>{setSelectionFollowsTempo(false);const wrapper=timelineWrapperRef.current;if(!wrapper)return;const rect=wrapper.getBoundingClientRect();const sl=wrapper.scrollLeft;const raw=Math.max(0,(e.clientX-rect.left+sl)/zoom);const t=snapValue!=='free'?snapTime(raw,snapValue,bpm):raw;if(e.ctrlKey){e.preventDefault();e.stopPropagation();setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,selectionStart:null,selectionEnd:null}:s));return;}if(e.shiftKey){e.preventDefault();e.stopPropagation();}handlePlayheadSet(t);subTabDragStartRef.current=t;isDraggingSubTabRef.current=true;},scrollLeft:scrollLeft,canvasRedrawCount:canvasRedrawCount})),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex flex-col relative bg-[#111111] min-h-full"},vTrack&&/*#__PURE__*/React.createElement("div",{style:{height:`${subTabHeight}px`},className:"relative overflow-hidden border-2 border-purple-500/40 rounded bg-[#151515] mx-1 mt-1 shrink-0"},/*#__PURE__*/React.createElement(SubTabWaveform,{buffer:st.buffer,subTabId:st.id,activeTab:activeTab,activeTool:activeTool,currentTime:st.currentTime,selectionStart:st.selectionStart,selectionEnd:st.selectionEnd,onSelectRange:(start,end)=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:start,selectionEnd:end}:s)),onPlayheadSet:time=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:time}:s)),onContextMenu:(e,clickTime)=>setContextMenu({x:e.clientX,y:e.clientY,isSubTab:true,subTabId:st.id,time:clickTime}),zoom:zoom,timelineWidth:subTabTimelineWidth,color:vTrack.color,name:vTrack.name,speed:st.speed||1.0,volumeNodes:st.volumeNodes||[],panningNodes:st.panningNodes||[],fadeInLen:st.fadeInLen||0,fadeOutLen:st.fadeOutLen||0,graphMode:st.graphMode,channelInfo:st.channelInfo,selectedNodeTime:subTabSelectedNodeTime,setSelectedNodeTime:setSubTabSelectedNodeTime,onUpdateNodes:nodes=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,[s.graphMode==='pan'?'panningNodes':'volumeNodes']:nodes}:s)),onUpdateFade:fade=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,fadeInLen:fade.fadeInLen??s.fadeInLen,fadeOutLen:fade.fadeOutLen??s.fadeOutLen}:s)),onModeToggle:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,graphMode:s.graphMode==='pan'?null:'pan'}:s)),onSpeedChange:newSpeed=>{setSubTabs(prev=>prev.map(s=>{if(s.id!==st.id)return s;const oldSpeed=s.speed||1.0;const ratio=oldSpeed/newSpeed;const newVolumeNodes=(s.volumeNodes||[]).map(n=>({...n,time:n.time*ratio}));const newPanningNodes=(s.panningNodes||[]).map(n=>({...n,time:n.time*ratio}));return{...s,speed:newSpeed,volumeNodes:newVolumeNodes,panningNodes:newPanningNodes,fadeInLen:(s.fadeInLen||0)*ratio,fadeOutLen:(s.fadeOutLen||0)*ratio,currentTime:(s.currentTime||0)*ratio,label:s.label.replace(/\s\(\d+%\)$/,'')+` (${Math.round(newSpeed*100)}%)`};}));const n=activeTrackNodesRef.current[st.trackId];if(n&&n.source)n.source.playbackRate.value=newSpeed;// Reset time refs to prevent playhead jump when speed changes mid-playback const ctx=getAudioContext();const elapsed=ctx.currentTime-startAudioTimeRef.current;const oldSpeed=activePlaybackSpeedRef.current;const ratio=oldSpeed/newSpeed;startBufferOffsetRef.current=startBufferOffsetRef.current+elapsed*oldSpeed;startOffsetTimeRef.current=(startOffsetTimeRef.current+elapsed)*ratio;startAudioTimeRef.current=ctx.currentTime;activePlaybackSpeedRef.current=newSpeed;}}),/*#__PURE__*/React.createElement("div",{onMouseDown:handleSubTabResizeMouseDown,className:"absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize z-30 hover:bg-purple-500/50 transition-colors"})),st.selectionStart!==null&&st.selectionEnd!==null&&st.selectionEnd>st.selectionStart&&/*#__PURE__*/React.createElement("div",{className:"absolute top-0 bottom-0 border-l border-r border-amber-500 bg-amber-500/10 z-20 pointer-events-none",style:{left:`${Math.min(st.selectionStart,st.selectionEnd)*zoom}px`,width:`${Math.abs(st.selectionEnd-st.selectionStart)*zoom}px`}})))));})())),/*#__PURE__*/React.createElement("div",{className:"w-1 cursor-ew-resize hover:bg-cyan-500/50 active:bg-cyan-400 transition-colors shrink-0 z-10",onMouseDown:startColResize}),renderDock('right','Right')),renderDock('bottom','Bottom'),showMixer&&/*#__PURE__*/React.createElement("div",{className:"flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",style:{height:mixerHeight+'px'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",onMouseDown:e=>{e.preventDefault();var startY=e.clientY;var startH=mixerHeight;var onMove=function(ev){var newH=Math.max(80,Math.min(400,startH-(ev.clientY-startY)));setMixerHeight(newH);localStorage.setItem('studio_mixer_height',newH.toString());};var onUp=function(){document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-horizontal",className:"w-3 h-3 text-zinc-600"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-zinc-400 uppercase tracking-wider"},"MIXER")),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowMixer(false),className:"p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 flex overflow-x-auto p-1.5 gap-1.5 items-stretch"},/*#__PURE__*/React.createElement(MasterStripConsole,{masterVolume:masterVolume,setMasterVolume:setMasterVolume,showMasteringModal:showMasteringModal,setShowMasteringModal:setShowMasteringModal,masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings,isPlaying:isPlaying}),activeTracks.length>0&&/*#__PURE__*/React.createElement("div",{className:"w-px bg-zinc-700 shrink-0 self-stretch mx-0.5"}),activeTracks.map(function(track,idx){return/*#__PURE__*/React.createElement(TrackStripConsole,{key:track.id,track:track,index:idx,onUpdateTrack:updateTrackProp,trackVuRefs:trackVuRefs});}))),/*#__PURE__*/React.createElement("div",{className:"flex flex-col border-t border-zinc-700 bg-slate-900 shrink-0 select-none",style:{height:mediaExplorerPanelHeight+'px',display:showMediaExplorer?'flex':'none'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between px-2 py-0.5 bg-zinc-800/80 shrink-0 cursor-ns-resize",onMouseDown:e=>{e.preventDefault();var startY=e.clientY;var startH=mediaExplorerPanelHeight;var onMove=function(ev){var newH=Math.max(120,Math.min(520,startH-(ev.clientY-startY)));setMediaExplorerPanelHeight(newH);localStorage.setItem('studio_media_explorer_height',newH.toString());};var onUp=function(){document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-horizontal",className:"w-3 h-3 text-zinc-600"})),/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-emerald-400 uppercase tracking-wider"},"Media Explorer (F6)")),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowMediaExplorer(false),className:"p-0.5 rounded text-zinc-500 hover:text-zinc-200 hover:bg-zinc-700 transition"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"})))),/*#__PURE__*/React.createElement("div",{className:"flex-1 min-h-0 overflow-hidden bg-[#262626]"},/*#__PURE__*/React.createElement(MediaExplorerPanel,{height:mediaExplorerPanelHeight,clipboardRef:clipboardRef,active:showMediaExplorer}))));})(),/*#__PURE__*/React.createElement("div",{className:"h-9 bg-[#141414] border-t border-zinc-900 px-4 flex items-center justify-between text-[18px] text-zinc-500 select-none shrink-0"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-4"},/*#__PURE__*/React.createElement("span",null,"Status: ",isPlaying?'Playing':'Stopped'),activeTab!=='main'&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-semibold uppercase"},"Sub-Tab"),activeTab==='main'&&/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-semibold uppercase"},"Track: ID ",selectedTrackId),selectionMode==='local'&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-semibold uppercase text-[18px]"},"Local Sel"),selectionMode==='global'&&/*#__PURE__*/React.createElement("span",{className:"text-purple-400 font-semibold uppercase text-[18px]"},"Global Sel"),hasAnySolo&&/*#__PURE__*/React.createElement("span",{className:"text-amber-500 font-semibold"},"Solo: ",tracks.filter(t=>t.solo).length," track(s)"),isLoopingSelection&&selectionMode==='local'&&/*#__PURE__*/React.createElement("span",{className:"text-emerald-400 font-semibold uppercase text-[18px]"},"Solo Loop"),isLoopingSelection&&selectionMode!=='local'&&/*#__PURE__*/React.createElement("span",{className:"text-cyan-400 font-semibold uppercase text-[18px]"},"Master Loop")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1.5 ml-3"},/*#__PURE__*/React.createElement("span",{className:"text-[18px] font-bold text-zinc-400 uppercase"},"Bars:"),/*#__PURE__*/React.createElement("input",{type:"number",min:"0",value:beginBar,onChange:e=>{const b=parseInt(e.target.value)||0;setBeginBar(b);const beatDuration=60/parseInt(bpm||120);const t=b*beatDuration*4;clearLocalSelection();setSelectionMode('global');setSelectionStart(t);setSelectionEnd(t+beatDuration*4);},className:"w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"-"),/*#__PURE__*/React.createElement("input",{type:"number",min:"0",value:endBar,onChange:e=>{const b=parseInt(e.target.value)||0;setEndBar(b);const beatDuration=60/parseInt(bpm||120);const t=b*beatDuration*4;// selectionEnd = vạch bar b (không +1 bar) — khớp hiển thị endBar diff --git a/app/static/js/services/vstiAutosample.js b/app/static/js/services/vstiAutosample.js index 61bb097..1820c50 100644 --- a/app/static/js/services/vstiAutosample.js +++ b/app/static/js/services/vstiAutosample.js @@ -27,6 +27,17 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; } catch (e) {} } + function formatSize(bytes) { + if (!bytes) return '0 KB'; + if (bytes >= 1048576) return (bytes / 1048576).toFixed(1) + ' MB'; + return Math.max(1, Math.round(bytes / 1024)) + ' KB'; + } + + // UI re-render (chip hien thi note_count/size) khi autosample xong/fail + function notify(k) { + try { window.dispatchEvent(new CustomEvent('sf:autosample-update', { detail: { key: k } })); } catch (e) {} + } + // Key = plugin + preset → autosample lại khi đổi preset (âm preset mới). function keyFor(synthEngine) { if (!synthEngine) return ''; @@ -65,6 +76,7 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; } if (!window.SonicAPI || !window.SonicAPI.autosampleVsti) { recordFail(k, 'API autosample khong kha dung'); + notify(k); return Promise.resolve(null); } _inFlight[k] = new Promise(function (resolve) { @@ -80,14 +92,17 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; var map = readMap(); map[k] = { sf_id: res.sf_id, size_bytes: res.size_bytes || 0, note_count: res.note_count || 0, plugin_id: synthEngine.plugin_id, preset_id: synthEngine.preset_id || synthEngine.presetId || '', ts: Date.now() }; writeMap(map); + notify(k); resolve(res.sf_id); } else { recordFail(k, (res && res.error) ? String(res.error) : 'Server khong tra sf_id'); + notify(k); resolve(null); } }).catch(function (err) { delete _inFlight[k]; recordFail(k, (err && err.message) ? String(err.message) : 'Loi mang / server autosample'); + notify(k); resolve(null); }); }); @@ -115,6 +130,7 @@ window.SonicVstiAutosample = window.SonicVstiAutosample || {}; keyFor: keyFor, sfIdFor: sfIdFor, entryFor: entryFor, + formatSize: formatSize, ensure: ensure, failReasonFor: failReasonFor, clearCache: clearCache diff --git a/tests/test_autosampler.py b/tests/test_autosampler.py index 5dd40dd..0dd0061 100644 --- a/tests/test_autosampler.py +++ b/tests/test_autosampler.py @@ -1,11 +1,15 @@ """Tests cho tools/autosample_vsti.py — SF2 writer tự-sinh phải là RIFF/sfbk hợp lệ mà sf2utils parse được (không cần VSTi/pedalboard).""" import os +import sys import numpy as np import pytest from tools.autosample_vsti import write_sf2 +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FAKE_VST2 = os.path.join(ROOT, "native_host", "tests", "fake_vst2.dll") + sf2utils = pytest.importorskip("sf2utils.sf2parse") @@ -53,3 +57,20 @@ def test_write_sf2_odd_name_no_corruption(tmp_path, caplog): sf2 = sf2utils.Sf2File(f) assert len([s for s in sf2.samples if s.end > s.start]) == 1 assert not any("corrupted" in r.message for r in caplog.records) + +def test_autosample_vst2_native_bridge(tmp_path): + """Nhanh VST2: render qua native bridge (fake_vst2.dll) → SF2 hop le, + khong can pedalboard. Skip tren nen khong phai Windows / thieu DLL.""" + if sys.platform.startswith("win") and os.path.isfile(FAKE_VST2): + from tools.autosample_vsti import autosample_sf2 + out = str(tmp_path / "vst2.sf2") + res = autosample_sf2("fake_vst2", out, low=60, high=64, step=2, + duration=0.3, release=0.2, + extra_vst_dirs=[os.path.dirname(FAKE_VST2)]) + assert res["note_count"] == 3 + assert res["size_bytes"] == os.path.getsize(out) + with open(out, "rb") as f: + sf2 = sf2utils.Sf2File(f) + assert len([s for s in sf2.samples if s.end > s.start]) == 3 + else: + pytest.skip("fake_vst2.dll chi chay Windows (bridge ctypes WinDLL)") diff --git a/tools/autosample_vsti.py b/tools/autosample_vsti.py index a66097f..f874e95 100644 --- a/tools/autosample_vsti.py +++ b/tools/autosample_vsti.py @@ -64,20 +64,20 @@ def write_sf2(out_path, samples, sample_rate=44100, name="AutoSampled"): phdr += name.encode("ascii", "replace")[:20].ljust(20, b"\x00") phdr += struct.pack(" int16 mono, peak 0.9 (chung cho 2 engine).""" + mono = buf.mean(axis=0) + peak = float(np.max(np.abs(mono))) + if peak < 1e-5: + return None + above = np.nonzero(np.abs(mono) > peak * 0.001)[0] + start = int(above[0]) if above.size else 0 + audio = mono[start:] / peak * 0.9 + return np.clip(audio * 32767, -32768, 32767).astype(np.int16) + + +def _is_vst2_path(path): + """VST2 = .dll/.so dung rieng (Windows VST3 la FOLDER .vst3, .so VST3 + co duoi .vst3.so) — native bridge render duoc; pedalboard thi khong.""" + low = (path or "").lower() + if ".vst3" in low: + return False + return low.endswith(".dll") or low.endswith(".so") + + +def _render_note_native(plugin_path, note, sr, dur, release, velocity): + """VST2 qua native bridge (Windows) — pedalboard khong ho tro VST2.""" + from app.core.native_audio_service import get_service + bpm = 120.0 + y = get_service().render_vst2_offline( + plugin_path, + [{"start_beat": 0.0, "duration_beats": dur * bpm / 60.0, + "note": int(note) & 0x7F, "velocity": int(velocity)}], + bpm=bpm, sr=sr, tail_sec=release, + ) + return _postprocess(y, sr) + + def autosample_sf2(instrument_id, out_path, preset_id=None, preset_path=None, preset_data_b64=None, low=36, high=96, step=2, duration=2.5, - release=1.0, velocity=100, sample_rate=44100, name=None, log=None): + release=1.0, velocity=100, sample_rate=44100, name=None, log=None, + extra_vst_dirs=None): """Auto-sample VSTi -> SF2 (16-bit mono). Dung cho server endpoint /api/v1/plugins/autosample va CLI main(). + VST3 -> pedalboard (ap preset neu co); VST2 (.dll/.so khong .vst3) -> + native bridge (Windows) — pedalboard khong ho tro VST2, preset VST2 chua + qua bridge duoc nen bo qua. + Returns dict {note_count, out_path, size_bytes}. Raises FileNotFoundError neu plugin khong tim thay, RuntimeError neu - pedalboard khong kha dung hoac khong render duoc not nao. + engine khong kha dung hoac khong render duoc not nao. """ from app.core.vst_engine import PluginManager, apply_preset_to_plugin, HAS_PEDALBOARD - if not HAS_PEDALBOARD: - raise RuntimeError("pedalboard khong kha dung - khong auto-sample duoc") - vst = PluginManager().load_vst(instrument_id) - if vst is None: + plugin_path = PluginManager(extra_vst_dirs=extra_vst_dirs or [])._scan_plugins().get(instrument_id) + if not plugin_path: raise FileNotFoundError(f"Khong tim thay VSTi: {instrument_id} - hay Scan trong Plugin Manager truoc") - if preset_id or preset_path or preset_data_b64: - apply_preset_to_plugin(vst, preset_id=preset_id, preset_path=preset_path, - preset_data_b64=preset_data_b64) def _log(msg): if log: log(msg) + if _is_vst2_path(plugin_path): + # VST2: native bridge (Windows) — pedalboard khong ho tro. Preset chua + # export qua bridge (VST2AudioEngine chua co chunk/preset) -> bo qua. + if preset_id or preset_path or preset_data_b64: + _log("VST2: native bridge chua ho tro preset - bo qua, sample preset mac dinh") + def _render(note): + return _render_note_native(plugin_path, note, sample_rate, duration, + release, velocity) + else: + if not HAS_PEDALBOARD: + raise RuntimeError("pedalboard khong kha dung - khong auto-sample VST3 duoc") + vst = PluginManager(extra_vst_dirs=extra_vst_dirs or []).load_vst(instrument_id) + if vst is None: + raise FileNotFoundError(f"Khong tim thay VSTi: {instrument_id} - hay Scan trong Plugin Manager truoc") + if preset_id or preset_path or preset_data_b64: + apply_preset_to_plugin(vst, preset_id=preset_id, preset_path=preset_path, + preset_data_b64=preset_data_b64) + def _render(note): + return _render_note(vst, note, sample_rate, duration, release, velocity) + samples = [] for note in range(low, high + 1, step): - frames = _render_note(vst, note, sample_rate, duration, release, velocity) + frames = _render(note) if frames is None: _log(f"note {note}: silent, bo qua") continue