From a667d0354fd652deefe768581b7858dc595e8693 Mon Sep 17 00:00:00 2001 From: locphamtran Date: Fri, 21 Aug 2026 12:35:20 +0700 Subject: [PATCH] =?UTF-8?q?feat(chords):=20th=C3=AAm=20Chords=20Panel=20?= =?UTF-8?q?=E2=80=94=20chordTheory=20service=20+=20presets/custom/AI=20tab?= =?UTF-8?q?,=20insert=20progression=20v=C3=A0o=20timeline,=20Shift+K/menu/?= =?UTF-8?q?toolbar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- IMPLEMENT_PHASE_3.md | 12 +- app/static/js/app.jsx | 304 ++++++++++++++++++++- app/static/js/app.precompiled.js | 20 +- app/static/js/services/chordTheory.js | 171 ++++++++++++ app/static/js/services/chordTheory.test.js | 63 +++++ app/templates/index.html | 3 +- 6 files changed, 562 insertions(+), 11 deletions(-) create mode 100644 app/static/js/services/chordTheory.js create mode 100644 app/static/js/services/chordTheory.test.js diff --git a/IMPLEMENT_PHASE_3.md b/IMPLEMENT_PHASE_3.md index 3eeab67..4da4a33 100644 --- a/IMPLEMENT_PHASE_3.md +++ b/IMPLEMENT_PHASE_3.md @@ -88,7 +88,17 @@ - [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3); `test_vst2_gui` DUNE host + editor VSTGUI, proc alive. ## PHASE 6 — CHORDS PANEL -(chưa bắt đầu) +- [x] Service `app/static/js/services/chordTheory.js` (169 dòng, IIFE global `const ChordTheory` + `module.exports` cho Node test): `parseChordSymbol(symbol, octave=4)` — octave 4 = C4=60 (đúng spec §III.5: Cmaj7=[60,64,67,71], Am9=[69,72,76,79,83], Dm7=[62,65,69,72], G13=[67,71,74,77,81,88], Fadd9=[65,69,72,79]); giữ nguyên case suffix ('m9' minor vs 'M9' major); `parseRomanToken` regex `/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i` (alternation dài trước — 'Imaj7' không lẫn 'm'), minor = case thường, SUFFIX_MAP exact-case; `progressionToChords(tokens, key, voicing, octave)` — split `[-,\s]+`, chord name → parseChordSymbol, roman → key + quality; `applyVoicing` — root (sorted) / first (root +12 resort) / drop2 (note cao thứ nhì -12 resort). +- [x] Unit test `app/static/js/services/chordTheory.test.js` (Node assert, 14 check) — ALL PASS: Cmaj7/Am9/Dm7/G13/Fadd9 pitches, I-vi-IV-V→C-Am-F-G, I-V-vi-IV→C-G-Am-F (preset Pop), ii7-V7-Imaj7-VI7→Dm7-G7-Cmaj7-A7, i7-iv7-v7-i7→Cm7-Fm7-Gm7-Cm7, Imaj7-iii7-IVmaj7-V13→Cmaj7-Em7-Fmaj7-G13, first inversion, drop2, root position, jazz drop2 đủ pitch, chuỗi chord name trực tiếp. +- [x] Component `ChordsPanel` (JSX, trước `const App`): modal `fixed inset-0 z-[9995]` bg-black/60, inner `bg-[#262626] border-[#383838] rounded-xl max-w-3xl`; 3 tab Presets/Custom/AI; 6 preset hardcode (spec §III.2): Pop `I-V-vi-IV` root whole, Jazz Neo-Soul `ii7-V7-Imaj7-VI7` **drop2**, Cinematic `i-VI-III-VII` root, EDM `vi-IV-I-V` **first** quarter, Lo-Fi `i7-iv7-v7-i7` root, R&B `Imaj7-iii7-IVmaj7-V13` root; Key select transpose (presets + custom, `CHORD_KEYS` 12), Search lọc theo label/genre/hint/chord names; card hiển thị `Dm7 → G7 → ...`, [Preview Sound] (chord đầu: onNoteOn từng pitch vel 0.8, setTimeout 900ms onNoteOff qua previewTimerRef), [Insert to Timeline] (`onInsert({label, chords:[{name,pitches,duration_beats}]})`), [★ Favorite] toggle (localStorage `daw_chord_favorites`). +- [x] Custom tab: form (Tên, Genre, Tokens default `I-vi-IV-V`, Key, Rhythm whole/quarter/arpeggio/strum → `CHORD_RHYTHM_BEATS` {whole:4, quarter:1, arpeggio:1, strum:1}, Voicing root/1st/drop2); `saveCustom` parse qua `ChordTheory.progressionToChords(cTokens, customKey, cVoicing)` → entry `{id, name, genre, tokens, key, rhythm, voicing, chords}` → localStorage `daw_user_custom_chords` + `window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'))` + reload list; danh sách My Customs [Preview][Insert][Del]; listener effect lắng nghe CUSTOM_CHORD_SAVED → setCustomList. +- [x] AI tab: DEFER — message "AI chưa cấu hình...", không gọi aiGateway. +- [x] Tích hợp App: state `chordsPanelOpen`; `window.__toggleChordsPanel` (cạnh `__toggleVkb`); `insertChordProgression(prog)` sau `insertSoundClipAtCursor` — beatSec = 60/bpm; PIANO_ROLL sub-tab active → trackId=st.trackId, item=track.midiItems[st.target_id], insertBeat = activeSub.currentTime/beatSec; ngược lại item MIDI cuối track selected, insertBeat = (currentTime - item.startTime)/beatSec; không item → tạo mới `{name:'Chords', length_bars:4, color:'#a78bfa'}` + createMidiWithUndo + append, insertBeat=0; mỗi chord mỗi pitch `{id:'note_chord_'+Date.now()+'_'+ci+'_'+p, pitch, start_beat, duration_beats, velocity:0.8, pan:0.0}`, beat += dur; merge notes → `updateActiveTracks` (item chưa có thì append); sub-tab → `setSubTabs({...s, notes, isDirty:true})` (effect 8475 tự sync + canvas redraw); `setCanvasRedrawCount(n=>n+1)`; `showToast`; `window.__lastChordInsert = {trackId, itemId, notes}` (cho verify). +- [x] Menu Insert: item "Chords panel" icon 'guitar' shortcut 'Shift+K' → `window.__toggleChordsPanel()`; keydown transport: Shift+K (không ctrl/alt) trước block F7 → preventDefault + stopPropagation + toggle; piano-roll toolbar: nút "Chords" (sau select Input/Omni, trước div snap) → toggle; render `chordsPanelOpen && ` sau VirtualMidiKeyboard trước ExportModal. +- [x] index.html (repo + bundle `src-tauri/target/release/daw_engine/_internal/app/templates/`): thêm `` sau undoRedoEngine, bump app.precompiled.js `?v=202608211150`→`?v=202608211300`; copy `chordTheory.js` vào bundle `_internal/app/static/js/services/`. +- [x] Build: `node build.mjs` OK (Babel warning >500KB bình thường); copy `app.precompiled.js` sang bundle; restart app (taskkill sonicforge-daw.exe → chạy lại exe, PID mới). +- [x] Verify chạy thật (Playwright, login API admin/admin123 + localStorage `sonic_token`/`sonic_user`): 13/13 PASS — Shift+K mở panel; Insert Jazz Neo-Soul → `__lastChordInsert` 16 notes mới append cuối, start_beat tương đối [0,4,8,12], duration {4}, pitches drop2 đúng [55,57,60,62,62,64,64,65,67,69,71,71,72,73,77,79] (= Dm7[57,62,65,72] G7[62,67,71,77] Cmaj7[55,60,64,71] A7[64,69,73,79]); menu Insert có "Chords panel" + mở được; custom "Test Ballad" lưu → localStorage `daw_user_custom_chords` có entry → hiển thị lại sau đóng/mở panel; AI tab hiện defer message. +- [x] 3 regression cũ PASS: `shm_note_energy` energy 0 (app chưa load instrument — đúng); `test_sf2_ring` energy 12.337→0.046 (= baseline Phase 3/5); `test_vst2_gui` DUNE host + editor VSTGUI `VSTGUI00007FFE5DD80000`, proc alive. ## PHASE 7 — BUILD TỔNG & VERIFY (chưa bắt đầu) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 7ecb856..fecae63 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -10436,7 +10436,11 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); 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("div", { className: "flex items-center gap-0.5 shrink-0" }, React.createElement("button", { + }, 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: () => window.__toggleChordsPanel && window.__toggleChordsPanel(), + title: "Chords panel (Shift+K)", + className: "px-1.5 py-0.5 rounded text-xs font-bold bg-zinc-800 hover:bg-zinc-700 text-amber-400" + }, "Chords"), React.createElement("div", { className: "flex items-center gap-0.5 shrink-0" }, 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'}` @@ -16422,6 +16426,224 @@ const VirtualMidiKeyboard = ({ settings, onChange, tracks, activePitches, onNote /*#__PURE__*/React.createElement("div", { className: "px-2 pb-1.5 text-[9px] text-zinc-600" }, "QWERTY: Z S X D C V G B H N J M / Q 2 W 3 E R 5 T 6 Y 7 U I") ); }; +// ── Chords Panel (Phase 6) ───────────────────────────────────────────── +// Floating chord progression panel — Insert menu / Shift+K / piano-roll +// toolbar. Presets theo virtual_keyboard_and_chords_panel_spec.md §III.2. +const CHORD_PRESETS = [ + { id: 'pop', label: 'Pop', genre: 'Pop', tokens: 'I-V-vi-IV', key: 'C', voicing: 'root', rhythm: 'whole', hint: 'C → G → Am → F' }, + { id: 'jazz', label: 'Jazz Neo-Soul', genre: 'Jazz Neo-Soul', tokens: 'ii7-V7-Imaj7-VI7', key: 'C', voicing: 'drop2', rhythm: 'whole', hint: 'Dm7 → G7 → Cmaj7 → A7' }, + { id: 'cinematic', label: 'Cinematic', genre: 'Cinematic', tokens: 'i-VI-III-VII', key: 'C', voicing: 'root', rhythm: 'whole', hint: 'Cm → Ab → Eb → Bb' }, + { id: 'edm', label: 'EDM', genre: 'EDM', tokens: 'vi-IV-I-V', key: 'C', voicing: 'first', rhythm: 'quarter', hint: 'Am → F → C → G' }, + { id: 'lofi', label: 'Lo-Fi', genre: 'Lo-Fi', tokens: 'i7-iv7-v7-i7', key: 'C', voicing: 'root', rhythm: 'whole', hint: 'Cm7 → Fm7 → Gm7 → Cm7' }, + { id: 'rnb', label: 'R&B', genre: 'R&B', tokens: 'Imaj7-iii7-IVmaj7-V13', key: 'C', voicing: 'root', rhythm: 'whole', hint: 'Cmaj7 → Em7 → Fmaj7 → G13' } +]; +const CHORD_RHYTHM_BEATS = { whole: 4, quarter: 1, arpeggio: 1, strum: 1 }; +const CHORD_RHYTHM_LABELS = { whole: 'Whole (4 beats)', quarter: 'Quarter (1 beat)', arpeggio: 'Arpeggio', strum: 'Strum' }; +const CHORD_KEYS = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']; +const CHORD_VOICING_LABELS = { root: 'Root', first: '1st Inv', drop2: 'Drop 2' }; + +const ChordsPanel = ({ onClose, onInsert, onNoteOn, onNoteOff, bpm }) => { + const [tab, setTab] = useState('preset'); + const [search, setSearch] = useState(''); + const [key, setKey] = useState('C'); + const [favorites, setFavorites] = useState(function () { + try { return JSON.parse(localStorage.getItem('daw_chord_favorites') || '[]'); } catch (e) { return []; } + }); + const [customList, setCustomList] = useState(function () { + try { return JSON.parse(localStorage.getItem('daw_user_custom_chords') || '[]'); } catch (e) { return []; } + }); + const [cName, setCName] = useState(''); + const [cGenre, setCGenre] = useState(''); + const [cTokens, setCTokens] = useState('I-vi-IV-V'); + const [cRhythm, setCRhythm] = useState('whole'); + const [cVoicing, setCVoicing] = useState('root'); + const [customKey, setCustomKey] = useState('C'); + const previewTimerRef = useRef(null); + + useEffect(function () { + const onSaved = function () { + try { setCustomList(JSON.parse(localStorage.getItem('daw_user_custom_chords') || '[]')); } catch (e) {} + }; + window.addEventListener('CUSTOM_CHORD_SAVED', onSaved); + return function () { window.removeEventListener('CUSTOM_CHORD_SAVED', onSaved); }; + }, []); + + useEffect(function () { + try { localStorage.setItem('daw_chord_favorites', JSON.stringify(favorites)); } catch (e) {} + }, [favorites]); + + useEffect(function () { return function () { if (previewTimerRef.current) clearTimeout(previewTimerRef.current); }; }, []); + + const resolveProgression = function (tokens, progKey, progVoicing) { + try { return (window.ChordTheory || ChordTheory).progressionToChords(tokens, progKey || key, progVoicing || 'root'); } catch (e) { return []; } + }; + + const previewChord = function (chord) { + if (!chord || !chord.pitches || !chord.pitches.length) return; + const vel = 0.8; + chord.pitches.forEach(function (p) { try { onNoteOn(p, vel); } catch (e) {} }); + if (previewTimerRef.current) clearTimeout(previewTimerRef.current); + previewTimerRef.current = setTimeout(function () { + chord.pitches.forEach(function (p) { try { onNoteOff(p); } catch (e) {} }); + }, 900); + }; + + const toggleFavorite = function (id) { + setFavorites(function (prev) { return prev.indexOf(id) !== -1 ? prev.filter(function (x) { return x !== id; }) : prev.concat([id]); }); + }; + + const insertProg = function (label, chords, rhythm) { + const dur = CHORD_RHYTHM_BEATS[rhythm] != null ? CHORD_RHYTHM_BEATS[rhythm] : 4; + const payload = (chords || []).filter(function (c) { return c.pitches && c.pitches.length; }).map(function (c) { return { name: c.name || c.token, pitches: c.pitches, duration_beats: dur }; }); + if (!payload.length) { if (window.showToast) window.showToast('Không có chord để chèn', 'warning'); return; } + onInsert({ label: label, chords: payload }); + }; + + const saveCustom = function () { + const tokens = (cTokens || '').trim(); + if (!tokens) { if (window.showToast) window.showToast('Nhập progression tokens', 'warning'); return; } + let chords = []; + try { chords = (window.ChordTheory || ChordTheory).progressionToChords(tokens, customKey, cVoicing); } catch (e) {} + if (!chords.length || chords.some(function (c) { return !c.pitches || !c.pitches.length; })) { if (window.showToast) window.showToast('Progression không parse được', 'warning'); return; } + const entry = { id: 'custom_' + Date.now(), name: (cName || '').trim() || 'My Chord', genre: (cGenre || '').trim() || 'Custom', tokens: tokens, key: customKey, rhythm: cRhythm, voicing: cVoicing, chords: chords }; + try { + const list = JSON.parse(localStorage.getItem('daw_user_custom_chords') || '[]'); + list.push(entry); + localStorage.setItem('daw_user_custom_chords', JSON.stringify(list)); + setCustomList(list); + window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED')); + if (window.showToast) window.showToast('Đã lưu chord custom: ' + entry.name, 'success'); + } catch (e) { if (window.showToast) window.showToast('Lưu thất bại', 'error'); } + }; + + const deleteCustom = function (id) { + try { + const list = JSON.parse(localStorage.getItem('daw_user_custom_chords') || '[]').filter(function (x) { return x.id !== id; }); + localStorage.setItem('daw_user_custom_chords', JSON.stringify(list)); + setCustomList(list); + window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED')); + } catch (e) {} + }; + + const q = (search || '').trim().toLowerCase(); + const filteredPresets = CHORD_PRESETS.filter(function (p) { + if (!q) return true; + const chords = resolveProgression(p.tokens, key, p.voicing); + const hay = (p.label + ' ' + p.genre + ' ' + p.hint + ' ' + chords.map(function (c) { return c.name; }).join(' ')).toLowerCase(); + return hay.indexOf(q) !== -1; + }); + + return ( +
+
+
+

Chords Panel

+ +
+
+
+ {[['preset', 'Presets'], ['custom', 'Custom'], ['ai', 'AI']].map(function (t) { + return ; + })} +
+ {tab !== 'ai' && ( + + + {tab === 'preset' && ( + + )} + + )} +
+ + {tab === 'preset' && ( +
+ {filteredPresets.map(function (p) { + const chords = resolveProgression(p.tokens, key, p.voicing); + const display = chords.map(function (c) { return c.name; }).join(' → '); + const fav = favorites.indexOf(p.id) !== -1; + return ( +
+
+ {p.label} + {p.genre} · {CHORD_VOICING_LABELS[p.voicing]} +
+
{display || p.hint}
+
+ + + +
+
+ ); + })} + {!filteredPresets.length &&
Không tìm thấy preset.
} +
+ )} + + {tab === 'custom' && ( +
+
+
Build Custom Progression
+
+ + + +
+ + + +
+ +
+
+
+
My Customs ({customList.length})
+ {customList.length === 0 ? ( +
Chưa có custom nào — tạo bên trái rồi Save.
+ ) : ( +
+ {customList.map(function (c) { + return ( +
+
+
{c.name} ({c.genre})
+
{c.chords.map(function (x) { return x.name; }).join(' → ')}
+
+
+ + + +
+
+ ); + })} +
+ )} +
+
+ )} + + {tab === 'ai' && ( +
+
♪
+
AI Chord Suggestions
+
AI chưa cấu hình. Bật AI Gateway trong Settings để nhận gợi ý chord theo genre / cảm xúc.
+
+ )} +
+
+ ); +}; + const App = () => { // ── State Definitions ── @@ -17183,6 +17405,7 @@ const App = () => { const [vkbOpen, setVkbOpen] = useState(false); const vkbOpenRef = useRef(false); vkbOpenRef.current = vkbOpen; + const [chordsPanelOpen, setChordsPanelOpen] = useState(false); const vkbHeldRef = useRef(new Set()); const [vkbSettings, setVkbSettings] = useState(function () { try { @@ -19948,6 +20171,12 @@ const App = () => { handleSplitTrackRef.current(selectedTrackIdRef.current); return; } + if (e.shiftKey && !e.ctrlKey && !e.altKey && (e.key === 'K' || e.key === 'k')) { + e.preventDefault(); + e.stopPropagation(); + if (window.__toggleChordsPanel) window.__toggleChordsPanel(); + return; + } if (e.key === 'F7') { e.preventDefault(); e.stopPropagation(); @@ -22003,6 +22232,10 @@ const App = () => { }); }; + window.__toggleChordsPanel = function () { + setChordsPanelOpen(function (p) { return !p; }); + }; + // ── Server-side upload ── // WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có // serverFileId — upload sớm thất bại/race → clip mất sau reload). @@ -26567,6 +26800,64 @@ const App = () => { input.click(); }; + // ── Chords Panel insert (Phase 6) ─────────────────────────────────────── + const insertChordProgression = (prog) => { + const curTracks = activeTracks; + const pr = prog || {}; + const chordList = pr.chords || []; + if (!chordList.length) { showToast('Không có chord để chèn', 'warning'); return; } + const beatSec = 60.0 / (parseInt(bpm) || 120); + const secondsPerBar = beatSec * 4; + const activeSub = activeTab && subTabs.find(s => s.id === activeTab && s.type === 'PIANO_ROLL'); + let trackId = null; + let item = null; + let insertBeat = 0; + if (activeSub) { + trackId = activeSub.trackId; + const trk = curTracks.find(t => t.id === trackId); + if (trk) item = (trk.midiItems || []).find(m => m.id === activeSub.target_id) || null; + insertBeat = Math.max(0, (activeSub.currentTime || 0) / beatSec); + } else { + trackId = selectedTrackId; + const trk = curTracks.find(t => t.id === trackId); + if (!trk) { showToast('Chọn track trước', 'warning'); return; } + const items = trk.midiItems || []; + if (!items.length) { + const newItem = { id: 'midi_' + Date.now(), name: 'Chords', startTime: currentTime, duration: secondsPerBar * 4, length_bars: 4, notes: [], color: '#a78bfa' }; + createMidiWithUndo(trackId, newItem); + updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, midiItems: [...(t.midiItems || []), newItem] } : t)); + item = newItem; + insertBeat = 0; + } else { + item = items[items.length - 1]; + insertBeat = Math.max(0, (currentTime - (item.startTime || 0)) / beatSec); + } + } + if (!item) { showToast('Không tìm thấy MIDI item', 'warning'); return; } + const merged = (item.notes || []).slice(); + let beat = insertBeat; + chordList.forEach(function (ch, ci) { + const dur = ch.duration_beats != null ? ch.duration_beats : 4; + (ch.pitches || []).forEach(function (p) { + merged.push({ id: 'note_chord_' + Date.now() + '_' + ci + '_' + p, pitch: p, start_beat: beat, duration_beats: dur, velocity: 0.8, pan: 0.0 }); + }); + beat += dur; + }); + const finalItem = { ...item, notes: merged }; + updateActiveTracks(prev => prev.map(t => { + if (t.id !== trackId) return t; + const has = (t.midiItems || []).some(m => m.id === item.id); + if (!has) return { ...t, midiItems: [...(t.midiItems || []), finalItem] }; + return { ...t, midiItems: (t.midiItems || []).map(m => m.id === item.id ? finalItem : m) }; + })); + if (activeSub) { + setSubTabs(prev => prev.map(s => s.id === activeSub.id ? { ...s, notes: merged, isDirty: true } : s)); + } + setCanvasRedrawCount(n => n + 1); + showToast('Đã chèn ' + chordList.length + ' chord (' + merged.length + ' notes)', 'success'); + window.__lastChordInsert = { trackId: trackId, itemId: item.id, notes: merged }; + }; + // ── Server-side Export ── const triggerWavExport = async () => { // TỰ ĐỘNG bounce realtime nếu project có MIDI items: FluidSynth (ScriptProcessor) @@ -29768,6 +30059,11 @@ STRICT CONSTRAINTS: label: 'Insert sound clip', icon: 'file-input', action: insertSoundClipAtCursor + }, { + label: 'Chords panel', + icon: 'guitar', + shortcut: 'Shift+K', + action: () => window.__toggleChordsPanel() }, { label: 'Insert track', icon: 'plus', @@ -32971,6 +33267,12 @@ STRICT CONSTRAINTS: onNoteOn: vkbNoteOn, onNoteOff: vkbNoteOff, onClose: () => window.__toggleVkb() + }), chordsPanelOpen && /*#__PURE__*/React.createElement(ChordsPanel, { + onClose: () => window.__toggleChordsPanel(), + onInsert: insertChordProgression, + onNoteOn: vkbNoteOn, + onNoteOff: vkbNoteOff, + bpm: bpm }), /*#__PURE__*/React.createElement(ExportModal, { open: showExportPanel, onClose: () => setShowExportPanel(false), diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 5e73122..6eb6f9d 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -695,7 +695,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("div",{className:"flex items-center gap-0.5 shrink-0"},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')),st.instrumentId&&typeof st.instrumentId==='string'&&!st.instrumentId.startsWith('sf_')?React.createElement("button",{onClick:()=>{ensureAndOpenVstGui(st);},title:"Mở lại GUI VSTi",className:"px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 shrink-0 bg-zinc-800 text-violet-300 border-violet-800 hover:bg-violet-900/40"},React.createElement("i",{"data-lucide":"sliders-horizontal",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[10px]"},"GUI")):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:async()=>{if(!notes||!notes.length){showToast('Chưa có nốt nhạc để xuất MIDI','warning');return;}const payload={bpm:parseFloat(bpm)||120,ppq:480,tracks:[{name:st.label||'Track',notes:(notes||[]).map(n=>({pitch:n.pitch||60,start_beat:n.start_beat||0,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8}))}]};try{await serverDownloadMidi(payload,(st.label||'midi')+'.mid');showToast('Đã xuất file MIDI!','success');}catch(err){showToast('Export MIDI thất bại: '+(err&&err.message?err.message:String(err)),'error');}},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:()=>window.__toggleChordsPanel&&window.__toggleChordsPanel(),title:"Chords panel (Shift+K)",className:"px-1.5 py-0.5 rounded text-xs font-bold bg-zinc-800 hover:bg-zinc-700 text-amber-400"},"Chords"),React.createElement("div",{className:"flex items-center gap-0.5 shrink-0"},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')),st.instrumentId&&typeof st.instrumentId==='string'&&!st.instrumentId.startsWith('sf_')?React.createElement("button",{onClick:()=>{ensureAndOpenVstGui(st);},title:"Mở lại GUI VSTi",className:"px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 shrink-0 bg-zinc-800 text-violet-300 border-violet-800 hover:bg-violet-900/40"},React.createElement("i",{"data-lucide":"sliders-horizontal",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[10px]"},"GUI")):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:async()=>{if(!notes||!notes.length){showToast('Chưa có nốt nhạc để xuất MIDI','warning');return;}const payload={bpm:parseFloat(bpm)||120,ppq:480,tracks:[{name:st.label||'Track',notes:(notes||[]).map(n=>({pitch:n.pitch||60,start_beat:n.start_beat||0,duration_beats:n.duration_beats||1,velocity:n.velocity!=null?n.velocity:0.8}))}]};try{await serverDownloadMidi(payload,(st.label||'midi')+'.mid');showToast('Đã xuất file MIDI!','success');}catch(err){showToast('Export MIDI thất bại: '+(err&&err.message?err.message:String(err)),'error');}},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 @@ -946,7 +946,10 @@ const btnRect=btnSynthRef.current?btnSynthRef.current.getBoundingClientRect():nu // Floating draggable QWERTY keyboard — F2 / menu Tools. Không nuốt Space/R // (handler toàn cục chỉ bắt phím trong VKB_MAP). Persist localStorage. const VKB_WHITE_OFFSETS=[0,2,4,5,7,9,11,12,14,16,17,19,21,23,24];const VKB_BLACK_OFFSETS=[1,3,6,8,10,13,15,18,20,22];const VKB_KEY_LABELS=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const VKB_SCALES={'None':null,'Major':[0,2,4,5,7,9,11],'Minor':[0,2,3,5,7,8,10],'Pentatonic Major':[0,2,4,7,9],'Pentatonic Minor':[0,3,5,7,10],'Dorian':[0,2,3,5,7,9,10],'Mixolydian':[0,2,4,5,7,9,10]};const VirtualMidiKeyboard=({settings,onChange,tracks,activePitches,onNoteOn,onNoteOff,onClose})=>{const KEY_W=24;const KEY_H=84;const BLACK_W=14;const BLACK_H=54;const octave=Math.max(0,Math.min(7,parseInt(settings.octave)||4));const basePitch=(octave+1)*12;const scale=VKB_SCALES[settings.scale]||null;const inScale=pitch=>{if(!scale)return false;const pc=(pitch%12+12)%12;return scale.indexOf(pc)!==-1;};const velNum=Math.max(1,Math.min(127,parseInt(settings.velocity)||100));// Channel hiển thị: user đã chọn (1..16) hoặc mặc định = channel track arm/select đầu tiên -let displayChannel=parseInt(settings.channel)||null;if(displayChannel===null){try{const trks=tracks||[];if(trks.length){const t=trks.find(x=>x.isArmed)||trks[0];displayChannel=assignTrackMidiChannel(t,trks)+1;}}catch(e){}if(displayChannel===null)displayChannel=1;}const startDrag=e=>{e.preventDefault();const sx=e.clientX,sy=e.clientY;const ox=settings.x,oy=settings.y;const onMove=ev=>{onChange({x:Math.max(-280,Math.min(window.innerWidth-80,ox+(ev.clientX-sx))),y:Math.max(0,Math.min(window.innerHeight-36,oy+(ev.clientY-sy)))});};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const fireOn=(e,pitch)=>{e.preventDefault();e.stopPropagation();onNoteOn(pitch,velNum/127);};const whiteKeys=VKB_WHITE_OFFSETS.map(function(off,wi){const pitch=Math.min(127,basePitch+off);const active=activePitches&&activePitches.has(pitch);const sc=inScale(pitch);return/*#__PURE__*/React.createElement("div",{key:'vkbw'+off,onMouseDown:e=>fireOn(e,pitch),onMouseUp:()=>onNoteOff(pitch),onMouseLeave:()=>onNoteOff(pitch),style:{position:'absolute',left:wi*KEY_W,top:0,width:KEY_W,height:KEY_H,boxSizing:'border-box'},className:"flex items-end justify-center pb-1 text-[9px] font-bold cursor-pointer rounded-b transition "+(active?'bg-emerald-500 text-white':'bg-zinc-100 text-zinc-600 hover:bg-amber-100')+(sc?' ring-1 ring-inset ring-amber-400':'')},off%12===0?VKB_KEY_LABELS[0]+(Math.floor(pitch/12)-1):'');});const blackKeys=VKB_BLACK_OFFSETS.map(function(off){const pitch=Math.min(127,basePitch+off);const active=activePitches&&activePitches.has(pitch);const sc=inScale(pitch);const wi=VKB_WHITE_OFFSETS.indexOf(off-1);return/*#__PURE__*/React.createElement("div",{key:'vkbb'+off,onMouseDown:e=>fireOn(e,pitch),onMouseUp:()=>onNoteOff(pitch),onMouseLeave:()=>onNoteOff(pitch),style:{position:'absolute',left:(wi+1)*KEY_W-BLACK_W/2,top:0,width:BLACK_W,height:BLACK_H,boxSizing:'border-box',zIndex:5},className:"cursor-pointer rounded-b transition "+(active?'bg-emerald-600':'bg-zinc-950 hover:bg-zinc-800')+(sc?' ring-1 ring-inset ring-amber-400':'')});});return/*#__PURE__*/React.createElement("div",{style:{position:'fixed',left:settings.x,top:settings.y,zIndex:9990,width:15*KEY_W+12},className:"bg-[#1c1c1e] border border-zinc-700 rounded-xl shadow-2xl select-none"},/*#__PURE__*/React.createElement("div",{onMouseDown:startDrag,className:"flex items-center justify-between px-3 py-1.5 cursor-move bg-zinc-800 rounded-t-xl border-b border-zinc-700"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-zinc-300 tracking-wide"},"VIRTUAL MIDI KEYBOARD"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},"F2"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();onClose();},className:"text-zinc-500 hover:text-red-400 text-xs px-1"},"\u2715"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 px-2 py-1.5 border-b border-zinc-800 text-[10px] text-zinc-400 flex-wrap"},/*#__PURE__*/React.createElement("button",{onClick:()=>onChange({octave:Math.max(0,octave-1)}),className:"px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 rounded"},"Oct -"),/*#__PURE__*/React.createElement("span",{className:"font-mono text-amber-400"},"Octave: "+octave),/*#__PURE__*/React.createElement("button",{onClick:()=>onChange({octave:Math.min(7,octave+1)}),className:"px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 rounded"},"Oct +"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Vel",/*#__PURE__*/React.createElement("input",{type:"range",min:1,max:127,value:velNum,onChange:e=>onChange({velocity:parseInt(e.target.value)||100}),className:"w-16"}),/*#__PURE__*/React.createElement("span",{className:"font-mono w-7 text-right"},velNum)),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Ch",/*#__PURE__*/React.createElement("select",{value:displayChannel,onChange:e=>onChange({channel:parseInt(e.target.value)}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5"},Array.from({length:16},function(_,i){return/*#__PURE__*/React.createElement("option",{key:i+1,value:i+1},i+1);}))),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Tr",/*#__PURE__*/React.createElement("input",{type:"number",min:-24,max:24,value:settings.transpose,onChange:e=>onChange({transpose:parseInt(e.target.value)||0}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5 w-10 font-mono"})),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Scale",/*#__PURE__*/React.createElement("select",{value:settings.scale,onChange:e=>onChange({scale:e.target.value}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5"},Object.keys(VKB_SCALES).map(function(s){return/*#__PURE__*/React.createElement("option",{key:s,value:s},s);})))),/*#__PURE__*/React.createElement("div",{className:"relative mx-1 my-2",style:{width:15*KEY_W,height:KEY_H}},whiteKeys,blackKeys),/*#__PURE__*/React.createElement("div",{className:"px-2 pb-1.5 text-[9px] text-zinc-600"},"QWERTY: Z S X D C V G B H N J M / Q 2 W 3 E R 5 T 6 Y 7 U I"));};const App=()=>{// ── State Definitions ── +let displayChannel=parseInt(settings.channel)||null;if(displayChannel===null){try{const trks=tracks||[];if(trks.length){const t=trks.find(x=>x.isArmed)||trks[0];displayChannel=assignTrackMidiChannel(t,trks)+1;}}catch(e){}if(displayChannel===null)displayChannel=1;}const startDrag=e=>{e.preventDefault();const sx=e.clientX,sy=e.clientY;const ox=settings.x,oy=settings.y;const onMove=ev=>{onChange({x:Math.max(-280,Math.min(window.innerWidth-80,ox+(ev.clientX-sx))),y:Math.max(0,Math.min(window.innerHeight-36,oy+(ev.clientY-sy)))});};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);};const fireOn=(e,pitch)=>{e.preventDefault();e.stopPropagation();onNoteOn(pitch,velNum/127);};const whiteKeys=VKB_WHITE_OFFSETS.map(function(off,wi){const pitch=Math.min(127,basePitch+off);const active=activePitches&&activePitches.has(pitch);const sc=inScale(pitch);return/*#__PURE__*/React.createElement("div",{key:'vkbw'+off,onMouseDown:e=>fireOn(e,pitch),onMouseUp:()=>onNoteOff(pitch),onMouseLeave:()=>onNoteOff(pitch),style:{position:'absolute',left:wi*KEY_W,top:0,width:KEY_W,height:KEY_H,boxSizing:'border-box'},className:"flex items-end justify-center pb-1 text-[9px] font-bold cursor-pointer rounded-b transition "+(active?'bg-emerald-500 text-white':'bg-zinc-100 text-zinc-600 hover:bg-amber-100')+(sc?' ring-1 ring-inset ring-amber-400':'')},off%12===0?VKB_KEY_LABELS[0]+(Math.floor(pitch/12)-1):'');});const blackKeys=VKB_BLACK_OFFSETS.map(function(off){const pitch=Math.min(127,basePitch+off);const active=activePitches&&activePitches.has(pitch);const sc=inScale(pitch);const wi=VKB_WHITE_OFFSETS.indexOf(off-1);return/*#__PURE__*/React.createElement("div",{key:'vkbb'+off,onMouseDown:e=>fireOn(e,pitch),onMouseUp:()=>onNoteOff(pitch),onMouseLeave:()=>onNoteOff(pitch),style:{position:'absolute',left:(wi+1)*KEY_W-BLACK_W/2,top:0,width:BLACK_W,height:BLACK_H,boxSizing:'border-box',zIndex:5},className:"cursor-pointer rounded-b transition "+(active?'bg-emerald-600':'bg-zinc-950 hover:bg-zinc-800')+(sc?' ring-1 ring-inset ring-amber-400':'')});});return/*#__PURE__*/React.createElement("div",{style:{position:'fixed',left:settings.x,top:settings.y,zIndex:9990,width:15*KEY_W+12},className:"bg-[#1c1c1e] border border-zinc-700 rounded-xl shadow-2xl select-none"},/*#__PURE__*/React.createElement("div",{onMouseDown:startDrag,className:"flex items-center justify-between px-3 py-1.5 cursor-move bg-zinc-800 rounded-t-xl border-b border-zinc-700"},/*#__PURE__*/React.createElement("span",{className:"text-[10px] font-bold text-zinc-300 tracking-wide"},"VIRTUAL MIDI KEYBOARD"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},"F2"),/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();onClose();},className:"text-zinc-500 hover:text-red-400 text-xs px-1"},"\u2715"))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 px-2 py-1.5 border-b border-zinc-800 text-[10px] text-zinc-400 flex-wrap"},/*#__PURE__*/React.createElement("button",{onClick:()=>onChange({octave:Math.max(0,octave-1)}),className:"px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 rounded"},"Oct -"),/*#__PURE__*/React.createElement("span",{className:"font-mono text-amber-400"},"Octave: "+octave),/*#__PURE__*/React.createElement("button",{onClick:()=>onChange({octave:Math.min(7,octave+1)}),className:"px-1.5 py-0.5 bg-zinc-800 hover:bg-zinc-700 rounded"},"Oct +"),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Vel",/*#__PURE__*/React.createElement("input",{type:"range",min:1,max:127,value:velNum,onChange:e=>onChange({velocity:parseInt(e.target.value)||100}),className:"w-16"}),/*#__PURE__*/React.createElement("span",{className:"font-mono w-7 text-right"},velNum)),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Ch",/*#__PURE__*/React.createElement("select",{value:displayChannel,onChange:e=>onChange({channel:parseInt(e.target.value)}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5"},Array.from({length:16},function(_,i){return/*#__PURE__*/React.createElement("option",{key:i+1,value:i+1},i+1);}))),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Tr",/*#__PURE__*/React.createElement("input",{type:"number",min:-24,max:24,value:settings.transpose,onChange:e=>onChange({transpose:parseInt(e.target.value)||0}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5 w-10 font-mono"})),/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-1"},"Scale",/*#__PURE__*/React.createElement("select",{value:settings.scale,onChange:e=>onChange({scale:e.target.value}),className:"bg-zinc-800 text-zinc-300 rounded px-1 py-0.5"},Object.keys(VKB_SCALES).map(function(s){return/*#__PURE__*/React.createElement("option",{key:s,value:s},s);})))),/*#__PURE__*/React.createElement("div",{className:"relative mx-1 my-2",style:{width:15*KEY_W,height:KEY_H}},whiteKeys,blackKeys),/*#__PURE__*/React.createElement("div",{className:"px-2 pb-1.5 text-[9px] text-zinc-600"},"QWERTY: Z S X D C V G B H N J M / Q 2 W 3 E R 5 T 6 Y 7 U I"));};// ── Chords Panel (Phase 6) ───────────────────────────────────────────── +// Floating chord progression panel — Insert menu / Shift+K / piano-roll +// toolbar. Presets theo virtual_keyboard_and_chords_panel_spec.md §III.2. +const CHORD_PRESETS=[{id:'pop',label:'Pop',genre:'Pop',tokens:'I-V-vi-IV',key:'C',voicing:'root',rhythm:'whole',hint:'C → G → Am → F'},{id:'jazz',label:'Jazz Neo-Soul',genre:'Jazz Neo-Soul',tokens:'ii7-V7-Imaj7-VI7',key:'C',voicing:'drop2',rhythm:'whole',hint:'Dm7 → G7 → Cmaj7 → A7'},{id:'cinematic',label:'Cinematic',genre:'Cinematic',tokens:'i-VI-III-VII',key:'C',voicing:'root',rhythm:'whole',hint:'Cm → Ab → Eb → Bb'},{id:'edm',label:'EDM',genre:'EDM',tokens:'vi-IV-I-V',key:'C',voicing:'first',rhythm:'quarter',hint:'Am → F → C → G'},{id:'lofi',label:'Lo-Fi',genre:'Lo-Fi',tokens:'i7-iv7-v7-i7',key:'C',voicing:'root',rhythm:'whole',hint:'Cm7 → Fm7 → Gm7 → Cm7'},{id:'rnb',label:'R&B',genre:'R&B',tokens:'Imaj7-iii7-IVmaj7-V13',key:'C',voicing:'root',rhythm:'whole',hint:'Cmaj7 → Em7 → Fmaj7 → G13'}];const CHORD_RHYTHM_BEATS={whole:4,quarter:1,arpeggio:1,strum:1};const CHORD_RHYTHM_LABELS={whole:'Whole (4 beats)',quarter:'Quarter (1 beat)',arpeggio:'Arpeggio',strum:'Strum'};const CHORD_KEYS=['C','C#','D','Eb','E','F','F#','G','Ab','A','Bb','B'];const CHORD_VOICING_LABELS={root:'Root',first:'1st Inv',drop2:'Drop 2'};const ChordsPanel=({onClose,onInsert,onNoteOn,onNoteOff,bpm})=>{const[tab,setTab]=useState('preset');const[search,setSearch]=useState('');const[key,setKey]=useState('C');const[favorites,setFavorites]=useState(function(){try{return JSON.parse(localStorage.getItem('daw_chord_favorites')||'[]');}catch(e){return[];}});const[customList,setCustomList]=useState(function(){try{return JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]');}catch(e){return[];}});const[cName,setCName]=useState('');const[cGenre,setCGenre]=useState('');const[cTokens,setCTokens]=useState('I-vi-IV-V');const[cRhythm,setCRhythm]=useState('whole');const[cVoicing,setCVoicing]=useState('root');const[customKey,setCustomKey]=useState('C');const previewTimerRef=useRef(null);useEffect(function(){const onSaved=function(){try{setCustomList(JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]'));}catch(e){}};window.addEventListener('CUSTOM_CHORD_SAVED',onSaved);return function(){window.removeEventListener('CUSTOM_CHORD_SAVED',onSaved);};},[]);useEffect(function(){try{localStorage.setItem('daw_chord_favorites',JSON.stringify(favorites));}catch(e){}},[favorites]);useEffect(function(){return function(){if(previewTimerRef.current)clearTimeout(previewTimerRef.current);};},[]);const resolveProgression=function(tokens,progKey,progVoicing){try{return(window.ChordTheory||ChordTheory).progressionToChords(tokens,progKey||key,progVoicing||'root');}catch(e){return[];}};const previewChord=function(chord){if(!chord||!chord.pitches||!chord.pitches.length)return;const vel=0.8;chord.pitches.forEach(function(p){try{onNoteOn(p,vel);}catch(e){}});if(previewTimerRef.current)clearTimeout(previewTimerRef.current);previewTimerRef.current=setTimeout(function(){chord.pitches.forEach(function(p){try{onNoteOff(p);}catch(e){}});},900);};const toggleFavorite=function(id){setFavorites(function(prev){return prev.indexOf(id)!==-1?prev.filter(function(x){return x!==id;}):prev.concat([id]);});};const insertProg=function(label,chords,rhythm){const dur=CHORD_RHYTHM_BEATS[rhythm]!=null?CHORD_RHYTHM_BEATS[rhythm]:4;const payload=(chords||[]).filter(function(c){return c.pitches&&c.pitches.length;}).map(function(c){return{name:c.name||c.token,pitches:c.pitches,duration_beats:dur};});if(!payload.length){if(window.showToast)window.showToast('Không có chord để chèn','warning');return;}onInsert({label:label,chords:payload});};const saveCustom=function(){const tokens=(cTokens||'').trim();if(!tokens){if(window.showToast)window.showToast('Nhập progression tokens','warning');return;}let chords=[];try{chords=(window.ChordTheory||ChordTheory).progressionToChords(tokens,customKey,cVoicing);}catch(e){}if(!chords.length||chords.some(function(c){return!c.pitches||!c.pitches.length;})){if(window.showToast)window.showToast('Progression không parse được','warning');return;}const entry={id:'custom_'+Date.now(),name:(cName||'').trim()||'My Chord',genre:(cGenre||'').trim()||'Custom',tokens:tokens,key:customKey,rhythm:cRhythm,voicing:cVoicing,chords:chords};try{const list=JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]');list.push(entry);localStorage.setItem('daw_user_custom_chords',JSON.stringify(list));setCustomList(list);window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'));if(window.showToast)window.showToast('Đã lưu chord custom: '+entry.name,'success');}catch(e){if(window.showToast)window.showToast('Lưu thất bại','error');}};const deleteCustom=function(id){try{const list=JSON.parse(localStorage.getItem('daw_user_custom_chords')||'[]').filter(function(x){return x.id!==id;});localStorage.setItem('daw_user_custom_chords',JSON.stringify(list));setCustomList(list);window.dispatchEvent(new CustomEvent('CUSTOM_CHORD_SAVED'));}catch(e){}};const q=(search||'').trim().toLowerCase();const filteredPresets=CHORD_PRESETS.filter(function(p){if(!q)return true;const chords=resolveProgression(p.tokens,key,p.voicing);const hay=(p.label+' '+p.genre+' '+p.hint+' '+chords.map(function(c){return c.name;}).join(' ')).toLowerCase();return hay.indexOf(q)!==-1;});return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[9995] flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:onClose},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-3xl p-5 text-slate-200 max-h-[85vh] overflow-y-auto",onClick:function(e){e.stopPropagation();}},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between pb-3 border-b border-[#383838] mb-3"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Chords Panel"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-zinc-500 hover:text-red-400 text-xs px-1",title:"Close"},"\u2715")),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 flex-wrap"},/*#__PURE__*/React.createElement("div",{className:"flex gap-1"},[['preset','Presets'],['custom','Custom'],['ai','AI']].map(function(t){return/*#__PURE__*/React.createElement("button",{key:t[0],onClick:function(){setTab(t[0]);},className:"px-3 py-1 rounded text-[11px] font-bold "+(tab===t[0]?'bg-amber-700 text-white':'bg-zinc-800 text-zinc-400 hover:bg-zinc-700')},t[1]);})),tab!=='ai'&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("select",{value:key,onChange:function(e){setKey(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none",title:"Key (transpose)"},CHORD_KEYS.map(function(k){return/*#__PURE__*/React.createElement("option",{key:k,value:k},k);})),tab==='preset'&&/*#__PURE__*/React.createElement("input",{value:search,onChange:function(e){setSearch(e.target.value);},placeholder:"Search preset...",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-[11px] outline-none focus:border-amber-500 flex-1 min-w-[120px]"}))),tab==='preset'&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 sm:grid-cols-2 gap-3"},filteredPresets.map(function(p){const chords=resolveProgression(p.tokens,key,p.voicing);const display=chords.map(function(c){return c.name;}).join(' → ');const fav=favorites.indexOf(p.id)!==-1;return/*#__PURE__*/React.createElement("div",{key:p.id,className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"flex items-center justify-between mb-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs font-bold text-amber-400"},p.label),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},p.genre," \xB7 ",CHORD_VOICING_LABELS[p.voicing])),/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-mono text-zinc-300 mb-2"},display||p.hint),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 flex-wrap"},/*#__PURE__*/React.createElement("button",{onClick:function(){previewChord(chords[0]);},disabled:!chords.length,className:"px-2 py-1 rounded text-[10px] font-semibold bg-emerald-800 hover:bg-emerald-700 text-emerald-100 disabled:opacity-40"},"Preview Sound"),/*#__PURE__*/React.createElement("button",{onClick:function(){insertProg(p.label,chords,p.rhythm);},disabled:!chords.length,className:"px-2 py-1 rounded text-[10px] font-semibold bg-blue-800 hover:bg-blue-700 text-blue-100 disabled:opacity-40"},"Insert to Timeline"),/*#__PURE__*/React.createElement("button",{onClick:function(){toggleFavorite(p.id);},className:"px-2 py-1 rounded text-[10px] font-semibold "+(fav?'bg-yellow-700 text-yellow-100':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},fav?'★ Favorite':'☆ Favorite')));}),!filteredPresets.length&&/*#__PURE__*/React.createElement("div",{className:"col-span-full text-[11px] text-zinc-500"},"Kh\xF4ng t\xECm th\u1EA5y preset.")),tab==='custom'&&/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 lg:grid-cols-2 gap-4"},/*#__PURE__*/React.createElement("div",{className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-amber-400 mb-2"},"Build Custom Progression"),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2"},/*#__PURE__*/React.createElement("input",{value:cName,onChange:function(e){setCName(e.target.value);},placeholder:"T\xEAn (vd: Ballad)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("input",{value:cGenre,onChange:function(e){setCGenre(e.target.value);},placeholder:"Genre (vd: Ballad)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("input",{value:cTokens,onChange:function(e){setCTokens(e.target.value);},placeholder:"Tokens (vd: I-vi-IV-V)",className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-2 py-1 text-xs outline-none focus:border-amber-500"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-2 flex-wrap"},/*#__PURE__*/React.createElement("select",{value:customKey,onChange:function(e){setCustomKey(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},CHORD_KEYS.map(function(k){return/*#__PURE__*/React.createElement("option",{key:k,value:k},k);})),/*#__PURE__*/React.createElement("select",{value:cRhythm,onChange:function(e){setCRhythm(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},Object.keys(CHORD_RHYTHM_BEATS).map(function(r){return/*#__PURE__*/React.createElement("option",{key:r,value:r},CHORD_RHYTHM_LABELS[r]);})),/*#__PURE__*/React.createElement("select",{value:cVoicing,onChange:function(e){setCVoicing(e.target.value);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-2 py-1 text-[11px] outline-none"},Object.keys(CHORD_VOICING_LABELS).map(function(v){return/*#__PURE__*/React.createElement("option",{key:v,value:v},CHORD_VOICING_LABELS[v]);}))),/*#__PURE__*/React.createElement("button",{onClick:saveCustom,className:"px-3 py-1.5 rounded text-xs font-bold bg-amber-700 hover:bg-amber-600 text-white"},"Save Custom"))),/*#__PURE__*/React.createElement("div",{className:"border border-[#383838] rounded-lg p-3 bg-[#2a2a2a]"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-amber-400 mb-2"},"My Customs (",customList.length,")"),customList.length===0?/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500"},"Ch\u01B0a c\xF3 custom n\xE0o \u2014 t\u1EA1o b\xEAn tr\xE1i r\u1ED3i Save."):/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-2 max-h-48 overflow-y-auto"},customList.map(function(c){return/*#__PURE__*/React.createElement("div",{key:c.id,className:"border border-zinc-700 rounded p-2 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("div",{className:"min-w-0"},/*#__PURE__*/React.createElement("div",{className:"text-[11px] font-bold text-zinc-200"},c.name," ",/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-zinc-500"},"(",c.genre,")")),/*#__PURE__*/React.createElement("div",{className:"text-[10px] font-mono text-zinc-400 truncate"},c.chords.map(function(x){return x.name;}).join(' → '))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 shrink-0"},/*#__PURE__*/React.createElement("button",{onClick:function(){previewChord(c.chords[0]);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-emerald-800 hover:bg-emerald-700 text-emerald-100"},"Preview"),/*#__PURE__*/React.createElement("button",{onClick:function(){insertProg(c.name,c.chords,c.rhythm);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-blue-800 hover:bg-blue-700 text-blue-100"},"Insert"),/*#__PURE__*/React.createElement("button",{onClick:function(){deleteCustom(c.id);},className:"px-2 py-1 rounded text-[10px] font-semibold bg-red-900 hover:bg-red-800 text-red-100"},"Del")));})))),tab==='ai'&&/*#__PURE__*/React.createElement("div",{className:"flex flex-col items-center justify-center py-12 text-center"},/*#__PURE__*/React.createElement("div",{className:"text-3xl mb-2"},"\u266A"),/*#__PURE__*/React.createElement("div",{className:"text-sm text-zinc-300 font-semibold mb-1"},"AI Chord Suggestions"),/*#__PURE__*/React.createElement("div",{className:"text-[11px] text-zinc-500 max-w-sm"},"AI ch\u01B0a c\u1EA5u h\xECnh. B\u1EADt AI Gateway trong Settings \u0111\u1EC3 nh\u1EADn g\u1EE3i \xFD chord theo genre / c\u1EA3m x\xFAc."))));};const App=()=>{// ── State Definitions ── const[tracks,setTracks]=useState([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[],isArmed:false,monitoringEnabled:true,inputSource:{deviceType:'NONE',deviceId:''}}]);const[appWarningModal,setAppWarningModal]=useState(null);const[bpm,setBpm]=useState(localStorage.getItem('studio_bpm')||'120');const prevBpmRef=useRef(bpm);const[draggedClip,setDraggedClip]=useState(null);const[hoveredTrackId,setHoveredTrackId]=useState(null);// Recalculate item/section/selection durations when BPM changes useEffect(()=>{const oldSpb=prevBpmRef.current?60.0/parseFloat(prevBpmRef.current)*4:null;const bpmVal=parseFloat(bpm)||120;const secondsPerBar=60.0/bpmVal*4;// Recalculate range loop selection to maintain bar count (tempo mode only) if(oldSpb&&selectionFollowsTempo&&selectionStart!==null&&selectionEnd!==null&&selectionEnd>selectionStart){const startBar=selectionStart/oldSpb;const endBar=selectionEnd/oldSpb;if(endBar-startBar>0.01){setSelectionStart(startBar*secondsPerBar);setSelectionEnd(endBar*secondsPerBar);}}prevBpmRef.current=bpm;// Force canvas redraw @@ -1043,7 +1046,7 @@ const lsb=msg.data[1];const msb=msg.data[2];const bendVal=msb<<7|lsb;if(window.S if(activeMIDIRecordersRef.current){for(let trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){rec.handleMIDIMessage(msg,input.id);}}}};}for(let input of access.inputs.values()){inputs.push(input);attachMidiHandler(input);}setMidiDevices(inputs);access.onstatechange=()=>{const inputs=[];for(let input of access.inputs.values()){inputs.push(input);// Re-attach handler to ensure new devices get it if(!input.onmidimessage){attachMidiHandler(input);}}setMidiDevices(inputs);};}).catch(err=>console.log('MIDI access error:',err));}},[]);const[showAIConfig,setShowAIConfig]=useState(false);const[recordingState,setRecordingState]=useState('IDLE');// 'IDLE' | 'COUNT_IN' | 'RECORDING' const[recTempMidiNotes,setRecTempMidiNotes]=useState([]);const[recTempAudioBuffer,setRecTempAudioBuffer]=useState(null);const[recStartTimelineTime,setRecStartTimelineTime]=useState(0);const[canvasRedrawCount,setCanvasRedrawCount]=useState(0);const[lastMidiNote,setLastMidiNote]=useState(null);const lastMidiNoteRef=useRef(null);const activeMIDIRecordersRef=useRef({});const activeAudioRecordersRef=useRef({});const pianoRollRecorderRef=useRef(null);const activeMidiPitchesRef=useRef(new Set());const[activeMidiPitches,setActiveMidiPitches]=useState(new Set());const recordingPCMDataRef=useRef({});const recordingStartTimeRef=useRef(0);const recordingSyncRef=useRef(null);const recordingStateRef=useRef(recordingState);recordingStateRef.current=recordingState;const lastTempCompileTimeRef=useRef(0);const nextMetronomeBeatRef=useRef(0);const[showExportPanel,setShowExportPanel]=useState(false);React.useEffect(()=>{if(showExportPanel)setTimeout(()=>{try{window.lucide&&window.lucide.createIcons();}catch(e){}},80);},[showExportPanel]);// ── Virtual MIDI Keyboard (Phase 5) ───────────────────────────────────── -const[vkbOpen,setVkbOpen]=useState(false);const vkbOpenRef=useRef(false);vkbOpenRef.current=vkbOpen;const vkbHeldRef=useRef(new Set());const[vkbSettings,setVkbSettings]=useState(function(){try{const saved=JSON.parse(localStorage.getItem('sonic_vkb_settings_v1')||'null');if(saved&&typeof saved==='object')return Object.assign({x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'},saved);}catch(e){}return{x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'};}());const vkbSettingsRef=useRef(vkbSettings);vkbSettingsRef.current=vkbSettings;const vkbNoteOnRef=useRef(null);const vkbNoteOffRef=useRef(null);const vkbUpdateSettings=patch=>{setVkbSettings(prev=>{const next=Object.assign({},prev,patch);try{localStorage.setItem('sonic_vkb_settings_v1',JSON.stringify(next));}catch(e){}return next;});};// Global keydown: F2 toggle + QWERTY notes khi VKB mở. capture:true + đăng ký +const[vkbOpen,setVkbOpen]=useState(false);const vkbOpenRef=useRef(false);vkbOpenRef.current=vkbOpen;const[chordsPanelOpen,setChordsPanelOpen]=useState(false);const vkbHeldRef=useRef(new Set());const[vkbSettings,setVkbSettings]=useState(function(){try{const saved=JSON.parse(localStorage.getItem('sonic_vkb_settings_v1')||'null');if(saved&&typeof saved==='object')return Object.assign({x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'},saved);}catch(e){}return{x:40,y:120,octave:4,velocity:100,channel:null,transpose:0,scale:'None'};}());const vkbSettingsRef=useRef(vkbSettings);vkbSettingsRef.current=vkbSettings;const vkbNoteOnRef=useRef(null);const vkbNoteOffRef=useRef(null);const vkbUpdateSettings=patch=>{setVkbSettings(prev=>{const next=Object.assign({},prev,patch);try{localStorage.setItem('sonic_vkb_settings_v1',JSON.stringify(next));}catch(e){}return next;});};// Global keydown: F2 toggle + QWERTY notes khi VKB mở. capture:true + đăng ký // TRƯỚC handler transport (~19402) → stopPropagation chặn phím tắt trùng // (s=split, v/g sub-tab) khi VKB đang mở; KHÔNG chặn Space/R. Note KHÔNG bắn // khi focus trong input/textarea (pass-through gõ văn bản bình thường). @@ -1303,7 +1306,7 @@ handlePasteTrackRef.current();return;}if(e.key==='Delete'||e.key==='Backspace'|| handleSaveProject();subTabsRef.current.filter(s=>s.isDirty).forEach(st=>{if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);}else if(st.buffer){// Audio clip sub-tab: save buffer to track const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}}});if(curTab.startsWith('session_')){// SECTION-TAB: cũng lưu section hiện tại handleSaveSectionTabRef.current(curTab);}showToast('Đã lưu dự án + các tab đã sửa','success');}else{// Sub-tab: save current tab -const st=subTabsRef.current.find(s=>s.id===curTab);if(st){if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);showToast('Đã lưu Piano Roll','success');}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);showToast('Đã lưu Section Tab','success');}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}showToast('Đã lưu Audio Tab','success');}}}return;}if(!ctrl&&!alt&&e.key==='s'){e.preventDefault();handleSplitTrackRef.current(selectedTrackIdRef.current);return;}if(e.key==='F7'){e.preventDefault();e.stopPropagation();window.__toggleMixerRef();return;}if(e.key==='F6'){e.preventDefault();e.stopPropagation();window.__toggleMediaExplorerRef();return;}};window.addEventListener('keydown',handler,{capture:true});return()=>window.removeEventListener('keydown',handler,{capture:true});},[]);// ── Temp Tab: draw isolated waveform ── +const st=subTabsRef.current.find(s=>s.id===curTab);if(st){if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);showToast('Đã lưu Piano Roll','success');}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);showToast('Đã lưu Section Tab','success');}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}showToast('Đã lưu Audio Tab','success');}}}return;}if(!ctrl&&!alt&&e.key==='s'){e.preventDefault();handleSplitTrackRef.current(selectedTrackIdRef.current);return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&(e.key==='K'||e.key==='k')){e.preventDefault();e.stopPropagation();if(window.__toggleChordsPanel)window.__toggleChordsPanel();return;}if(e.key==='F7'){e.preventDefault();e.stopPropagation();window.__toggleMixerRef();return;}if(e.key==='F6'){e.preventDefault();e.stopPropagation();window.__toggleMediaExplorerRef();return;}};window.addEventListener('keydown',handler,{capture:true});return()=>window.removeEventListener('keydown',handler,{capture:true});},[]);// ── Temp Tab: draw isolated waveform ── useEffect(()=>{if(!tempTabActive||!tempTabBuffer||!tempTabCanvasRef.current)return;const canvas=tempTabCanvasRef.current;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const rect=canvas.getBoundingClientRect();canvas.width=rect.width*dpr;canvas.height=rect.height*dpr;ctx.scale(dpr,dpr);const w=rect.width;const h=rect.height;ctx.fillStyle='#181818';ctx.fillRect(0,0,w,h);const data=tempTabBuffer.getChannelData(0);const sr=tempTabBuffer.sampleRate;const totalSamples=data.length;if(totalSamples===0)return;ctx.strokeStyle='#6ee7b7';ctx.lineWidth=1;for(let px=0;pxmaxVal)maxVal=abs;}const mid=h/2;const peakHeight=maxVal*(h*0.4);ctx.beginPath();ctx.moveTo(px,mid-peakHeight);ctx.lineTo(px,mid+peakHeight);ctx.stroke();}},[tempTabActive,tempTabBuffer]);// ── Sub Tab: open as new tab instead of modal (LOOP_EDITOR_2.md §1) ── const openTempTab=()=>{const useLocal=selectionMode==='local'&&localSelectionTrackId;const trackId=useLocal?localSelectionTrackId:selectedTrackId;const t=tracks.find(x=>x.id===trackId);if(!t||!t.buffer){showToast('Vui lòng chọn track có dữ liệu âm thanh.','warning');return;}if(selLeft===null||selRight===null||selRight<=selLeft){showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.','warning');return;}// Check if a subtab for this track+range already exists const existing=subTabs.find(s=>s.trackId===trackId&&s.startTime===selLeft&&s.endTime===selRight);if(existing){setActiveTab(existing.id);showToast(`Sub Tab already open.`,'info');return;}const sr=t.buffer.sampleRate;const trackStart=t.startTime||0;const relSelLeft=Math.max(0,selLeft-trackStart);const relSelRight=Math.max(0,selRight-trackStart);const startSample=Math.max(0,Math.floor(relSelLeft*sr));const endSample=Math.min(t.buffer.length,Math.floor(relSelRight*sr));const len=endSample-startSample;if(len<100){showToast('Khoảng chọn quá ngắn.','warning');return;}const ctx=getAudioContext();const numChannels=t.buffer.numberOfChannels||1;const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,startTime:selLeft,endTime:selRight,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditClipInSubTab=(trackId,clipId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const clips=track.clips&&track.clips.length>0?track.clips:track.buffer?[{id:'default_'+track.id,buffer:track.buffer,startTime:track.startTime||0,name:track.name}]:[];const clip=clips.find(c=>c.id===clipId||clipId==='default'&&c.id==='default_'+trackId);if(!clip||!clip.buffer)return;const resolvedClipId=clip.id==='default'?'default_'+trackId:clip.id;const existing=subTabs.find(s=>s.clipId===resolvedClipId&&s.trackId===trackId);if(existing){setActiveTab(existing.id);showToast(`Sub Tab for "${clip.name}" already open.`,'info');return;}const sr=clip.buffer.sampleRate;const len=clip.buffer.length;const numChannels=clip.buffer.numberOfChannels||1;const ctx=getAudioContext();const subBuffer=ctx.createBuffer(numChannels,len,sr);for(let c=0;c[...prev,{id:tabId,label:tabLabel,trackId:trackId,clipId:resolvedClipId,startTime:clip.startTime,endTime:clip.startTime+clip.buffer.duration,buffer:subBuffer,channelInfo:subChannelInfo,effects:{reverse:false,gainDb:0,fadeInMs:0,fadeOutMs:0,normalizeDb:0,pitch:0,speedStretch:100},currentTime:0,selectionStart:null,selectionEnd:null,isPlaying:false,speed:1.0,volumeNodes:[],panningNodes:[],fadeInLen:0,fadeOutLen:0,graphMode:null,isLooping:false,loopCount:0}]);setActiveTab(tabId);};const handleEditMidiInTab=(trackId,midiItemId)=>{const track=activeTracks.find(t=>t.id===trackId);if(!track)return;const midiItem=(track.midiItems||[]).find(m=>m.id===midiItemId);if(!midiItem)return;// Main đang play → CHỈ lưu điểm resume; useEffect [activeTab] tự @@ -1434,7 +1437,7 @@ const showToast=(text,type='info',actionText=null,onActionClick=null)=>{if(toast // per-track channel alloc; native SF/VST; SonicSF fallback; Carla; VU + giữ // counter (heldMidiNotesRef); forward recorder (live record vào midiItems). const vkbNoteOn=(pitch,vel)=>{try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}const allTracks=activeTracksRef.current||[];const armed=allTracks.filter(function(t){return t.isArmed;});const targets=armed.length?armed:allTracks;if(!armed.length&&!window.__vkbNoArmToast){window.__vkbNoArmToast=true;showToast('Không có track nào ARM — chơi vào tất cả track.','info');}activeMidiPitchesRef.current.add(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));vkbHeldRef.current.add(pitch);targets.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);ensureSonicInstrument(ctx);if(window.triggerMidiVuActivity)window.triggerMidiVuActivity(t.id,Math.round(vel*127));try{heldMidiNotesRef.current[t.id]=(heldMidiNotesRef.current[t.id]||0)+1;}catch(err){}const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{if(window.__ensureBridgeProgram)window.__ensureBridgeProgram(t.id,ctx.program,ctx.synthEngine);}catch(er){}try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_ON',channel:t.id,pitch:pitch,velocity:vel,percussion:percussion,live:true});}catch(e){}}else if(isStandaloneSf()&&(isSfTrackEngine(ctx.synthEngine)||isVstTrackEngine(ctx.synthEngine))&&!shouldRouteCarla(ctx.synthEngine)){if(isSfTrackEngine(ctx.synthEngine))playNativeSfNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);else playNativeVstNote(t,pitch,vel,60000,undefined,'vkb_'+t.id+'_'+pitch);}else if(window.SonicSF){const dest=activeTrackNodesRef.current[t.id]&&activeTrackNodesRef.current[t.id].gainNode||null;window.SonicSF.playNote(pitch,Math.max(1,Math.round(vel*127)),60000,undefined,ctx.program,dest,ctx.ch,ctx.synthEngine);}if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(ctx.synthEngine,t.isArmed)){try{window.SonicCarlaMidi.noteOn(ctx.ch,pitch,Math.max(1,Math.round(vel*127)));}catch(e){}}});// Live record: forward tới mọi recorder đang chạy (timeline take / piano roll) -if(activeMIDIRecordersRef.current){const msg={data:[0x90,pitch,Math.max(1,Math.round(vel*127))]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};const vkbNoteOff=pitch=>{vkbHeldRef.current.delete(pitch);activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const allTracks=activeTracksRef.current||[];allTracks.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:pitch,velocity:0,percussion:percussion,live:true});}catch(e){}}else if(window.SonicSF&&window.SonicSF.stopNote){if(isStandaloneSf()){stopNativeSfNote('vkb_'+t.id+'_'+pitch);stopNativeVstNote('vkb_'+t.id+'_'+pitch);}window.SonicSF.stopNote(ctx.ch,pitch);if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(t.synth_engine,t.isArmed)){try{window.SonicCarlaMidi.noteOff(ctx.ch,pitch);}catch(e){}}}try{if(heldMidiNotesRef.current[t.id]!==undefined){heldMidiNotesRef.current[t.id]=Math.max(0,heldMidiNotesRef.current[t.id]-1);if(heldMidiNotesRef.current[t.id]===0)delete heldMidiNotesRef.current[t.id];}}catch(err){}});if(activeMIDIRecordersRef.current){const msg={data:[0x80,pitch,0]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};vkbNoteOnRef.current=vkbNoteOn;vkbNoteOffRef.current=vkbNoteOff;window.__toggleVkb=function(){setVkbOpen(function(p){const next=!p;if(!next&&vkbHeldRef.current.size){Array.from(vkbHeldRef.current).forEach(function(pitch){try{vkbNoteOffRef.current(pitch);}catch(e){}});}return next;});};// ── Server-side upload ── +if(activeMIDIRecordersRef.current){const msg={data:[0x90,pitch,Math.max(1,Math.round(vel*127))]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};const vkbNoteOff=pitch=>{vkbHeldRef.current.delete(pitch);activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const allTracks=activeTracksRef.current||[];allTracks.forEach(function(t){const ctx=resolveTrackInstrumentCtx(t,allTracks);const percussion=!!(ctx&&ctx.synthEngine&&ctx.synthEngine.soundfont_bank===128);if(window.SonicMidiRouter&&window.SonicMidiRouter.isBridgeActive()){try{window.SonicMidiRouter.pushEvent({cmd:'NOTE_OFF',channel:t.id,pitch:pitch,velocity:0,percussion:percussion,live:true});}catch(e){}}else if(window.SonicSF&&window.SonicSF.stopNote){if(isStandaloneSf()){stopNativeSfNote('vkb_'+t.id+'_'+pitch);stopNativeVstNote('vkb_'+t.id+'_'+pitch);}window.SonicSF.stopNote(ctx.ch,pitch);if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(t.synth_engine,t.isArmed)){try{window.SonicCarlaMidi.noteOff(ctx.ch,pitch);}catch(e){}}}try{if(heldMidiNotesRef.current[t.id]!==undefined){heldMidiNotesRef.current[t.id]=Math.max(0,heldMidiNotesRef.current[t.id]-1);if(heldMidiNotesRef.current[t.id]===0)delete heldMidiNotesRef.current[t.id];}}catch(err){}});if(activeMIDIRecordersRef.current){const msg={data:[0x80,pitch,0]};for(const trackId in activeMIDIRecordersRef.current){const rec=activeMIDIRecordersRef.current[trackId];if(rec){try{rec.handleMIDIMessage(msg,null);}catch(e){}}}}};vkbNoteOnRef.current=vkbNoteOn;vkbNoteOffRef.current=vkbNoteOff;window.__toggleVkb=function(){setVkbOpen(function(p){const next=!p;if(!next&&vkbHeldRef.current.size){Array.from(vkbHeldRef.current).forEach(function(pitch){try{vkbNoteOffRef.current(pitch);}catch(e){}});}return next;});};window.__toggleChordsPanel=function(){setChordsPanelOpen(function(p){return!p;});};// ── Server-side upload ── // WAV encoder tối thiểu — upload buffer-only clip khi LƯU (nếu chưa có // serverFileId — upload sớm thất bại/race → clip mất sau reload). // ── Lossless WAV encode helpers (lossless-audio-compliance skill) ───────── @@ -1914,7 +1917,8 @@ if(notes.length)midiTracks.push({name:track.name||'Track',notes});});if(!midiTra const insertTrackBelow=()=>{const curTracks=activeTracks;const rearrangeNewId=`t${Date.now()}`;const colors=['#0f766e','#1d4ed8','#701a75','#a21caf','#b45309'];const selectColor=colors[curTracks.length%colors.length];const newTrack={id:rearrangeNewId,name:`Track ${rearrangeNewId}`,buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,masteringBypass:false,audioBypass:false,midiBypass:false,fxActive:true,color:selectColor,markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]};updateActiveTracks(prev=>{const idx=prev.findIndex(t=>t.id===selectedTrackId);if(idx===-1)return[...prev,newTrack];const copy=[...prev];copy.splice(idx+1,0,newTrack);return copy;});setSelectedTrackId(rearrangeNewId);showToast(`Đã thêm Track ${rearrangeNewId}.`,'info');setTimeout(()=>lucide.createIcons(),200);};// ── Insert Section at Playhead ── const insertSectionAtPlayhead=()=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===selectedTrackId);if(!track){showToast('Chọn track trước','warning');return;}const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const section={id:`sec_${Date.now()}`,name:'Section',start:currentTime,duration:4*secondsPerBar,length_bars:4,color:track.color||'#06b6d4'};createSectionWithUndo(selectedTrackId,section);updateActiveTracks(prev=>prev.map(t=>t.id===selectedTrackId?{...t,sections:[...(t.sections||[]),section]}:t));showToast(`Đã thêm Section tại ${currentTime.toFixed(2)}s`,'success');};// ── Insert MIDI Item at Playhead ── const insertMidiItemAtPlayhead=()=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===selectedTrackId);if(!track){showToast('Chọn track trước','warning');return;}const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const midiItem={id:`midi_${Date.now()}`,name:'MIDI Item',startTime:currentTime,duration:4*secondsPerBar,length_bars:4,notes:[],color:'#a78bfa'};createMidiWithUndo(selectedTrackId,midiItem);updateActiveTracks(prev=>prev.map(t=>t.id===selectedTrackId?{...t,midiItems:[...(t.midiItems||[]),midiItem]}:t));showToast(`Đã thêm MIDI item tại ${currentTime.toFixed(2)}s`,'success');};// ── Insert Sound Clip at Cursor ── -const insertSoundClipAtCursor=()=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===selectedTrackId);if(!track){showToast('Chọn track trước','warning');return;}const input=document.createElement('input');input.type='file';input.accept='audio/*';input.multiple=true;input.onchange=async e=>{const files=Array.from(e.target.files||[]);if(!files.length)return;let offset=currentTime;let count=0;for(const file of files){try{const uploadResult=await uploadToServer(file,track.id);const fileId=uploadResult?.file_id||null;const{audioBuffer:decodedBuffer,channelInfo}=await window.SonicAudio.decodeAudioFile(file);const clipId=`clip_${Date.now()}_${Math.random().toString(36).slice(2,6)}`;const newClip={id:clipId,buffer:decodedBuffer,startTime:offset,name:file.name,speed:1.0,serverFileId:fileId};createClipWithUndo(selectedTrackId,newClip);updateActiveTracks(prev=>prev.map(t=>t.id===selectedTrackId?{...t,serverFileId:t.serverFileId||fileId,clips:[...(t.clips||[]),newClip]}:t));offset+=decodedBuffer.duration;count++;}catch(err){showToast(`Lỗi nạp ${file.name}`,'error');}}if(count>0)showToast(`Đã chèn ${count} file âm thanh`,'success');};input.click();};// ── Server-side Export ── +const insertSoundClipAtCursor=()=>{const curTracks=activeTracks;const track=curTracks.find(t=>t.id===selectedTrackId);if(!track){showToast('Chọn track trước','warning');return;}const input=document.createElement('input');input.type='file';input.accept='audio/*';input.multiple=true;input.onchange=async e=>{const files=Array.from(e.target.files||[]);if(!files.length)return;let offset=currentTime;let count=0;for(const file of files){try{const uploadResult=await uploadToServer(file,track.id);const fileId=uploadResult?.file_id||null;const{audioBuffer:decodedBuffer,channelInfo}=await window.SonicAudio.decodeAudioFile(file);const clipId=`clip_${Date.now()}_${Math.random().toString(36).slice(2,6)}`;const newClip={id:clipId,buffer:decodedBuffer,startTime:offset,name:file.name,speed:1.0,serverFileId:fileId};createClipWithUndo(selectedTrackId,newClip);updateActiveTracks(prev=>prev.map(t=>t.id===selectedTrackId?{...t,serverFileId:t.serverFileId||fileId,clips:[...(t.clips||[]),newClip]}:t));offset+=decodedBuffer.duration;count++;}catch(err){showToast(`Lỗi nạp ${file.name}`,'error');}}if(count>0)showToast(`Đã chèn ${count} file âm thanh`,'success');};input.click();};// ── Chords Panel insert (Phase 6) ─────────────────────────────────────── +const insertChordProgression=prog=>{const curTracks=activeTracks;const pr=prog||{};const chordList=pr.chords||[];if(!chordList.length){showToast('Không có chord để chèn','warning');return;}const beatSec=60.0/(parseInt(bpm)||120);const secondsPerBar=beatSec*4;const activeSub=activeTab&&subTabs.find(s=>s.id===activeTab&&s.type==='PIANO_ROLL');let trackId=null;let item=null;let insertBeat=0;if(activeSub){trackId=activeSub.trackId;const trk=curTracks.find(t=>t.id===trackId);if(trk)item=(trk.midiItems||[]).find(m=>m.id===activeSub.target_id)||null;insertBeat=Math.max(0,(activeSub.currentTime||0)/beatSec);}else{trackId=selectedTrackId;const trk=curTracks.find(t=>t.id===trackId);if(!trk){showToast('Chọn track trước','warning');return;}const items=trk.midiItems||[];if(!items.length){const newItem={id:'midi_'+Date.now(),name:'Chords',startTime:currentTime,duration:secondsPerBar*4,length_bars:4,notes:[],color:'#a78bfa'};createMidiWithUndo(trackId,newItem);updateActiveTracks(prev=>prev.map(t=>t.id===trackId?{...t,midiItems:[...(t.midiItems||[]),newItem]}:t));item=newItem;insertBeat=0;}else{item=items[items.length-1];insertBeat=Math.max(0,(currentTime-(item.startTime||0))/beatSec);}}if(!item){showToast('Không tìm thấy MIDI item','warning');return;}const merged=(item.notes||[]).slice();let beat=insertBeat;chordList.forEach(function(ch,ci){const dur=ch.duration_beats!=null?ch.duration_beats:4;(ch.pitches||[]).forEach(function(p){merged.push({id:'note_chord_'+Date.now()+'_'+ci+'_'+p,pitch:p,start_beat:beat,duration_beats:dur,velocity:0.8,pan:0.0});});beat+=dur;});const finalItem={...item,notes:merged};updateActiveTracks(prev=>prev.map(t=>{if(t.id!==trackId)return t;const has=(t.midiItems||[]).some(m=>m.id===item.id);if(!has)return{...t,midiItems:[...(t.midiItems||[]),finalItem]};return{...t,midiItems:(t.midiItems||[]).map(m=>m.id===item.id?finalItem:m)};}));if(activeSub){setSubTabs(prev=>prev.map(s=>s.id===activeSub.id?{...s,notes:merged,isDirty:true}:s));}setCanvasRedrawCount(n=>n+1);showToast('Đã chèn '+chordList.length+' chord ('+merged.length+' notes)','success');window.__lastChordInsert={trackId:trackId,itemId:item.id,notes:merged};};// ── Server-side Export ── const triggerWavExport=async()=>{// TỰ ĐỘNG bounce realtime nếu project có MIDI items: FluidSynth (ScriptProcessor) // không render được offline, nên bounce là cách duy nhất giữ trung thực // MIDI + FX Rack + Mastering Chain trong file xuất. @@ -2115,7 +2119,7 @@ if(result.zoom>1&&setZoom)setZoom(result.zoom);if(result.masteringSettings)setMa try{stopAllPlayback();}catch(e){}setSubTabs([]);setSessionTabs([]);setActiveTab('main');// Reset instrument/channel/route context đã load trackMidiChannelsRef.current={};Object.keys(trackMidiBypassMap).forEach(function(k){delete trackMidiBypassMap[k];});Object.keys(trackAudioBypassMap).forEach(function(k){delete trackAudioBypassMap[k];});Object.keys(trackMasteringBypassMap).forEach(function(k){delete trackMasteringBypassMap[k];});setSelectedItemIds(new Set());setTracks([{id:'1',name:'Track 01',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#0f766e',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]},{id:'2',name:'Track 02',buffer:null,startTime:0,volumeDb:0,pan:0,muted:false,solo:false,color:'#1d4ed8',markers:[],serverFileId:null,clips:[],sections:[],midiItems:[]}]);setSelectedTrackId('1');setProjectName('');setCurrentProjectId(null);localStorage.removeItem('sonic_project_name');localStorage.removeItem('sonic_project_id');showToast('New project created','info');}},{label:'Open Project...',icon:'folder-open',shortcut:'Ctrl+O',action:()=>setOpenProjectModalOpen(true)},{label:'Save Project',icon:'upload-cloud',shortcut:'Ctrl+S',action:()=>{// Save project + save TẤT CẢ dirty sub-tabs (user 07:35 — nhấn lưu ở // MAIN phải lưu cả piano roll/audio tab đã sửa) -handleSaveProject();subTabsRef.current.filter(s=>s.isDirty).forEach(st=>{if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}}});showToast('Đã lưu dự án + các tab đã sửa','success');}},{label:'Save As...',icon:'download',shortcut:'Ctrl+Alt+S',action:()=>setSaveAsModalOpen(true)},{sep:true},{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'Import Audio...',icon:'file-input',shortcut:'Ctrl+Alt+I',action:()=>{const input=document.createElement('input');input.type='file';input.accept='audio/*';input.onchange=async e=>{if(e.target.files[0]){addNewTrack();const rearrangeNewId=(tracks.length+1).toString();setTimeout(()=>loadFileOnTrack(rearrangeNewId,e.target.files[0]),100);}};input.click();showToast('Import audio','info');}},{label:'Export Mix...',icon:'file-output',action:()=>triggerWavExport()},{label:'Export MIDI...',icon:'music',action:()=>triggerMidiExport()},{label:'Master FX Chain',icon:'wand-2',shortcut:'Ctrl+Shift+M',action:()=>setShowMasteringModal(true)},{sep:true},...(currentUser?[{label:'Profile',icon:'user',action:()=>setProfileModalOpen(true)}]:[]),...(currentUser&¤tUser.role==='admin'?[{label:'System Manager',icon:'settings',action:()=>setSystemManagerModalOpen(true)}]:[]),{label:'Logout',icon:'log-out',action:()=>handleLogout()}]},{label:'Edit',items:[{label:'Insert New Track',icon:'plus',shortcut:'Ctrl+I',action:addNewTrack},{label:'Insert Music to Track',icon:'music',shortcut:'Ctrl+Alt+I',action:()=>showToast('Select music file to insert','info')},{sep:true},{label:'Edit in New Tab',icon:'file-edit',shortcut:'Ctrl+E',action:()=>openTempTab()},{label:'Split at Playhead',icon:'scissors',shortcut:'S',action:()=>handleSplitTrack(selectedTrackId)},{label:'Merge Tracks',icon:'combine',shortcut:'Ctrl+M',action:()=>{handleMergeTracks();}},{sep:true},{label:'Undo',icon:'undo',shortcut:'Ctrl+Z',action:()=>{handleUndo();}},{label:'Redo',icon:'redo',shortcut:'Ctrl+Y',action:()=>{handleRedo();}},{sep:true},{label:'Copy',icon:'copy',shortcut:'Ctrl+C',action:()=>{handleCopyTrack();}},{label:'Cut',icon:'scissors',shortcut:'Ctrl+X',action:()=>{handleCutTrack();}},{label:'Paste',icon:'clipboard',shortcut:'Ctrl+V',action:handlePasteTrack},{sep:true},{label:'Delete Track',icon:'trash-2',shortcut:'Del',action:()=>{handleDeleteTrack();}}]},{label:'Insert',items:[...(!sessionTabs.some(s=>s.id===activeTab)?[{label:'Insert Section',icon:'folder-plus',action:insertSectionAtPlayhead}]:[]),{label:'Insert MIDI item',icon:'music',action:insertMidiItemAtPlayhead},{label:'Insert sound clip',icon:'file-input',action:insertSoundClipAtCursor},{label:'Insert track',icon:'plus',action:insertTrackBelow}]},{label:'View',items:[{label:'Master Track',icon:'disc',action:()=>showToast('Master track view','info')},{label:'Maker View',icon:'layout',action:()=>showToast('Maker view','info')},{label:'Mixer',icon:'sliders',action:()=>window.__toggleMixerRef()},{label:'Tempo Track',icon:'timer',action:()=>showToast('Tempo track','info')},{label:'Video',icon:'film',action:()=>showToast('Video panel','info')},{label:'Media Explorer',icon:'folder-search',action:()=>window.__toggleMediaExplorerRef()}]},{label:'Tools',items:[{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'AI MIDI Preset Manager...',icon:'sliders',action:()=>setAiPresetModalOpen(true)},{label:'DSP Tools Panel',icon:'wrench',action:()=>{// DSP Tool đã được move vào SUB-TAB editor (audioclip). Mở sub-tab +handleSaveProject();subTabsRef.current.filter(s=>s.isDirty).forEach(st=>{if(st.type==='PIANO_ROLL'){handleSaveMidiNotes(st.id,st.trackId,st.target_id,st.notes||[]);}else if(st.type==='SECTION'){handleSaveSectionTab(st.id);}else if(st.buffer){const subTrack=activeTracksRef.current.find(t=>t.id===st.trackId);if(subTrack){updateActiveTracks(prev=>prev.map(t=>t.id===st.trackId?{...t,buffer:st.buffer}:t));}}});showToast('Đã lưu dự án + các tab đã sửa','success');}},{label:'Save As...',icon:'download',shortcut:'Ctrl+Alt+S',action:()=>setSaveAsModalOpen(true)},{sep:true},{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'Import Audio...',icon:'file-input',shortcut:'Ctrl+Alt+I',action:()=>{const input=document.createElement('input');input.type='file';input.accept='audio/*';input.onchange=async e=>{if(e.target.files[0]){addNewTrack();const rearrangeNewId=(tracks.length+1).toString();setTimeout(()=>loadFileOnTrack(rearrangeNewId,e.target.files[0]),100);}};input.click();showToast('Import audio','info');}},{label:'Export Mix...',icon:'file-output',action:()=>triggerWavExport()},{label:'Export MIDI...',icon:'music',action:()=>triggerMidiExport()},{label:'Master FX Chain',icon:'wand-2',shortcut:'Ctrl+Shift+M',action:()=>setShowMasteringModal(true)},{sep:true},...(currentUser?[{label:'Profile',icon:'user',action:()=>setProfileModalOpen(true)}]:[]),...(currentUser&¤tUser.role==='admin'?[{label:'System Manager',icon:'settings',action:()=>setSystemManagerModalOpen(true)}]:[]),{label:'Logout',icon:'log-out',action:()=>handleLogout()}]},{label:'Edit',items:[{label:'Insert New Track',icon:'plus',shortcut:'Ctrl+I',action:addNewTrack},{label:'Insert Music to Track',icon:'music',shortcut:'Ctrl+Alt+I',action:()=>showToast('Select music file to insert','info')},{sep:true},{label:'Edit in New Tab',icon:'file-edit',shortcut:'Ctrl+E',action:()=>openTempTab()},{label:'Split at Playhead',icon:'scissors',shortcut:'S',action:()=>handleSplitTrack(selectedTrackId)},{label:'Merge Tracks',icon:'combine',shortcut:'Ctrl+M',action:()=>{handleMergeTracks();}},{sep:true},{label:'Undo',icon:'undo',shortcut:'Ctrl+Z',action:()=>{handleUndo();}},{label:'Redo',icon:'redo',shortcut:'Ctrl+Y',action:()=>{handleRedo();}},{sep:true},{label:'Copy',icon:'copy',shortcut:'Ctrl+C',action:()=>{handleCopyTrack();}},{label:'Cut',icon:'scissors',shortcut:'Ctrl+X',action:()=>{handleCutTrack();}},{label:'Paste',icon:'clipboard',shortcut:'Ctrl+V',action:handlePasteTrack},{sep:true},{label:'Delete Track',icon:'trash-2',shortcut:'Del',action:()=>{handleDeleteTrack();}}]},{label:'Insert',items:[...(!sessionTabs.some(s=>s.id===activeTab)?[{label:'Insert Section',icon:'folder-plus',action:insertSectionAtPlayhead}]:[]),{label:'Insert MIDI item',icon:'music',action:insertMidiItemAtPlayhead},{label:'Insert sound clip',icon:'file-input',action:insertSoundClipAtCursor},{label:'Chords panel',icon:'guitar',shortcut:'Shift+K',action:()=>window.__toggleChordsPanel()},{label:'Insert track',icon:'plus',action:insertTrackBelow}]},{label:'View',items:[{label:'Master Track',icon:'disc',action:()=>showToast('Master track view','info')},{label:'Maker View',icon:'layout',action:()=>showToast('Maker view','info')},{label:'Mixer',icon:'sliders',action:()=>window.__toggleMixerRef()},{label:'Tempo Track',icon:'timer',action:()=>showToast('Tempo track','info')},{label:'Video',icon:'film',action:()=>showToast('Video panel','info')},{label:'Media Explorer',icon:'folder-search',action:()=>window.__toggleMediaExplorerRef()}]},{label:'Tools',items:[{label:'Config AI Providers...',icon:'settings',action:()=>setAiConfigModalOpen(true)},{label:'AI MIDI Preset Manager...',icon:'sliders',action:()=>setAiPresetModalOpen(true)},{label:'DSP Tools Panel',icon:'wrench',action:()=>{// DSP Tool đã được move vào SUB-TAB editor (audioclip). Mở sub-tab // edit cho track/clip đang chọn — nếu chưa có clip → mở panel cũ. const t=activeTracks.find(x=>x.id===selectedTrackId);if(t&&(t.buffer||t.clips&&t.clips.length)){const clipId=t.clips&&t.clips[0]?t.clips[0].id:'default';handleEditClipInSubTab(t.id,clipId);}else{openPanel('python_tools');}}},{label:'Virtual MIDI Keyboard',icon:'piano',shortcut:'F2',action:()=>window.__toggleVkb()},{sep:true},{label:'Plugin Manager (SoundFont/VSTi)',icon:'zap',action:()=>{setPluginManagerModalOpen(true);window.SonicAPI.listPlugins().then(data=>setPluginsData(data)).catch(()=>{});}},{sep:true},{label:'Preferences...',icon:'settings-2',action:()=>setPreferencesModalOpen(true)}]},{label:'Help',items:[{label:'Hướng dẫn sử dụng...',icon:'book-open',action:()=>setHelpModalOpen(true)},{sep:true},{label:'About SonicForge Studio...',icon:'info',action:()=>setAboutModalOpen(true)}]}].map(menu=>/*#__PURE__*/React.createElement("div",{key:menu.label,className:"relative"},/*#__PURE__*/React.createElement("button",{onClick:()=>setMenuOpen(menuOpen===menu.label?null:menu.label),className:`px-3 py-1 text-xs font-medium transition rounded ${menuOpen===menu.label?'bg-zinc-700 text-zinc-100':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`},menu.label),menuOpen===menu.label&&/*#__PURE__*/React.createElement("div",{className:`absolute top-full left-0 mt-0.5 bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 z-50 ${menu.label==='Edit'?'w-72':'w-64'}`,onClick:()=>setMenuOpen(null)},menu.items.map((item,i)=>item.sep?/*#__PURE__*/React.createElement("div",{key:i,className:"h-px bg-zinc-700 my-1"}):/*#__PURE__*/React.createElement("button",{key:item.label,onClick:e=>{e.stopPropagation();item.action();setMenuOpen(null);},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":item.icon,className:"w-3.5 h-3.5 text-zinc-500 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},item.label),item.shortcut&&/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},item.shortcut)))))),/*#__PURE__*/React.createElement("div",{className:"flex-1"}),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 px-2"},/*#__PURE__*/React.createElement("span",{className:`text-xs font-bold uppercase px-1.5 py-0.5 rounded ${serverStatus==='connected'?'bg-emerald-950 text-emerald-400':serverStatus==='checking...'?'bg-amber-950 text-amber-400':'bg-red-950 text-red-400'}`},"Server: ",serverStatus,serverAddr?" · "+serverAddr:""),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowAIConfig(!showAIConfig),className:`px-1.5 py-0.5 rounded text-xs border transition ${showAIConfig?'bg-purple-900 text-purple-200 border-purple-700':'bg-zinc-800 text-zinc-400 border-zinc-700 hover:text-zinc-200'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3"}))))),menuOpen&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-40",onClick:()=>setMenuOpen(null)}),/*#__PURE__*/React.createElement("div",{className:"h-7 bg-[#222] border-b border-zinc-800 flex items-stretch px-2 gap-0.5 shrink-0 overflow-x-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('main'),className:`px-3 text-xs font-bold uppercase tracking-wider border-b-2 transition flex items-center gap-1 ${activeTab==='main'?'text-cyan-400 border-cyan-500 bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"layout-dashboard",className:"w-3 h-3"}))," Main Session"),sessionTabs.map(st=>{const isTabActive=activeTab===st.id;const tabColor=st.color||(isTabActive?'#06b6d4':null);const borderStyle=isTabActive?{borderBottomColor:tabColor,color:tabColor}:st.color?{color:st.color,borderBottomColor:'transparent'}:{};return/*#__PURE__*/React.createElement("div",{key:st.id,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab(st.id),onAuxClick:e=>{if(e.button===1){e.preventDefault();closeSessionTab(st.id);}},onContextMenu:e=>{e.preventDefault();setTabContextMenu({x:e.clientX,y:e.clientY,tabId:st.id,tabType:'session'});},style:borderStyle,className:`px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive?'bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"layers",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"max-w-[120px] truncate"},st.name),st.isDirty&&/*#__PURE__*/React.createElement("span",{className:"w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",title:"Chưa lưu thay đổi"})),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSessionTab(st.id),className:"px-1 text-zinc-600 hover:text-red-400 transition text-xs",title:"Close tab"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));}),subTabs.map(st=>{const isTabActive=activeTab===st.id;const tabColor=st.color||(isTabActive?'#f59e0b':null);const borderStyle=isTabActive?{borderBottomColor:tabColor,color:tabColor}:st.color?{color:st.color,borderBottomColor:'transparent'}:{};const iconName=st.type==='PIANO_ROLL'?'music':'file-edit';return/*#__PURE__*/React.createElement("div",{key:st.id,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab(st.id),onAuxClick:e=>{if(e.button===1){e.preventDefault();closeSubTab(st.id);}},onContextMenu:e=>{e.preventDefault();setTabContextMenu({x:e.clientX,y:e.clientY,tabId:st.id,tabType:'sub'});},style:borderStyle,className:`px-2 text-xs font-medium border-b-2 transition flex items-center gap-1 ${isTabActive?'bg-zinc-800/50':'text-zinc-500 border-transparent hover:text-zinc-300 hover:bg-zinc-800/30'}`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":iconName,className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"max-w-[100px] truncate"},st.label),st.isDirty&&/*#__PURE__*/React.createElement("span",{className:"w-1.5 h-1.5 rounded-full bg-amber-500 ml-1",title:"Chưa lưu thay đổi"})),/*#__PURE__*/React.createElement("button",{onClick:()=>closeSubTab(st.id),className:"px-1 text-zinc-600 hover:text-red-400 transition text-xs",title:"Close tab"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}))));})),showAIConfig&&/*#__PURE__*/React.createElement("div",{className:"bg-zinc-900 border-b border-purple-900 p-3 flex flex-col gap-2 transition-all"},/*#__PURE__*/React.createElement("div",{className:"text-xs font-bold text-purple-400 uppercase tracking-wider flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-4 h-4"}))," Cấu hình cổng kết nối API"),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-1 md:grid-cols-3 gap-2 text-xs"},/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"Endpoint Base URL"),/*#__PURE__*/React.createElement("input",{type:"text",value:aiConfig.baseUrl,onChange:e=>setAiConfig(prev=>({...prev,baseUrl:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"https://api.openai.com/v1"})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"API Token Key"),/*#__PURE__*/React.createElement("input",{type:"password",value:aiConfig.apiKey,onChange:e=>setAiConfig(prev=>({...prev,apiKey:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"sk-..."})),/*#__PURE__*/React.createElement("div",{className:"flex flex-col gap-1"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 font-bold uppercase"},"Model Name"),/*#__PURE__*/React.createElement("input",{type:"text",value:aiConfig.model,onChange:e=>setAiConfig(prev=>({...prev,model:e.target.value})),className:"bg-zinc-850 text-zinc-200 p-1.5 rounded border border-zinc-800 focus:outline-none focus:border-purple-600 font-mono text-xs",placeholder:"gpt-4o-mini"})))),/*#__PURE__*/React.createElement("div",{className:"h-9 bg-[#222] border-b border-zinc-800 flex items-center px-3 gap-2 shrink-0 select-none"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-1 bg-zinc-900 border border-zinc-700 rounded px-1.5 py-0.5 shadow-lg",title:"Kéo để di chuyển toolbar",style:{cursor:'grab'}},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 mr-1 text-zinc-600"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"grip-vertical",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('select');showToast('Select Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='select'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Select Tool (V)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"mouse-pointer",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('grab');showToast('Grab Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='grab'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Grab Tool (H)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"hand",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-0.5 border border-zinc-700 rounded bg-zinc-850 px-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('razor');showToast('Razor Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='razor'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Razor Tool (C)"},/*#__PURE__*/React.createElement("svg",{className:"w-3.5 h-3.5 text-orange-400",viewBox:"0 0 24 24",fill:"none",stroke:"currentColor",strokeWidth:"2.5",strokeLinecap:"round",strokeLinejoin:"round"},/*#__PURE__*/React.createElement("path",{d:"M6 3h12a2 2 0 0 1 2 2v2a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z"}),/*#__PURE__*/React.createElement("path",{d:"M4 9h16l-3 9H7z"}),/*#__PURE__*/React.createElement("circle",{cx:"12",cy:"6",r:"1"}))),/*#__PURE__*/React.createElement("button",{onClick:handleGlueTracks,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-purple-400 hover:bg-zinc-800",title:"Glue Clips"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"link",className:"w-3.5 h-3.5"})))),/*#__PURE__*/React.createElement("button",{onClick:()=>{setActiveTool('pen');showToast('Pen Tool','info');},className:`w-7 h-7 flex items-center justify-center rounded ${activeTool==='pen'?'bg-cyan-700 text-white':'text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800'}`,title:"Pen Tool (P)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"pen-tool",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:handleCutTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-red-400 hover:bg-zinc-800",title:"Cut (Ctrl+X)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handleCopyTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-blue-400 hover:bg-zinc-800",title:"Copy (Ctrl+C)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handlePasteTrack,className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-emerald-400 hover:bg-zinc-800",title:"Paste (Ctrl+V)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:addNewTrack,className:"px-2 py-1 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",title:"Thêm Track Mới (Ctrl+I)"},/*#__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"})),/*#__PURE__*/React.createElement("span",null,"Track")),sessionTabs.some(s=>s.id===activeTab)&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSaveSectionTab(activeTab),className:"px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold rounded text-xs flex items-center gap-1 shadow transition",title:"Lưu Section vào Main Session"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3.5 h-3.5"})),/*#__PURE__*/React.createElement("span",null,"Lưu Section"))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-700 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:handleUndo,disabled:undoStack.length===0&&(!window.UndoRedoEngine||!window.UndoRedoEngine.canUndo()),className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",title:"Undo (Ctrl+Z)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"undo",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handleRedo,disabled:redoStack.length===0&&(!window.UndoRedoEngine||!window.UndoRedoEngine.canRedo()),className:"w-7 h-7 flex items-center justify-center rounded text-zinc-400 hover:text-zinc-200 hover:bg-zinc-800 disabled:opacity-30",title:"Redo (Ctrl+Y)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"redo",className:"w-3.5 h-3.5"})))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-800 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>seekPlaybackTo(0),className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Quay lại đầu"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"skip-back",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){const st=subTabs.find(s=>s.id===activeTab);if(!st)return;const left=st.selectionStart!==null&&st.selectionEnd!==null?Math.min(st.selectionStart,st.selectionEnd):null;if(left!==null)seekPlaybackTo(left);}else{if(selLeft!==null)seekPlaybackTo(selLeft);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Đầu vùng chọn"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-back",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:handlePlayPause,className:`w-7 h-7 flex items-center justify-center rounded border transition ${isPlaying?'bg-emerald-600 text-black border-emerald-500 hover:bg-emerald-500':'bg-cyan-600 text-white border-cyan-500 hover:bg-cyan-500'}`,title:isPlaying?"Tạm dừng":"Play"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":isPlaying?"pause":"play",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:handleStop,className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Stop"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"square",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:handleRecordClick,className:`w-7 h-7 flex items-center justify-center rounded border transition ${recordingState==='RECORDING'?'bg-red-600 text-white border-red-500 hover:bg-red-500 animate-pulse':recordingState==='COUNT_IN'?'bg-amber-500 text-black border-amber-400 hover:bg-amber-400 animate-pulse':'bg-zinc-800 text-red-500 border-zinc-700 hover:bg-zinc-700 hover:text-red-400'}`,title:recordingState==='RECORDING'?"Đang ghi âm...":recordingState==='COUNT_IN'?"Chuẩn bị ghi âm...":"Ghi âm (Record)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"circle",className:"w-3.5 h-3.5 fill-current"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){setSubTabs(prev=>prev.map(s=>{if(s.id!==activeTab)return s;const right=s.selectionStart!==null&&s.selectionEnd!==null?Math.max(s.selectionStart,s.selectionEnd):null;return right!==null?{...s,currentTime:right}:s;}));}else{if(selRight!==null)setCurrentTime(selRight);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Cuối vùng chọn"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"step-forward",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const isSubTab=subTabs.some(sub=>sub.id===activeTab);if(isSubTab){setSubTabs(prev=>prev.map(s=>{if(s.id!==activeTab)return s;const duration=s.buffer?s.buffer.duration/(s.speed||1.0):0;return{...s,currentTime:duration};}));}else{setCurrentTime(maxDuration);}},className:"w-7 h-7 flex items-center justify-center bg-zinc-800 hover:bg-zinc-700 text-zinc-200 rounded border border-zinc-700 transition",title:"Đến cuối"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"skip-forward",className:"w-3.5 h-3.5"}))),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-5 bg-zinc-800 mx-0.5"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{setIsLoopingSelection(prev=>!prev);// Sync loop state with active sub-tab (piano roll / audio editor) const activeSub=activeTab&&subTabs.find(s=>s.id===activeTab&&['PIANO_ROLL','AUDIO_CLIP_EDITOR','SECTION_EDITOR'].includes(s.type));if(activeSub){const newLoop=!activeSub.isLooping;setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,isLooping:newLoop}:s));if(newLoop){const bpmVal=parseInt(bpm)||120;const beatSec=60.0/bpmVal;let maxEnd=0;if(activeSub.type==='PIANO_ROLL'){(activeSub.notes||[]).forEach(n=>{const end=(n.start_beat||0)+(n.duration_beats||1);if(end>maxEnd)maxEnd=end;});}else if(activeSub.type==='SECTION_EDITOR'){(activeSub.clips||[]).forEach(c=>{const end=(c.startTime||0)+(c.duration||0);if(end>maxEnd)maxEnd=end;});}else if(activeSub.type==='AUDIO_CLIP_EDITOR'){const dur=activeSub.buffer?.duration||0;if(dur>maxEnd)maxEnd=dur;}const loopEndTime=activeSub.type==='PIANO_ROLL'?Math.max(maxEnd,16)*beatSec+1.0:Math.max(maxEnd,1);setSubTabs(prev=>prev.map(s=>s.id===activeTab?{...s,selectionStart:0,selectionEnd:loopEndTime}:s));}}else{// Main timeline / section tab: nếu user đã chọn vùng loop riêng → @@ -2144,7 +2148,7 @@ if(window.__mediaExplorerDragFile){const f=await resolveMediaExplorerDropFile(); 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 // (quét 8 bars hiển thị "0 đến 8"): nhập 8 → selection tới vạch 8. setSelectionEnd(t);setNumberBar(Math.max(0,b-beginBar));},className:"w-[60px] bg-black text-white text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"#"),/*#__PURE__*/React.createElement("input",{type:"number",min:"1",value:numberBar,readOnly:true,className:"w-[60px] bg-black text-zinc-400 text-[18px] px-1 py-0 rounded border border-zinc-800 text-center font-mono"}),/*#__PURE__*/React.createElement("button",{onClick:()=>setSelectionFollowsTempo(prev=>!prev),className:`px-1.5 py-0 text-[18px] rounded border font-bold ${selectionFollowsTempo?'bg-amber-700 text-white border-amber-500':'bg-zinc-800 text-zinc-400 border-zinc-700'}`,title:selectionFollowsTempo?"Selection theo tempo (đổi BPM → selection thay đổi)":"Selection theo thời gian (cố định)"},selectionFollowsTempo?"♪T":"⏱T"),/*#__PURE__*/React.createElement("div",{className:"w-[1px] h-6 bg-zinc-800 mx-1"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"Start:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selLeft!==null?formatTime(selLeft):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));clearLocalSelection();setSelectionMode('global');setSelectionStart(secs);}},className:"w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"End:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selRight!==null?formatTime(selRight):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));setSelectionEnd(secs);}},className:"w-24 bg-black text-zinc-200 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"}),/*#__PURE__*/React.createElement("span",{className:"text-[18px] text-zinc-500"},"Len:"),/*#__PURE__*/React.createElement("input",{type:"text",value:selLeft!==null&&selRight!==null?formatTime(Math.abs(selRight-selLeft)):'',onChange:e=>{const parts=e.target.value.split(/[:.]/);if(parts.length===3&&selLeft!==null){const secs=parseInt(parts[0])*60+parseInt(parts[1])+parseFloat('0.'+(parts[2]||'0'));setSelectionEnd(selLeft+secs);}},className:"w-24 bg-black text-amber-300 text-[18px] px-1 py-0 rounded border border-zinc-700 text-center font-mono"})),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>setShowExportPanel(p=>!p),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showExportPanel?'bg-cyan-900 text-cyan-300':'text-zinc-500 hover:text-zinc-300'}`,title:`Export (floating modal)`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"text-[7px] opacity-60"},showExportPanel?panelPositions.export[0].toUpperCase():'')),/*#__PURE__*/React.createElement("button",{onClick:()=>setShowAIPanel(p=>!p),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showAIPanel?'bg-purple-900 text-purple-300':'text-zinc-500 hover:text-zinc-300'}`,title:`AI Panel (${panelPositions.ai})`},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3"})),/*#__PURE__*/React.createElement("span",{className:"text-[7px] opacity-60"},showAIPanel?panelPositions.ai[0].toUpperCase():'')),/*#__PURE__*/React.createElement("button",{onClick:()=>window.__toggleMixerRef(),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMixer?'bg-indigo-900 text-indigo-300':'text-zinc-500 hover:text-zinc-300'}`,title:"Mixer Panel (F7)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"sliders-horizontal",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("button",{onClick:()=>window.__toggleMediaExplorerRef(),className:`px-1.5 py-0.5 rounded text-xs font-semibold transition flex items-center gap-1 ${showMediaExplorer?'bg-emerald-900 text-emerald-300':'text-zinc-500 hover:text-zinc-300'}`,title:"Media Explorer Panel (F6)"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"folder-open",className:"w-3 h-3"}))),/*#__PURE__*/React.createElement("span",{className:"w-[1px] h-3 bg-zinc-800 mx-1"}),/*#__PURE__*/React.createElement("span",{className:"flex items-center gap-1"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"info",className:"w-3 h-3 text-zinc-600"})),prHint?/*#__PURE__*/React.createElement("span",{className:"text-cyan-400"},prHint):/*#__PURE__*/React.createElement("span",{className:"text-zinc-600 italic"},"Adaptive tips: hover vào vùng làm việc để xem hướng dẫn")))),contextMenu&&(contextMenu.isSubTab?/*#__PURE__*/React.createElement("div",{className:"fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",style:{left:Math.min(contextMenu.x,window.innerWidth-260),top:contextMenu.y+200>window.innerHeight?undefined:contextMenu.y,bottom:contextMenu.y+200>window.innerHeight?window.innerHeight-contextMenu.y:undefined,maxHeight:'60vh'},onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"px-3 py-1 text-xs text-zinc-500 font-bold uppercase border-b border-zinc-800 pb-1 mb-1"},"Selection: ",formatTime(subTabs.find(s=>s.id===contextMenu.subTabId)?.selectionStart||0)," - ",formatTime(subTabs.find(s=>s.id===contextMenu.subTabId)?.selectionEnd||0)),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabCut(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-rose-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Cut"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+X")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabCopy(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5 text-zinc-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Copy"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+C")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabPaste(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Paste"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+V")),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabDelete(contextMenu.subTabId);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3.5 h-3.5 text-red-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Delete Selected Segment"),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 text-xs font-semibold font-mono ml-auto"},"Del")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:()=>{handleSubTabLoop(contextMenu.subTabId,4);closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"repeat",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Loop Selection 4 times"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto"},"Ctrl+L"))):/*#__PURE__*/React.createElement("div",{className:"fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",style:{left:Math.min(contextMenu.x,window.innerWidth-260),top:contextMenu.y+260>window.innerHeight?undefined:contextMenu.y,bottom:contextMenu.y+260>window.innerHeight?window.innerHeight-contextMenu.y:undefined,maxHeight:'60vh'},onClick:e=>e.stopPropagation()},contextMenu.sectionId?/*#__PURE__*/React.createElement("button",{onClick:contextMenuEditSection,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-edit",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Edit Section"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+E")):/*#__PURE__*/React.createElement("button",{onClick:contextMenuEdit,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-edit",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Edit"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+E")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuSplit,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Split"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"S")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuMerge,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"combine",className:"w-3.5 h-3.5 text-purple-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Merge"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+M")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("div",{className:"px-3 py-0.5 text-[10px] text-zinc-500 font-bold uppercase tracking-wider"},"Insert"),!sessionTabs.some(s=>s.id===activeTab)&&/*#__PURE__*/React.createElement("button",{onClick:()=>{insertSectionAtPlayhead();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"folder-plus",className:"w-3.5 h-3.5 text-amber-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Section")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertMidiItemAtPlayhead();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"music",className:"w-3.5 h-3.5 text-purple-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert MIDI Item")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertSoundClipAtCursor();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"file-audio",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Sound Clip")),/*#__PURE__*/React.createElement("button",{onClick:()=>{insertTrackBelow();closeContextMenu();},className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"plus-square",className:"w-3.5 h-3.5 text-cyan-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Insert Track")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:contextMenuCopy,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"copy",className:"w-3.5 h-3.5 text-zinc-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Copy"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+C")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuCut,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"scissors",className:"w-3.5 h-3.5 text-rose-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Cut"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+X")),/*#__PURE__*/React.createElement("button",{onClick:contextMenuPaste,className:"w-full px-3 py-1.5 text-xs text-zinc-200 hover:bg-zinc-700 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"clipboard",className:"w-3.5 h-3.5 text-emerald-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Paste"),/*#__PURE__*/React.createElement("span",{className:"text-purple-400 text-xs font-semibold font-mono ml-auto pl-8"},"Ctrl+V")),/*#__PURE__*/React.createElement("div",{className:"h-px bg-zinc-700 my-1"}),/*#__PURE__*/React.createElement("button",{onClick:contextMenuDelete,className:"w-full px-3 py-1.5 text-xs text-red-300 hover:bg-red-950 text-left flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"trash-2",className:"w-3.5 h-3.5 text-red-400 shrink-0"})),/*#__PURE__*/React.createElement("span",{className:"flex-1"},"Delete"),/*#__PURE__*/React.createElement("span",{className:"text-amber-400 text-xs font-semibold font-mono ml-auto pl-8"},"Del")))),toastMessage&&/*#__PURE__*/React.createElement("div",{className:"absolute top-14 right-4 z-50 px-4 py-2.5 rounded shadow-lg text-xs font-semibold flex items-center gap-2 border bg-zinc-900 text-zinc-100 border-zinc-800"},/*#__PURE__*/React.createElement("span",{className:"inline-flex items-center shrink-0"},/*#__PURE__*/React.createElement("i",{"data-lucide":"bell",className:`w-4 h-4 ${toastMessage.type==='success'?'text-emerald-400':toastMessage.type==='error'?'text-rose-400':toastMessage.type==='warning'?'text-amber-400':'text-cyan-400'}`})),toastMessage.text,toastMessage.onActionClick&&/*#__PURE__*/React.createElement("button",{onClick:()=>{toastMessage.onActionClick();setToastMessage(null);},className:"ml-2 px-2 py-0.5 bg-purple-700 hover:bg-purple-600 text-white rounded text-[10px] uppercase font-bold"},toastMessage.actionText||'Tải về'))),appWarningModal&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[100] flex items-center justify-center bg-black/60 backdrop-blur-sm"},/*#__PURE__*/React.createElement("div",{className:"bg-[#222222] border border-zinc-800 rounded-lg shadow-2xl p-6 max-w-md w-full text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2 mb-3 text-cyan-400 font-bold"},/*#__PURE__*/React.createElement("h3",{className:"text-base font-bold"},appWarningModal.title)),/*#__PURE__*/React.createElement("div",{className:"text-xs text-slate-300 mb-5 leading-relaxed whitespace-pre-line"},appWarningModal.message),/*#__PURE__*/React.createElement("div",{className:"flex justify-end gap-2"},appWarningModal.isAlert?/*#__PURE__*/React.createElement("button",{onClick:()=>setAppWarningModal(null),className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"},"Đóng"):/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("button",{onClick:()=>setAppWarningModal(null),className:"px-4 py-2 bg-zinc-700 hover:bg-zinc-600 active:bg-zinc-800 text-slate-300 rounded text-xs font-semibold transition"},"Hủy"),/*#__PURE__*/React.createElement("button",{onClick:()=>{const fn=appWarningModal.onConfirm;setAppWarningModal(null);if(fn)fn();},className:"px-4 py-2 bg-cyan-600 hover:bg-cyan-500 active:bg-cyan-700 text-white rounded text-xs font-semibold transition"},"Xác nhận"))))),tabContextMenu&&/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-[190]",onClick:()=>setTabContextMenu(null),onContextMenu:e=>{e.preventDefault();setTabContextMenu(null);}}),/*#__PURE__*/React.createElement("div",{className:"fixed bg-zinc-900 border border-zinc-800 rounded-lg shadow-2xl p-2 z-[200] flex gap-1.5",style:{top:tabContextMenu.y,left:tabContextMenu.x},onClick:e=>e.stopPropagation()},['#f43f5e','#f59e0b','#10b981','#06b6d4','#8b5cf6','#64748b'].map(color=>/*#__PURE__*/React.createElement("button",{key:color,className:"w-5 h-5 rounded-full border border-zinc-700 hover:scale-110 active:scale-95 transition",style:{backgroundColor:color},onClick:()=>{handleSetTabColor(tabContextMenu.tabId,tabContextMenu.tabType,color);setTabContextMenu(null);}})))),/*#__PURE__*/React.createElement(AuthModal,{isOpen:authModalOpen,mode:authMode,forceMandatory:isMandatoryLogin,onClose:()=>setAuthModalOpen(false),onSuccess:handleAuthSuccess}),/*#__PURE__*/React.createElement(ProfileModal,{isOpen:profileModalOpen,onClose:()=>setProfileModalOpen(false),tracks:tracks,setTracks:setTracks,setSelectedTrackId:setSelectedTrackId,projectName:projectName,setProjectName:setProjectName,currentProjectId:currentProjectId,setCurrentProjectId:setCurrentProjectId,showToast:showToast,loadAudioBuffersForTracks:loadAudioBuffersForTracks,setAppWarningModal:setAppWarningModal,bpm:bpm,setBpm:setBpm,setMasteringSettings:setMasteringSettings,setSessionTabs:setSessionTabs,setSubTabs:setSubTabs,trackMidiChannelsRef:trackMidiChannelsRef}),/*#__PURE__*/React.createElement(SaveProjectModal,{isOpen:saveProjectModalOpen,onClose:()=>setSaveProjectModalOpen(false),projectName:projectName,onSaveCloud:(newName,existingId)=>{handleSaveCloudProject(newName,existingId);},onSaveLocal:newName=>{handleSaveLocalProject(newName);}}),/*#__PURE__*/React.createElement(SaveAsModal,{isOpen:saveAsModalOpen,onClose:()=>setSaveAsModalOpen(false),projectName:projectName,onSaveCloud:newName=>{handleSaveAsCloud(newName);},onSaveLocal:newName=>{handleExportSFS(newName);setProjectName(newName);localStorage.setItem('sonic_project_name',newName);}}),/*#__PURE__*/React.createElement(OpenProjectModal,{isOpen:openProjectModalOpen,onClose:()=>setOpenProjectModalOpen(false),onOpenCloud:(projId,projName)=>{setOpenProjectModalOpen(false);handleOpenProject(projId,projName);},onOpenLocal:()=>{setOpenProjectModalOpen(false);handleImportSFS();}}),/*#__PURE__*/React.createElement(AIConfigModal,{isOpen:aiConfigModalOpen,onClose:()=>setAiConfigModalOpen(false),onConfigSaved:providers=>{setAiProviders(providers);const active=providers.find(p=>p.is_active)||providers[0];if(active)setSelectedProviderId(active.id);}}),/*#__PURE__*/React.createElement(AboutModal,{isOpen:aboutModalOpen,onClose:()=>setAboutModalOpen(false)}),/*#__PURE__*/React.createElement(HelpModal,{isOpen:helpModalOpen,onClose:()=>setHelpModalOpen(false),lang:prefs.language}),/*#__PURE__*/React.createElement(PreferencesModal,{isOpen:preferencesModalOpen,onClose:()=>setPreferencesModalOpen(false),prefs:prefs,onPrefsChange:handlePrefsChange}),/*#__PURE__*/React.createElement(SystemManagerModal,{isOpen:systemManagerModalOpen,onClose:()=>setSystemManagerModalOpen(false)}),/*#__PURE__*/React.createElement(PluginManagerModal,{isOpen:pluginManagerModalOpen,onClose:()=>setPluginManagerModalOpen(false),pluginsData:pluginsData,onInsertInstrument:inst=>{// Chèn instrument (soundfont bank/program) vào track đang chọn — nút Synth -const tid=selectedTrackId||activeTracks&&activeTracks[0]&&activeTracks[0].id;if(tid&&inst&&inst.instrumentId){setTrackInstrumentWithProgram(tid,inst.instrumentId,inst.program,inst.displayName||inst.name,inst.bank);showToast('Đã chèn nhạc cụ: '+(inst.displayName||inst.name),'success');}setPluginManagerModalOpen(false);}}),/*#__PURE__*/React.createElement(AIPresetModal,{isOpen:aiPresetModalOpen,onClose:()=>{setAiPresetModalOpen(false);setAiPresetVersion(v=>v+1);}}),/*#__PURE__*/React.createElement(MasteringModal,{isOpen:showMasteringModal,onClose:()=>setShowMasteringModal(false),masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings}),fxRackTarget&&/*#__PURE__*/React.createElement(FXRackModal,{track:(activeTracksRef.current||[]).find(t=>t.id===fxRackTarget.trackId)||tracks.find(t=>t.id===fxRackTarget.trackId)||null,onUpdateTrack:updateTrackProp,onClose:()=>setFxRackTarget(null)}),vkbOpen&&/*#__PURE__*/React.createElement(VirtualMidiKeyboard,{settings:vkbSettings,onChange:vkbUpdateSettings,tracks:activeTracks,activePitches:activeMidiPitches,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,onClose:()=>window.__toggleVkb()}),/*#__PURE__*/React.createElement(ExportModal,{open:showExportPanel,onClose:()=>setShowExportPanel(false),exportSettings:exportSettings,setExportSettings:setExportSettings,isExporting:isExporting,onExport:triggerWavExport,onBounce:triggerBounceExport}),instrumentSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:closeInstrumentSelector},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Select Instrument"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("select",{value:instrumentSelectorTrackId||'',onChange:e=>{var tid=e.target.value;if(tid){openInstrumentSelector(tid);}},className:"bg-[#1e1e1e] text-zinc-300 border border-zinc-700 rounded px-2 py-1 text-xs font-mono cursor-pointer"},activeTracks.map(function(at){return/*#__PURE__*/React.createElement("option",{key:at.id,value:at.id},at.name+' ('+(at.instrumentName||'Synth')+')');})),/*#__PURE__*/React.createElement("button",{onClick:closeInstrumentSelector,className:"text-slate-400 hover:text-slate-200"},"\u2715"))),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:sfPresetSearchQuery,onChange:e=>setSfPresetSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border border-zinc-700 rounded px-2 py-1.5 outline-none my-3 text-sm"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-4",style:{height:"420px"}},/*#__PURE__*/React.createElement("div",{className:"w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setSelectedSoundFontId(null);setSynthCategory(null);},className:"w-full text-left px-3 py-2 text-sm rounded "+(!selectedSoundFontId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},"All Instruments"),(instrumentSelectorData?.soundfonts||[]).map(sf=>{const sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;const sfName=sf.display||sf.name||sf.id;return/*#__PURE__*/React.createElement("button",{key:sfId,onClick:()=>{setSelectedSoundFontId(sfId);setSfPresets(null);const baseId=sfId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(baseId).then(data=>{if(data&&data.presets){const mapping=data.presets.map(p=>({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)}));setSfPresets(mapping);setInstrumentSelectorData(prev=>prev?{...prev,soundfonts:(prev.soundfonts||[]).map(s=>s.id===sf.id?{...s,presets:data.presets}:s)}:prev);}else{setSfPresets([]);}}).catch(()=>{setSfPresets([]);});},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectedSoundFontId===sfId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},sfName);}),// ── VST Instruments (scanned bởi Plugin Manager) ── +const tid=selectedTrackId||activeTracks&&activeTracks[0]&&activeTracks[0].id;if(tid&&inst&&inst.instrumentId){setTrackInstrumentWithProgram(tid,inst.instrumentId,inst.program,inst.displayName||inst.name,inst.bank);showToast('Đã chèn nhạc cụ: '+(inst.displayName||inst.name),'success');}setPluginManagerModalOpen(false);}}),/*#__PURE__*/React.createElement(AIPresetModal,{isOpen:aiPresetModalOpen,onClose:()=>{setAiPresetModalOpen(false);setAiPresetVersion(v=>v+1);}}),/*#__PURE__*/React.createElement(MasteringModal,{isOpen:showMasteringModal,onClose:()=>setShowMasteringModal(false),masteringSettings:masteringSettings,setMasteringSettings:setMasteringSettings}),fxRackTarget&&/*#__PURE__*/React.createElement(FXRackModal,{track:(activeTracksRef.current||[]).find(t=>t.id===fxRackTarget.trackId)||tracks.find(t=>t.id===fxRackTarget.trackId)||null,onUpdateTrack:updateTrackProp,onClose:()=>setFxRackTarget(null)}),vkbOpen&&/*#__PURE__*/React.createElement(VirtualMidiKeyboard,{settings:vkbSettings,onChange:vkbUpdateSettings,tracks:activeTracks,activePitches:activeMidiPitches,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,onClose:()=>window.__toggleVkb()}),chordsPanelOpen&&/*#__PURE__*/React.createElement(ChordsPanel,{onClose:()=>window.__toggleChordsPanel(),onInsert:insertChordProgression,onNoteOn:vkbNoteOn,onNoteOff:vkbNoteOff,bpm:bpm}),/*#__PURE__*/React.createElement(ExportModal,{open:showExportPanel,onClose:()=>setShowExportPanel(false),exportSettings:exportSettings,setExportSettings:setExportSettings,isExporting:isExporting,onExport:triggerWavExport,onBounce:triggerBounceExport}),instrumentSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:closeInstrumentSelector},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-4xl p-5 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-3 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-amber-400"},"Select Instrument"),/*#__PURE__*/React.createElement("div",{className:"flex items-center gap-2"},/*#__PURE__*/React.createElement("select",{value:instrumentSelectorTrackId||'',onChange:e=>{var tid=e.target.value;if(tid){openInstrumentSelector(tid);}},className:"bg-[#1e1e1e] text-zinc-300 border border-zinc-700 rounded px-2 py-1 text-xs font-mono cursor-pointer"},activeTracks.map(function(at){return/*#__PURE__*/React.createElement("option",{key:at.id,value:at.id},at.name+' ('+(at.instrumentName||'Synth')+')');})),/*#__PURE__*/React.createElement("button",{onClick:closeInstrumentSelector,className:"text-slate-400 hover:text-slate-200"},"\u2715"))),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:sfPresetSearchQuery,onChange:e=>setSfPresetSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border border-zinc-700 rounded px-2 py-1.5 outline-none my-3 text-sm"}),/*#__PURE__*/React.createElement("div",{className:"flex gap-4",style:{height:"420px"}},/*#__PURE__*/React.createElement("div",{className:"w-1/3 border-r border-[#383838] overflow-y-auto space-y-0.5 pr-2"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setSelectedSoundFontId(null);setSynthCategory(null);},className:"w-full text-left px-3 py-2 text-sm rounded "+(!selectedSoundFontId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},"All Instruments"),(instrumentSelectorData?.soundfonts||[]).map(sf=>{const sfId=sf.id.startsWith('sf_')?sf.id:'sf_'+sf.id;const sfName=sf.display||sf.name||sf.id;return/*#__PURE__*/React.createElement("button",{key:sfId,onClick:()=>{setSelectedSoundFontId(sfId);setSfPresets(null);const baseId=sfId.replace('sf_','');window.SonicAPI.listSoundfontInstruments(baseId).then(data=>{if(data&&data.presets){const mapping=data.presets.map(p=>({...p,_sfId:sfId,_sfName:sfName,_sfDisplay:sfName.substring(0,30)}));setSfPresets(mapping);setInstrumentSelectorData(prev=>prev?{...prev,soundfonts:(prev.soundfonts||[]).map(s=>s.id===sf.id?{...s,presets:data.presets}:s)}:prev);}else{setSfPresets([]);}}).catch(()=>{setSfPresets([]);});},className:"w-full text-left px-3 py-2 text-sm rounded "+(selectedSoundFontId===sfId?"bg-amber-700 text-white":"bg-zinc-800 hover:bg-zinc-700 text-zinc-300")},sfName);}),// ── VST Instruments (scanned bởi Plugin Manager) ── instrumentSelectorData?.vst_instruments?.length>0&&/*#__PURE__*/React.createElement("div",{className:"mt-3 text-[10px] text-zinc-500 uppercase font-bold px-1 flex items-center gap-1"},/*#__PURE__*/React.createElement("i",{"data-lucide":"cpu",className:"w-3 h-3 text-violet-400"}),"VST Instruments"),(instrumentSelectorData?.vst_instruments||[]).map(v=>/*#__PURE__*/React.createElement("button",{key:'vst_modal_'+(v.id||v.name),onClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,v.id,v.name||v.id);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-violet-800 text-zinc-300 flex items-center justify-between gap-2"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[9px] text-cyan-400 shrink-0"},v.type||"VST")))),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto space-y-0.5"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),sfPresets===null?(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},"Loading instruments...")):sfPresets.length>0?sfPresets.filter(p=>!selectedSoundFontId||p._sfId===selectedSoundFontId).filter(p=>!sfPresetSearchQuery||(p.name||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())||(p._sfName||'').toLowerCase().includes(sfPresetSearchQuery.toLowerCase())).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:i,onClick:()=>setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program),onDoubleClick:()=>{setTrackInstrumentWithUndo(instrumentSelectorTrackId,p._sfId,p.name||'Preset '+p.program,p.bank,p.program);closeInstrumentSelector();},className:"w-full text-left px-3 py-2 text-sm rounded bg-zinc-800/60 hover:bg-amber-800 text-zinc-300 truncate flex items-center gap-2"},/*#__PURE__*/React.createElement("span",{className:"text-xs text-zinc-500 shrink-0"},p._sfDisplay),p.bank===128?/*#__PURE__*/React.createElement("span",{className:"mr-1"},"🥁"):null,p.name||'Preset '+p.program)):(/*#__PURE__*/React.createElement("p",{className:"text-sm text-zinc-500 py-2"},sfPresetSearchQuery?"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 ph\u00f9 h\u1ee3p.":"No presets found.")))))),fxSelectorTrackId&&/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm",onClick:()=>setFxSelectorTrackId(null)},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-64 p-4 text-slate-200",onClick:e=>e.stopPropagation()},/*#__PURE__*/React.createElement("h3",{className:"text-sm font-bold text-purple-400 mb-3"},"Track FX"),/*#__PURE__*/React.createElement("div",{className:"space-y-1"},/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,null),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'chorus'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-cyan-800 text-cyan-300"},"Chorus"),/*#__PURE__*/React.createElement("button",{onClick:()=>handleSetTrackFx(fxSelectorTrackId,'reverb'),className:"w-full text-left px-3 py-2 text-xs rounded bg-zinc-800 hover:bg-purple-800 text-purple-300"},"Reverb")))),instrumentDropdownTrackId&&instrumentDropdownBtnRect&&/*#__PURE__*/React.createElement("div",{"data-instr-dropdown":"",className:"fixed z-[100] bg-[#1e1e1e] border border-zinc-700 rounded shadow-xl w-[220px] max-h-[300px] flex flex-col",style:{top:instrumentDropdownBtnRect.bottom+4,left:Math.max(4,Math.min(instrumentDropdownBtnRect.left,window.innerWidth-224))}},/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"T\u00ecm nh\u1ea1c c\u1ee5...",value:instrumentSearchQuery,onChange:e=>setInstrumentSearchQuery(e.target.value),autoFocus:true,className:"w-full bg-black border-b border-zinc-700 px-2 py-1.5 text-xs outline-none"}),/*#__PURE__*/React.createElement("div",{className:"flex-1 overflow-y-auto"},/*#__PURE__*/React.createElement("button",{onClick:()=>setTrackInstrumentWithUndo(instrumentDropdownTrackId,null),className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"},"None (Default Synth)"),filteredInstruments.soundfonts.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"SoundFonts"),filteredInstruments.soundfonts.map((sf,i)=>/*#__PURE__*/React.createElement("button",{key:"sfd_"+i,onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,"sf_"+sf.id,sf.display||sf.name||sf.id,undefined,undefined);},className:"w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},sf.display||sf.name||sf.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-amber-400 shrink-0 ml-1"},"SF"))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Instruments"),filteredInstruments.vst.map((v,i)=>/*#__PURE__*/React.createElement("div",{key:"vstd_"+i,className:"flex items-stretch"},/*#__PURE__*/React.createElement("button",{onClick:()=>{setInstrumentDropdownTrackId(null);setInstrumentDropdownBtnRect(null);setTrackInstrumentWithUndo(instrumentDropdownTrackId,v.id,v.name||v.id);},className:"flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},v.name||v.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-cyan-400 shrink-0 ml-1"},v.type||"VST")))),filteredInstruments.vst.length>0&&/*#__PURE__*/React.createElement("div",{className:"text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold"},"VST Presets (.vstpreset)"),(window.SonicRuntime&&window.SonicRuntime.presets||[]).map((p,i)=>/*#__PURE__*/React.createElement("button",{key:"psd_"+i,onClick:()=>setTrackPreset(instrumentDropdownTrackId,p.id),className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name||p.id),/*#__PURE__*/React.createElement("span",{className:"text-[10px] text-emerald-400 shrink-0 ml-1"},"PRESET"))),/*#__PURE__*/React.createElement("button",{onClick:()=>{const inp=document.createElement('input');inp.type='file';inp.accept='.vstpreset,.fxp,.fxb,.dspreset';inp.onchange=()=>{const f=inp.files&&inp.files[0];if(!f)return;window.SonicAPI.uploadPreset(f).then(function(r){if(r&&r.success){if(window.SonicRuntime)window.SonicRuntime.refreshPresets();showToast('Đã upload preset: '+(r.original_name||r.name||''),'success');}else{showToast('Upload preset thất bại','error');}}).catch(function(err){showToast('Upload lỗi: '+(err.message||err),'error');});};inp.click();},className:"w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",title:"Upload preset .vstpreset xuất từ Carla (native GUI)"},"\u2B06 Upload preset (t\u1EEB Carla...)"),!filteredInstruments.soundfonts.length&&!filteredInstruments.vst.length&&/*#__PURE__*/React.createElement("p",{className:"text-xs text-zinc-500 py-4 text-center"},"Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o"))));};// React onWheel is passive -> e.preventDefault() can't stop panel scroll, so the // ruler slips away from the cursor mid-scroll. Native capture listener (non-passive) // blocks scrolling whenever the wheel starts over a [data-sf-wheel] container. diff --git a/app/static/js/services/chordTheory.js b/app/static/js/services/chordTheory.js new file mode 100644 index 0000000..e9bf813 --- /dev/null +++ b/app/static/js/services/chordTheory.js @@ -0,0 +1,171 @@ +// SonicForge Studio - Chord Theory Service (BIG_PHASE_3.md §6.1, virtual_keyboard_and_chords_panel_spec.md §III) +// Thuần hàm: parse chord symbol / roman numeral → pitch array; áp voicing. +// Dùng được cả browser (script tag) lẫn Node (module.exports) cho unit test. + +const ChordTheory = (function() { + // Root note name → pitch class (C=0 ... B=11) + const NOTE_TO_PC = { + 'C': 0, 'C#': 1, 'Db': 1, 'D': 2, 'D#': 3, 'Eb': 3, 'E': 4, 'F': 5, + 'F#': 6, 'Gb': 6, 'G': 7, 'G#': 8, 'Ab': 8, 'A': 9, 'A#': 10, 'Bb': 10, 'B': 11 + }; + const ROOT_NAMES = Object.keys(NOTE_TO_PC).sort(function(a, b) { return b.length - a.length; }); // 'C#'/'Bb' trước 'C'/'B' + const PC_TO_NAME = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']; + + // Semitone intervals (ascending, relative to root). Octave = 4 mặc định + // (C4=60) — đúng spec §III.5: Pitches(Cmaj7) = [60,64,67,71]. + const QUALITY_INTERVALS = { + '': [0, 4, 7], + 'maj': [0, 4, 7], + 'm': [0, 3, 7], + 'min': [0, 3, 7], + '-': [0, 3, 7], + 'maj7': [0, 4, 7, 11], + 'M7': [0, 4, 7, 11], + '7': [0, 4, 7, 10], + 'm7': [0, 3, 7, 10], + 'min7': [0, 3, 7, 10], + 'maj9': [0, 4, 7, 11, 14], + 'M9': [0, 4, 7, 11, 14], + '9': [0, 4, 7, 10, 14], + 'm9': [0, 3, 7, 10, 14], + 'min9': [0, 3, 7, 10, 14], + '11': [0, 4, 7, 10, 14, 17], + 'm11': [0, 3, 7, 10, 14, 17], + '13': [0, 4, 7, 10, 14, 21], + 'm13': [0, 3, 7, 10, 14, 21], + 'add9': [0, 4, 7, 14], + 'sus2': [0, 2, 7], + 'sus4': [0, 5, 7], + 'sus': [0, 5, 7], + '7sus4': [0, 5, 7, 10], + 'dim': [0, 3, 6], + 'dim7': [0, 3, 6, 9], + 'aug': [0, 4, 8], + '+': [0, 4, 8], + '6': [0, 4, 7, 9], + 'm6': [0, 3, 7, 9], + 'm7b5': [0, 3, 6, 10], + '5': [0, 7] + }; + const QUALITY_KEYS = Object.keys(QUALITY_INTERVALS).sort(function(a, b) { return b.length - a.length; }); + + // Roman numeral degree → semitone offset trong major scale (spec: I-V-vi-IV + // với key C → C-G-Am-F; vi → A, IV → F, v → G...) + const ROMAN_DEGREE = { I: 0, II: 2, III: 4, IV: 5, V: 7, VI: 9, VII: 11 }; + + function noteToMidi(rootName, octave) { + const pc = NOTE_TO_PC[rootName] !== undefined ? NOTE_TO_PC[rootName] : 0; + return 12 * ((octave == null ? 4 : octave) + 1) + pc; + } + + // 'Cmaj7' → [60,64,67,71]; 'Am9' → [57,60,64,71]; 'G13' → [67,71,74,77,81,88] + function parseChordSymbol(symbol, octave) { + if (!symbol) return []; + const s = String(symbol).trim(); + if (!s) return []; + const upper = s.toUpperCase(); + const root = ROOT_NAMES.find(function(n) { return upper.indexOf(n) === 0; }); + if (!root) return []; + // Giữ nguyên case của suffix: 'm' = minor (Am9 → 'm9'), 'M7' = major 7th + const suffix = s.slice(root.length); + const quality = QUALITY_KEYS.find(function(q) { return suffix === q; }) || ''; + const intervals = QUALITY_INTERVALS[quality] || QUALITY_INTERVALS['']; + const rootMidi = noteToMidi(root, octave); + return intervals.map(function(i) { return rootMidi + i; }); + } + + // Kiểm tra symbol có phải chord name (bắt đầu bằng tên nốt) không + function isChordName(token) { + const upper = String(token || '').trim().toUpperCase(); + return ROOT_NAMES.some(function(n) { return upper.indexOf(n) === 0; }); + } + + // 'ii7' → { degree: 2, minor: true, suffix: '7', accidental: 0 } + // 'bVI' → { degree: 9, minor: false, suffix: '', accidental: -1 } + function parseRomanToken(token) { + const t = String(token || '').trim(); + // Alternation dài trước (VII trước VI trước V...); /i nhận cả chữ thường + const m = t.match(/^(b|#)?(VII|VI|IV|IX|III|II|V|I)(.*)$/i); + if (!m) return null; + const accidental = m[1] === 'b' ? -1 : (m[1] === '#' ? 1 : 0); + const numeral = m[2].toUpperCase(); + const minor = m[2] !== m[2].toUpperCase(); // 'vi' → minor, 'VI' → major + if (ROMAN_DEGREE[numeral] === undefined) return null; + const suffix = m[3] || ''; + let quality = null; + const SUFFIX_MAP = { + '': function(min) { return min ? 'm' : ''; }, + '7': function(min) { return min ? 'm7' : '7'; }, + '9': function(min) { return min ? 'm9' : '9'; }, + '13': function(min) { return min ? 'm13' : '13'; }, + 'maj7': function() { return 'maj7'; }, + 'M7': function() { return 'maj7'; }, + 'maj9': function() { return 'maj9'; }, + 'add9': function() { return 'add9'; }, + '6': function(min) { return min ? 'm6' : '6'; }, + 'sus4': function() { return 'sus4'; }, + 'sus2': function() { return 'sus2'; }, + '7sus4': function() { return '7sus4'; }, + 'dim': function() { return 'dim'; }, + 'dim7': function() { return 'dim7'; }, + 'm7b5': function() { return 'm7b5'; }, + 'aug': function() { return 'aug'; } + }; + const key = SUFFIX_MAP[suffix] || SUFFIX_MAP['']; + quality = key ? key(minor) : (minor ? 'm' : ''); + return { degree: ROMAN_DEGREE[numeral], minor: minor, suffix: suffix, accidental: accidental, quality: quality }; + } + + // Chuỗi tokens 'I-vi-IV-V' hoặc 'Cmaj7-Am9-Fadd9-G13' → [{ name, pitches }] + // key dùng cho roman numeral; voicing: 'root' | 'first' | 'drop2'. + function progressionToChords(tokens, key, voicing, octave) { + if (!tokens) return []; + const parts = String(tokens).split(/[-,\s]+/).filter(function(x) { return x.length > 0; }); + const keyRoot = key || 'C'; + return parts.map(function(token) { + if (isChordName(token)) { + const pitches = parseChordSymbol(token, octave); + return { token: token, name: token, pitches: applyVoicing(pitches, voicing) }; + } + const parsed = parseRomanToken(token); + if (!parsed) return { token: token, name: token, pitches: [] }; + const keyPc = NOTE_TO_PC[keyRoot] !== undefined ? NOTE_TO_PC[keyRoot] : 0; + const rootPc = (((keyPc + parsed.degree + parsed.accidental) % 12) + 12) % 12; + const rootName = PC_TO_NAME[rootPc]; + const quality = parsed.quality; + const name = rootName + quality; + const rootMidi = 12 * ((octave == null ? 4 : octave) + 1) + rootPc; + const intervals = QUALITY_INTERVALS[quality] || QUALITY_INTERVALS['']; + const pitches = intervals.map(function(i) { return rootMidi + i; }); + return { token: token, name: name, pitches: applyVoicing(pitches, voicing) }; + }); + } + + // Root position: giữ nguyên (sorted). First inversion: root +12. + // Drop-2: note cao thứ nhì -12 (spec §III.5). + function applyVoicing(pitches, voicing) { + if (!pitches || !pitches.length) return []; + const arr = pitches.slice().sort(function(a, b) { return a - b; }); + if (voicing === 'first' && arr.length >= 2) { + arr[0] = arr[0] + 12; + arr.sort(function(a, b) { return a - b; }); + return arr; + } + if (voicing === 'drop2' && arr.length >= 3) { + arr[arr.length - 2] = arr[arr.length - 2] - 12; + arr.sort(function(a, b) { return a - b; }); + return arr; + } + return arr; + } + + return { + parseChordSymbol: parseChordSymbol, + parseRomanToken: parseRomanToken, + isChordName: isChordName, + progressionToChords: progressionToChords, + applyVoicing: applyVoicing + }; +})(); + +if (typeof module !== 'undefined' && module.exports) module.exports = ChordTheory; diff --git a/app/static/js/services/chordTheory.test.js b/app/static/js/services/chordTheory.test.js new file mode 100644 index 0000000..38e2148 --- /dev/null +++ b/app/static/js/services/chordTheory.test.js @@ -0,0 +1,63 @@ +// SonicForge Studio - chordTheory.js unit test (BIG_PHASE_3.md §6.4) +// Node assert, không framework. Chạy: node app/static/js/services/chordTheory.test.js +const assert = require('assert'); +const ChordTheory = require('./chordTheory.js'); + +let fails = 0; +function check(name, fn) { + try { fn(); console.log('ok: ' + name); } + catch (e) { fails++; console.error('FAIL: ' + name + ' — ' + e.message); } +} +function deep(arr, expected) { assert.deepStrictEqual(arr, expected); } + +// 1. Parse chord symbol → pitch array (octave 4 = C4=60) +check('Cmaj7 = [60,64,67,71]', () => deep(ChordTheory.parseChordSymbol('Cmaj7'), [60, 64, 67, 71])); +check('Am9 = [69,72,76,79,83]', () => deep(ChordTheory.parseChordSymbol('Am9'), [69, 72, 76, 79, 83])); +check('Dm7 = [62,65,69,72]', () => deep(ChordTheory.parseChordSymbol('Dm7'), [62, 65, 69, 72])); +check('G13 = [67,71,74,77,81,88]', () => deep(ChordTheory.parseChordSymbol('G13'), [67, 71, 74, 77, 81, 88])); +check('Fadd9 = [65,69,72,79]', () => deep(ChordTheory.parseChordSymbol('Fadd9'), [65, 69, 72, 79])); + +// 2. Roman numeral progression với key C +check('I-vi-IV-V key C → C-Am-F-G', () => { + const prog = ChordTheory.progressionToChords('I-vi-IV-V', 'C', 'root'); + deep(prog.map(c => c.name), ['C', 'Am', 'F', 'G']); + deep(prog.map(c => c.pitches), [[60, 64, 67], [69, 72, 76], [65, 69, 72], [67, 71, 74]]); +}); +check('I-V-vi-IV key C → C-G-Am-F (preset Pop)', () => { + const prog = ChordTheory.progressionToChords('I-V-vi-IV', 'C', 'root'); + deep(prog.map(c => c.name), ['C', 'G', 'Am', 'F']); +}); +check('ii7-V7-Imaj7-VI7 key C → Dm7-G7-Cmaj7-A7', () => { + const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'root'); + deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']); + deep(prog[0].pitches, [62, 65, 69, 72]); // Dm7 + deep(prog[2].pitches, [60, 64, 67, 71]); // Cmaj7 +}); +check('i7-iv7-v7-i7 key C → Cm7-Fm7-Gm7-Cm7', () => { + const prog = ChordTheory.progressionToChords('i7-iv7-v7-i7', 'C', 'root'); + deep(prog.map(c => c.name), ['Cm7', 'Fm7', 'Gm7', 'Cm7']); +}); +check('Imaj7-iii7-IVmaj7-V13 key C → Cmaj7-Em7-Fmaj7-G13', () => { + const prog = ChordTheory.progressionToChords('Imaj7-iii7-IVmaj7-V13', 'C', 'root'); + deep(prog.map(c => c.name), ['Cmaj7', 'Em7', 'Fmaj7', 'G13']); +}); + +// 3. Voicing +check('first inversion: root +12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'first'), [64, 67, 71, 72])); +check('drop2: second-highest -12', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'drop2'), [55, 60, 64, 71])); +check('root position giữ nguyên', () => deep(ChordTheory.applyVoicing([60, 64, 67, 71], 'root'), [60, 64, 67, 71])); +check('jazz drop2: Dm7-G7-Cmaj7-A7 ra đủ pitch', () => { + const prog = ChordTheory.progressionToChords('ii7-V7-Imaj7-VI7', 'C', 'drop2'); + deep(prog.map(c => c.name), ['Dm7', 'G7', 'Cmaj7', 'A7']); + assert(prog.every(c => c.pitches.length === 4), 'mỗi chord 4 pitch'); +}); + +// 4. Edge cases +check('chuỗi chord name trực tiếp Cmaj7-Am9-Fadd9-G13', () => { + const prog = ChordTheory.progressionToChords('Cmaj7-Am9-Fadd9-G13', 'C', 'root'); + deep(prog.map(c => c.name), ['Cmaj7', 'Am9', 'Fadd9', 'G13']); + deep(prog[0].pitches, [60, 64, 67, 71]); +}); + +console.log(fails ? 'FAILED: ' + fails : 'ALL PASS'); +process.exit(fails ? 1 : 0); diff --git a/app/templates/index.html b/app/templates/index.html index 85f80cb..d34d3d4 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -50,7 +50,8 @@ - + +