From 45ab31d42e911fcddd0003be292282240898711e Mon Sep 17 00:00:00 2001 From: 3dtours Date: Fri, 7 Aug 2026 20:46:47 +0700 Subject: [PATCH] =?UTF-8?q?IMPROVE:=20th=C3=AAm=20c=C3=A1c=20t=C3=ADnh=20n?= =?UTF-8?q?=C4=83ng=20cho=20MIDI=20trong=20PIANO=20ROLL=20TAB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 446 +++++++++++++++++++++++++++---- app/static/js/app.precompiled.js | 33 ++- app/templates/index.html | 2 +- wiki.md | 39 +++ 4 files changed, 460 insertions(+), 60 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index feb5c6a..6476d04 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -7109,11 +7109,54 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable)) return; e.preventDefault(); setSelectedNoteIds(notes.map(n => n.id)); + return; + } + // Spec 20:12 shortcuts: Alt+A Arp, Alt+S Strum, Alt+R Humanize, + // Shift+C chord stamp, Shift+S lock-scale toggle + const inField = e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.isContentEditable); + // Ctrl+C/X/V — copy/paste/cut NOTES (thay 2 nút Copy/Paste đã bỏ — + // user 08:40) + if ((e.ctrlKey || e.metaKey) && !e.altKey && !inField) { + if (e.key === 'c') { + e.preventDefault(); + if (notes && notes.length) { + if (onCopyNotes) onCopyNotes(notes); + showToast('Đã copy ' + notes.length + ' nốt vào clipboard.', 'success'); + } else { showToast('Không có nốt để copy.', 'warning'); } + return; + } + if (e.key === 'x') { + e.preventDefault(); + if (notes && notes.length) { + if (onCopyNotes) onCopyNotes(notes); + pushToUndo(notes); + setNotes([]); + showToast('Đã cắt ' + notes.length + ' nốt.', 'success'); + } else { showToast('Không có nốt để cắt.', 'warning'); } + return; + } + if (e.key === 'v') { + e.preventDefault(); + if (!clipboardNotes || !clipboardNotes.length) { showToast('Clipboard trống — bấm Ctrl+C trước.', 'warning'); return; } + pushToUndo(notes); + setNotes(prev => [...(prev || []), ...clipboardNotes.map(function(n) { return { ...n, id: 'note_cp_' + Date.now() + '_' + Math.floor(Math.random() * 100000) }; })]); + showToast('Đã paste ' + clipboardNotes.length + ' nốt.', 'success'); + return; + } + } + if (e.altKey && !e.ctrlKey && !inField && e.key === 'a') { e.preventDefault(); setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }); return; } + if (e.altKey && !e.ctrlKey && !inField && e.key === 's') { e.preventDefault(); setStrumModal({ ms: 30, direction: 'DOWN' }); return; } + if (e.altKey && !e.ctrlKey && !inField && e.key === 'r') { e.preventDefault(); setHumanizeModal({ timingMs: 12, velRange: 15, durRange: 10 }); return; } + if (e.shiftKey && !e.ctrlKey && !e.altKey && !inField && e.key === 'c') { e.preventDefault(); setChordStampMode(m => !m); return; } + if (e.shiftKey && !e.ctrlKey && !e.altKey && !inField && e.key === 's') { + e.preventDefault(); + setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)); + return; } }; window.addEventListener('keydown', handler); return () => window.removeEventListener('keydown', handler); - }, [notes, setSelectedNoteIds]); + }, [notes, setSelectedNoteIds, clipboardNotes]); // Undo/redo stacks const undoStackRef = React.useRef([]); @@ -7390,11 +7433,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos canvas.height = h * dpr; ctx.scale(dpr, dpr); + // Scale highlight: rows thuộc scale (root+scale) sáng vàng; ngoài scale + // dim hơn (spec 20:12 — Scale Highlight & Lock) + const scHighlight = scaleWithRoot(); // Draw background rows for (let pitch = 0; pitch < 128; pitch++) { const y = (127 - pitch) * NoteHeight; const isBlack = [1, 3, 6, 8, 10].includes(pitch % 12); - ctx.fillStyle = isBlack ? '#1a1a1e' : '#25252a'; + const inScale = scHighlight ? scHighlight.includes(pitch % 12) : null; + ctx.fillStyle = isBlack ? (inScale === false ? '#141419' : '#1f1f25') : (inScale === false ? '#1b1b20' : (inScale === true ? '#2c2a1e' : '#25252a')); ctx.fillRect(0, y, viewWidth, NoteHeight); ctx.strokeStyle = '#2d2d35'; @@ -7883,6 +7930,12 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos return; } } + // Chord stamp mode: click canvas → stamp full chord shape (spec 20:12) + if (chordStampMode) { + const start = getSnapBeat(beat, snapValue); + applyChordStamp(start, snapToScaleRef.current ? snapPitchToScale(pitch, scaleWithRoot()) : pitch); + return; + } // Click on empty space with pen tool: DRAW a new note (brush mode with visitedPitches) pushToUndo(notes); const start = getSnapBeat(beat, snapValue); @@ -8232,6 +8285,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos return nearest; }; + // TẤT CẢ notes bắt đầu tại cùng beat (chord — velocity bars CÙNG cột) — + // user 08:25: ctrl+draw qua vị trí → vẽ lại TẤT CẢ velocity cùng vị trí + // (trước đây findCCNoteIndex chỉ trả 1 note → chord khó draw). + const findCCNoteIndicesAtBeat = (b) => { + const snapped = getSnapBeat(b, snapValue); + const out = []; + notes.forEach((n, idx) => { + if (Math.abs(n.start_beat - snapped) < 0.01) out.push(idx); + }); + return out; + }; + const handleCCMouseDown = (e) => { const canvas = ccCanvasRef.current; if (!canvas) return; @@ -8264,7 +8329,9 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos ccDragRef.current = { active: true, lastBeat: beat, selectedMode: true, lastPainted: [] }; } } else { - ccDragRef.current = { active: true, lastBeat: beat, lastPainted: noteIdx !== -1 ? [noteIdx] : [] }; + // Không chọn notes: vẽ TẤT CẢ notes cùng beat (chord — user 08:25) + const idxs = findCCNoteIndicesAtBeat(beat); + ccDragRef.current = { active: true, lastBeat: beat, lastPainted: idxs }; } return; } @@ -8318,19 +8385,18 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos return; } - const candidateIdx = findCCNoteIndex(beat, y, h); - if (candidateIdx !== -1 && !painted.includes(candidateIdx)) { - const currentVal = ccMode === 'pan' ? ((notes[candidateIdx].pan || 0) / 2.0 + 0.5) : (notes[candidateIdx].velocity !== undefined ? notes[candidateIdx].velocity : 0.8); - if (Math.abs(currentVal - val) > 0.001) { - const updatedNotes = notes.map((n, i) => { - if (i !== candidateIdx) return n; - if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 }; - return { ...n, velocity: val }; - }); - setNotes(updatedNotes); - if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); - } - drag.lastPainted = [...painted, candidateIdx]; + // Vẽ TẤT CẢ notes cùng beat (chord — user 08:25) — trước đây chỉ 1 note + const candidateIdxs = findCCNoteIndicesAtBeat(beat); + const unpainted = candidateIdxs.filter(ci => !painted.includes(ci)); + if (unpainted.length > 0) { + const updatedNotes = notes.map((n, i) => { + if (!unpainted.includes(i)) return n; + if (ccMode === 'pan') return { ...n, pan: (val - 0.5) * 2.0 }; + return { ...n, velocity: val }; + }); + setNotes(updatedNotes); + if (isPlaying && onRescheduleMidi) onRescheduleMidi(updatedNotes); + drag.lastPainted = [...painted, ...unpainted]; } }; @@ -8458,6 +8524,17 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos const [selectedScale, setSelectedScale] = React.useState(null); const selectedScaleRef = React.useRef(null); selectedScaleRef.current = selectedScale; + // Root note (0-11 — C..B) — transpose scale highlight + snap (spec 20:12) + const [scaleRoot, setScaleRoot] = React.useState(0); + const scaleRootRef = React.useRef(0); + scaleRootRef.current = scaleRoot; + // Modal states: Arpeggiator / Strummer / Humanize (spec 20:12) + const [arpModal, setArpModal] = React.useState(null); // { pattern, rate, octaves, gate } + const [strumModal, setStrumModal] = React.useState(null); // { ms, direction } + const [humanizeModal, setHumanizeModal] = React.useState(null); // { timingMs, velRange, durRange } + const [chordType, setChordType] = React.useState('triad'); // chord stamp (spec) + const [chordStampMode, setChordStampMode] = React.useState(false); // stamp mode toggle + const [velocityTarget, setVelocityTarget] = React.useState(80); // compress target (spec) const snapToScaleRef = React.useRef(true); snapToScaleRef.current = st.snapToScale !== undefined ? st.snapToScale : true; const [scaleMenuPos, setScaleMenuPos] = React.useState(null); @@ -8476,6 +8553,188 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos return octave * 12 + best; }; + // ── MIDI Transform Engine (spec 20:12 — direct integration) ── + const scaleWithRoot = () => { + 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 < p.octaves; o++) { + sorted.forEach(n => { + 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; i < steps; i++) { + if (p.pattern === 'CHORD') { pool.forEach(n => seqPitches.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; @@ -8593,16 +8852,6 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); className: "flex items-center gap-1 text-xs" }, React.createElement("span", { className: "text-zinc-500 font-semibold" - }, "Snap to Scale"), React.createElement("button", { - onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)), - className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`, - style: { padding: 0 } - }, React.createElement("div", { - className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}` - }))), 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); }, @@ -8661,33 +8910,23 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); }), "Đóng"), React.createElement("div", { style: { flexBasis: "100%", height: 0 } }), React.createElement("button", { - onClick: applyHumanize, - className: "px-2 py-1 rounded text-xs bg-amber-900/40 text-amber-300 border border-amber-700/60 hover:bg-amber-800/50 transition", - title: "Humanize: randomize velocity + timing" - }, "\uD83C\uDF9A Humanize"), React.createElement("select", { - key: "humstr", value: humanizeStrength, - onChange: function(e) { setHumanizeStrength(parseFloat(e.target.value)); }, - className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer", - title: "Cường độ humanize" - }, React.createElement("option", { key: "l", value: 0.05 }, "Nh\u1EB9"), React.createElement("option", { key: "m", value: 0.10 }, "V\u1EEBa"), React.createElement("option", { key: "s", value: 0.18 }, "M\u1EA1nh")), React.createElement("button", { - onClick: function() { - if (notes && notes.length) { - if (onCopyNotes) onCopyNotes(notes); - showToast('Đã copy ' + notes.length + ' nốt vào clipboard — mở tab khác + Paste.', 'success'); - } else { showToast('Không có nốt để copy.', 'warning'); } - }, + onClick: () => setArpModal({ pattern: 'UP', rate: '1/16', octaves: 2, gate: 80, triplet: false, dotted: false }), className: "px-2 py-1 rounded text-xs bg-cyan-900/40 text-cyan-300 border border-cyan-700/60 hover:bg-cyan-800/50 transition", - title: "Copy toàn bộ notes vào clipboard (paste sang PIANO ROLL TAB khác)" - }, "\uD83D\uDCCB Copy"), React.createElement("button", { - onClick: function() { - if (!clipboardNotes || !clipboardNotes.length) { showToast('Clipboard trống — bấm Copy trước.', 'warning'); return; } - pushToUndo(notes); - setNotes(prev => [...(prev || []), ...clipboardNotes.map(function(n) { return { ...n, id: 'note_cp_' + Date.now() + '_' + Math.floor(Math.random() * 100000) }; })]); - showToast('Đã paste ' + clipboardNotes.length + ' nốt từ clipboard.', 'success'); - }, + title: "Arpeggiate (Alt+A)" + }, "ARP"), React.createElement("button", { + onClick: () => setStrumModal({ ms: 30, direction: 'DOWN' }), className: "px-2 py-1 rounded text-xs bg-teal-900/40 text-teal-300 border border-teal-700/60 hover:bg-teal-800/50 transition", - title: "Paste notes từ clipboard vào tab này (append cuối)" - }, "\uD83D\uDCE5 Paste"), React.createElement("div", { + title: "Strum (Alt+S)" + }, "STRUM"), React.createElement("button", { + onClick: () => setHumanizeModal({ timingMs: Math.round(humanizeStrength * 100), velRange: Math.round(humanizeStrength * 127), durRange: Math.round(humanizeStrength * 50) }), + className: "px-2 py-1 rounded text-xs bg-amber-900/40 text-amber-300 border border-amber-700/60 hover:bg-amber-800/50 transition", + title: "Humanize (Alt+R)" + }, "HUMANIZE"), React.createElement("select", { + key: "humstr", value: humanizeStrength, + onChange: function(e) { var v = parseFloat(e.target.value); setHumanizeStrength(v); setHumanizeModal({ timingMs: Math.round(v * 100), velRange: Math.round(v * 127), durRange: Math.round(v * 50) }); }, + className: "px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer", + title: "Mức humanize (mở modal theo mức)" + }, React.createElement("option", { key: "l", value: 0.05 }, "Nh\u1EB9"), React.createElement("option", { key: "m", value: 0.10 }, "V\u1EEBa"), React.createElement("option", { key: "s", value: 0.18 }, "M\u1EA1nh")), React.createElement("div", { key: "transpose", className: "flex items-center gap-1" }, React.createElement("input", { key: "in", type: "number", step: 1, min: -24, max: 24, value: transposeSemis, @@ -8714,11 +8953,89 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); key: "btn", onClick: applyTransposeToKey, className: "px-2 py-1 rounded text-xs bg-violet-900/40 text-violet-300 border border-violet-700/60 hover:bg-violet-800/50 transition", title: "Chuyển giọng: map degree hiện tại sang giọng đích (auto-detect key nguồn)" - }, "🎵 Chuyển giọng")), React.createElement("div", { + }, "🎵 Chuyển giọng")), /* ── CÙNG HÀNG (sau Chuyển giọng — user 08:40: gộp 1 hàng, bỏ spacer) ── */ React.createElement("div", { + className: "flex items-center gap-1 text-xs" + }, React.createElement("span", { + className: "text-zinc-500 font-semibold" + }, "Snap to Scale"), React.createElement("button", { + onClick: () => setSubTabs(prev => prev.map(s => s.id === st.id ? { ...s, snapToScale: !(s.snapToScale !== undefined ? s.snapToScale : true) } : s)), + className: `w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'bg-yellow-600' : 'bg-zinc-700'}`, + style: { padding: 0 } + }, React.createElement("div", { + className: `w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale !== undefined ? st.snapToScale : true) ? 'translate-x-3.5' : 'translate-x-0.5'}` + }))), React.createElement("div", { + className: "flex items-center gap-1 text-xs" + }, React.createElement("span", { + className: "text-zinc-500 font-semibold" + }, "Scale:"), React.createElement("select", { + value: JSON.stringify(selectedScale || null), + onChange: e => { + const v = e.target.value; + setSelectedScale(v === 'null' ? null : JSON.parse(v)); + 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 max-w-[110px]" + }, React.createElement("option", { value: "null" }, "None"), (function() { + const opts = []; + const pushKey = (label, val) => opts.push(React.createElement("option", { key: label, value: JSON.stringify(val) }, label)); + Object.keys(SCALES || {}).forEach(k => { + const v = SCALES[k]; + if (v === null) return; + if (Array.isArray(v)) pushKey(k, v); + else Object.keys(v).forEach(sk => pushKey(sk, v[sk])); + }); + return opts; + }())), React.createElement("select", { + value: scaleRoot, + onChange: e => { setScaleRoot(parseInt(e.target.value) || 0); 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" + }, ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'].map((r, i) => React.createElement("option", { key: r, value: i }, r))), React.createElement("button", { + onClick: () => applyForceToScale(), + title: "Force selected notes to scale", + className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-amber-400 border border-zinc-700 hover:border-amber-600" + }, "Force")), React.createElement("div", { + className: "flex items-center gap-1 text-xs" + }, React.createElement("span", { + className: "text-zinc-500 font-semibold ml-1" + }, "Chord:"), React.createElement("select", { + value: chordType, + onChange: e => setChordType(e.target.value), + className: "bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-[10px] outline-none" + }, React.createElement("option", { value: "triad" }, "Triad"), React.createElement("option", { value: "min7" }, "Min7"), React.createElement("option", { value: "sus4" }, "Sus4"), React.createElement("option", { value: "add9" }, "Add9")), React.createElement("button", { + onClick: () => setChordStampMode(m => !m), + title: "Chord stamp mode — click canvas to stamp chord (Shift+C)", + className: `px-1.5 py-0.5 rounded text-[10px] border ${chordStampMode ? 'bg-amber-800/60 text-amber-300 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-zinc-700 hover:border-amber-600'}` + }, "Stamp"), React.createElement("button", { + onClick: () => applyHarmonize(3), + title: "Harmonize +3rd", + className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600" + }, "+3"), React.createElement("button", { + onClick: () => applyHarmonize(5), + title: "Harmonize +5th", + className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600" + }, "+5"), React.createElement("button", { + onClick: () => applyHarmonize(7), + title: "Harmonize +7th", + className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600" + }, "+7")), React.createElement("div", { + className: "flex items-center gap-1 text-xs" + }, React.createElement("span", { + className: "text-zinc-500 font-semibold" + }, "Vel:"), React.createElement("button", { + onClick: () => applyVelocityCompress(), + title: "Compress velocity toward target", + className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600" + }, "Comp"), React.createElement("input", { + type: "number", value: velocityTarget, onChange: e => setVelocityTarget(parseInt(e.target.value) || 80), + className: "w-9 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-[10px] text-center" + }), React.createElement("button", { + onClick: () => applyVelocityNormalize(), + title: "Normalize (max → 127)", + className: "px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600" + }, "Norm")), 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" + onClick: () => onSaveNotes(st.id, st.trackId, st.target_id, notes), className: "px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold" }, React.createElement("i", { "data-lucide": "save", className: "w-3 h-3" }), "L\u01B0u"), React.createElement("button", { onClick: () => { const ppq = 480; @@ -9008,6 +9325,27 @@ const beatSec = 60.0 / (parseInt(bpm) || 120); })))), /* 5. OVERLAY / CONTEXT MENU */ + (arpModal && React.createElement("div", { + className: "absolute inset-0 z-40 flex items-center justify-center bg-black/60", + onMouseDown: e => e.stopPropagation(), + onClick: e => e.stopPropagation() + }, React.createElement("div", { + className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl" + }, React.createElement("div", { className: "text-sm font-bold text-cyan-300 mb-3" }, "Arpeggiate"), React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs mb-3" }, React.createElement("label", { className: "text-zinc-500" }, "Pattern"), React.createElement("select", { value: arpModal.pattern, onChange: e => setArpModal({ ...arpModal, pattern: e.target.value }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5" }, ['UP', 'DOWN', 'UP-DOWN', 'RANDOM', 'CHORD'].map(p => React.createElement("option", { key: p, value: p }, p))), React.createElement("label", { className: "text-zinc-500" }, "Rate"), React.createElement("select", { value: arpModal.rate, onChange: e => setArpModal({ ...arpModal, rate: e.target.value }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5" }, ['1/4', '1/8', '1/16', '1/32'].map(r => React.createElement("option", { key: r, value: r }, r))), React.createElement("label", { className: "text-zinc-500" }, "Octaves"), React.createElement("input", { type: "number", min: 1, max: 4, value: arpModal.octaves, onChange: e => setArpModal({ ...arpModal, octaves: parseInt(e.target.value) || 1 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14" }), React.createElement("label", { className: "text-zinc-500" }, "Gate %"), React.createElement("input", { type: "number", min: 10, max: 200, value: arpModal.gate, onChange: e => setArpModal({ ...arpModal, gate: parseInt(e.target.value) || 80 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14" })), React.createElement("div", { className: "flex gap-1 mb-3" }, React.createElement("label", { className: "flex items-center gap-1 text-[10px] text-zinc-400" }, React.createElement("input", { type: "checkbox", checked: !!arpModal.triplet, onChange: e => setArpModal({ ...arpModal, triplet: e.target.checked }) }), "Triplet"), React.createElement("label", { className: "flex items-center gap-1 text-[10px] text-zinc-400" }, React.createElement("input", { type: "checkbox", checked: !!arpModal.dotted, onChange: e => setArpModal({ ...arpModal, dotted: e.target.checked }) }), "Dotted")), React.createElement("div", { className: "flex gap-2" }, React.createElement("button", { onClick: () => applyArpeggiate(arpModal), className: "flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white" }, "Apply"), React.createElement("button", { onClick: () => setArpModal(null), className: "px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300" }, "Cancel"))))), + (strumModal && React.createElement("div", { + className: "absolute inset-0 z-40 flex items-center justify-center bg-black/60", + onMouseDown: e => e.stopPropagation(), + onClick: e => e.stopPropagation() + }, React.createElement("div", { + className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl" + }, React.createElement("div", { className: "text-sm font-bold text-cyan-300 mb-3" }, "Strum"), React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs mb-3" }, React.createElement("label", { className: "text-zinc-500" }, "Strum ms"), React.createElement("input", { type: "number", min: 0, max: 120, value: strumModal.ms, onChange: e => setStrumModal({ ...strumModal, ms: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" }), React.createElement("label", { className: "text-zinc-500" }, "Direction"), React.createElement("select", { value: strumModal.direction, onChange: e => setStrumModal({ ...strumModal, direction: e.target.value }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5" }, ['DOWN', 'UP', 'ALTERNATE'].map(d => React.createElement("option", { key: d, value: d }, d)))), React.createElement("div", { className: "flex gap-2" }, React.createElement("button", { onClick: () => applyStrum(strumModal), className: "flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white" }, "Apply"), React.createElement("button", { onClick: () => setStrumModal(null), className: "px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300" }, "Cancel"))))), + (humanizeModal && React.createElement("div", { + className: "absolute inset-0 z-40 flex items-center justify-center bg-black/60", + onMouseDown: e => e.stopPropagation(), + onClick: e => e.stopPropagation() + }, React.createElement("div", { + className: "bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl" + }, React.createElement("div", { className: "text-sm font-bold text-cyan-300 mb-3" }, "Humanize"), React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs mb-3" }, React.createElement("label", { className: "text-zinc-500" }, "Timing ±ms"), React.createElement("input", { type: "number", min: 0, max: 30, value: humanizeModal.timingMs, onChange: e => setHumanizeModal({ ...humanizeModal, timingMs: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" }), React.createElement("label", { className: "text-zinc-500" }, "Vel ±(0-127)"), React.createElement("input", { type: "number", min: 0, max: 20, value: humanizeModal.velRange, onChange: e => setHumanizeModal({ ...humanizeModal, velRange: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" }), React.createElement("label", { className: "text-zinc-500" }, "Dur ±%"), React.createElement("input", { type: "number", min: 0, max: 50, value: humanizeModal.durRange, onChange: e => setHumanizeModal({ ...humanizeModal, durRange: parseInt(e.target.value) || 0 }), className: "bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16" })), React.createElement("div", { className: "flex gap-2" }, React.createElement("button", { onClick: () => applyHumanizeModal(humanizeModal), className: "flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white" }, "Apply"), React.createElement("button", { onClick: () => setHumanizeModal(null), className: "px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300" }, "Cancel"))))), scaleMenuPos && renderScaleContextMenu() ); }; diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 1befcc5..baea0b1 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -393,7 +393,11 @@ const draggedNoteRef=React.useRef(draggedNote);draggedNoteRef.current=draggedNot // FluidSynth (scheduled qua setTimeout) TRỄ → âm bị lag 1-2 lần khi mở // piano roll tab lúc đang play (user bug 06:30). Ghost chỉ là hiển thị // tham chiếu — không cần khi âm đang chạy — tính lại khi dừng. -if(isPlaying)return[];if(!activeTracks||!st||!st.target_id)return[];var fn=window.SonicGhost&&window.SonicGhost.extractGhostLayers;return fn?fn(activeTracks,st.trackId,st.target_id,parseInt(bpm)||120):[];},[activeTracks,st.trackId,st.target_id,bpm,isPlaying]);const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const activeTargetItem=React.useMemo(function(){if(!activeTracks||!st)return null;var trk=activeTracks.find(function(t){return t.id===st.trackId;});return trk?(trk.midiItems||[]).find(function(m){return m.id===st.target_id;}):null;},[activeTracks,st.trackId,st.target_id]);var activeParentTrackName='';if(st.target_id&&activeTracks){var aptTrk=window.SonicPianoRoll?window.SonicPianoRoll.getParentTrackByItemId(st.target_id,activeTracks):null;if(!aptTrk)aptTrk=activeTracks.find(function(t){return t.id===st.trackId;});if(!aptTrk&&activeTargetItem)aptTrk=activeTracks.find(function(t){return(t.midiItems||[]).some(function(m){return m.id===st.target_id;});});if(aptTrk)activeParentTrackName=aptTrk.name||aptTrk.id;}const sessionStartBar=0;const renderBeatOffset=sessionSyncMode&&activeTargetItem?activeTargetItem.startTime/secondsPerBar*timeSigNum:0;const sessionLengthBars=React.useMemo(function(){var maxSec=0;(activeTracks||[]).forEach(function(tr){(tr.midiItems||[]).forEach(function(m){var end=(m.startTime||0)+(m.duration||4);if(end>maxSec)maxSec=end;});});return Math.ceil((maxSec||4)/secondsPerBar);},[activeTracks,secondsPerBar]);const handleSwitchMidiItem=function(itemId){if(itemId===st.target_id)return;var match=allMidiItems.find(function(m){return m.id===itemId;});if(!match)return;var scope=window.SonicPianoRoll?window.SonicPianoRoll.buildActiveScope(itemId,activeTracks):null;var trk=scope?null:(activeTracks||[]).find(function(t){return t.id===match._trackId;});var newBeatOff=match.startTime/secondsPerBar*timeSigNum;var spb=60.0/(parseInt(bpm)||120);var newTime=0;setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{trackId:scope?scope.parent_track_id:match._trackId||trk?.id,target_id:match.id,label:'Piano Roll: '+(match.name||'MIDI'),notes:match.notes||[],duration:match.duration||4,instrumentProgram:scope?scope.instrument_program:trk?trk.instrumentProgram:undefined,instrumentName:scope?scope.instrument_name:trk?trk.instrumentName:undefined,active_scope:scope||null,note_selection:[],currentTime:newTime});});});setSelectedNoteIds([]);};const rawTotalBeats=Math.max(rollBeats,noteMaxBeat+16,64);const drawWidth=rawTotalBeats*pixelsPerBeat;const[rollViewWidth,setRollViewWidth]=React.useState(800);const viewWidth=Math.max(drawWidth,rollViewWidth);const viewBeats=Math.ceil(viewWidth/pixelsPerBeat)+4;const totalBeats=Math.max(rawTotalBeats,viewBeats+32);const[notes,setNotes]=React.useState(st.notes||[]);const notesRef=React.useRef(notes);notesRef.current=notes;React.useEffect(()=>{if(!draggedNoteRef.current)setNotes(st.notes||[]);},[st.notes]);const brushVelocityRef=React.useRef(0.8);const lastNoteDurationRef=React.useRef(null);const previewPitchRef=React.useRef(null);const previewNodesRef=React.useRef(null);var stopPreviewNote=function(){var pn=previewNodesRef.current;if(pn){try{pn.osc.stop();}catch(e){}try{pn.osc.disconnect();}catch(e){}try{pn.gain.disconnect();}catch(e){}previewNodesRef.current=null;}};const[selectedNoteIds,setSelectedNoteIds]=React.useState([]);const[loopStartBeat,setLoopStartBeat]=React.useState(null);const[loopEndBeat,setLoopEndBeat]=React.useState(null);const[isLooping,setIsLooping]=React.useState(false);const rulerDragRef=React.useRef(null);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='a'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();setSelectedNoteIds(notes.map(n=>n.id));}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds]);// Undo/redo stacks +if(isPlaying)return[];if(!activeTracks||!st||!st.target_id)return[];var fn=window.SonicGhost&&window.SonicGhost.extractGhostLayers;return fn?fn(activeTracks,st.trackId,st.target_id,parseInt(bpm)||120):[];},[activeTracks,st.trackId,st.target_id,bpm,isPlaying]);const secondsPerBar=60.0/(parseInt(bpm)||120)*4;const activeTargetItem=React.useMemo(function(){if(!activeTracks||!st)return null;var trk=activeTracks.find(function(t){return t.id===st.trackId;});return trk?(trk.midiItems||[]).find(function(m){return m.id===st.target_id;}):null;},[activeTracks,st.trackId,st.target_id]);var activeParentTrackName='';if(st.target_id&&activeTracks){var aptTrk=window.SonicPianoRoll?window.SonicPianoRoll.getParentTrackByItemId(st.target_id,activeTracks):null;if(!aptTrk)aptTrk=activeTracks.find(function(t){return t.id===st.trackId;});if(!aptTrk&&activeTargetItem)aptTrk=activeTracks.find(function(t){return(t.midiItems||[]).some(function(m){return m.id===st.target_id;});});if(aptTrk)activeParentTrackName=aptTrk.name||aptTrk.id;}const sessionStartBar=0;const renderBeatOffset=sessionSyncMode&&activeTargetItem?activeTargetItem.startTime/secondsPerBar*timeSigNum:0;const sessionLengthBars=React.useMemo(function(){var maxSec=0;(activeTracks||[]).forEach(function(tr){(tr.midiItems||[]).forEach(function(m){var end=(m.startTime||0)+(m.duration||4);if(end>maxSec)maxSec=end;});});return Math.ceil((maxSec||4)/secondsPerBar);},[activeTracks,secondsPerBar]);const handleSwitchMidiItem=function(itemId){if(itemId===st.target_id)return;var match=allMidiItems.find(function(m){return m.id===itemId;});if(!match)return;var scope=window.SonicPianoRoll?window.SonicPianoRoll.buildActiveScope(itemId,activeTracks):null;var trk=scope?null:(activeTracks||[]).find(function(t){return t.id===match._trackId;});var newBeatOff=match.startTime/secondsPerBar*timeSigNum;var spb=60.0/(parseInt(bpm)||120);var newTime=0;setSubTabs(function(prev){return prev.map(function(s){if(s.id!==st.id)return s;return Object.assign({},s,{trackId:scope?scope.parent_track_id:match._trackId||trk?.id,target_id:match.id,label:'Piano Roll: '+(match.name||'MIDI'),notes:match.notes||[],duration:match.duration||4,instrumentProgram:scope?scope.instrument_program:trk?trk.instrumentProgram:undefined,instrumentName:scope?scope.instrument_name:trk?trk.instrumentName:undefined,active_scope:scope||null,note_selection:[],currentTime:newTime});});});setSelectedNoteIds([]);};const rawTotalBeats=Math.max(rollBeats,noteMaxBeat+16,64);const drawWidth=rawTotalBeats*pixelsPerBeat;const[rollViewWidth,setRollViewWidth]=React.useState(800);const viewWidth=Math.max(drawWidth,rollViewWidth);const viewBeats=Math.ceil(viewWidth/pixelsPerBeat)+4;const totalBeats=Math.max(rawTotalBeats,viewBeats+32);const[notes,setNotes]=React.useState(st.notes||[]);const notesRef=React.useRef(notes);notesRef.current=notes;React.useEffect(()=>{if(!draggedNoteRef.current)setNotes(st.notes||[]);},[st.notes]);const brushVelocityRef=React.useRef(0.8);const lastNoteDurationRef=React.useRef(null);const previewPitchRef=React.useRef(null);const previewNodesRef=React.useRef(null);var stopPreviewNote=function(){var pn=previewNodesRef.current;if(pn){try{pn.osc.stop();}catch(e){}try{pn.osc.disconnect();}catch(e){}try{pn.gain.disconnect();}catch(e){}previewNodesRef.current=null;}};const[selectedNoteIds,setSelectedNoteIds]=React.useState([]);const[loopStartBeat,setLoopStartBeat]=React.useState(null);const[loopEndBeat,setLoopEndBeat]=React.useState(null);const[isLooping,setIsLooping]=React.useState(false);const rulerDragRef=React.useRef(null);React.useEffect(()=>{const handler=e=>{if((e.ctrlKey||e.metaKey)&&e.key==='a'){const target=e.target;if(target&&(target.tagName==='INPUT'||target.tagName==='TEXTAREA'||target.isContentEditable))return;e.preventDefault();setSelectedNoteIds(notes.map(n=>n.id));return;}// Spec 20:12 shortcuts: Alt+A Arp, Alt+S Strum, Alt+R Humanize, +// Shift+C chord stamp, Shift+S lock-scale toggle +const inField=e.target&&(e.target.tagName==='INPUT'||e.target.tagName==='TEXTAREA'||e.target.isContentEditable);// Ctrl+C/X/V — copy/paste/cut NOTES (thay 2 nút Copy/Paste đã bỏ — +// user 08:40) +if((e.ctrlKey||e.metaKey)&&!e.altKey&&!inField){if(e.key==='c'){e.preventDefault();if(notes&¬es.length){if(onCopyNotes)onCopyNotes(notes);showToast('Đã copy '+notes.length+' nốt vào clipboard.','success');}else{showToast('Không có nốt để copy.','warning');}return;}if(e.key==='x'){e.preventDefault();if(notes&¬es.length){if(onCopyNotes)onCopyNotes(notes);pushToUndo(notes);setNotes([]);showToast('Đã cắt '+notes.length+' nốt.','success');}else{showToast('Không có nốt để cắt.','warning');}return;}if(e.key==='v'){e.preventDefault();if(!clipboardNotes||!clipboardNotes.length){showToast('Clipboard trống — bấm Ctrl+C trước.','warning');return;}pushToUndo(notes);setNotes(prev=>[...(prev||[]),...clipboardNotes.map(function(n){return{...n,id:'note_cp_'+Date.now()+'_'+Math.floor(Math.random()*100000)};})]);showToast('Đã paste '+clipboardNotes.length+' nốt.','success');return;}}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='a'){e.preventDefault();setArpModal({pattern:'UP',rate:'1/16',octaves:2,gate:80,triplet:false,dotted:false});return;}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='s'){e.preventDefault();setStrumModal({ms:30,direction:'DOWN'});return;}if(e.altKey&&!e.ctrlKey&&!inField&&e.key==='r'){e.preventDefault();setHumanizeModal({timingMs:12,velRange:15,durRange:10});return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&!inField&&e.key==='c'){e.preventDefault();setChordStampMode(m=>!m);return;}if(e.shiftKey&&!e.ctrlKey&&!e.altKey&&!inField&&e.key==='s'){e.preventDefault();setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s));return;}};window.addEventListener('keydown',handler);return()=>window.removeEventListener('keydown',handler);},[notes,setSelectedNoteIds,clipboardNotes]);// Undo/redo stacks const undoStackRef=React.useRef([]);const redoStackRef=React.useRef([]);const notesBeforeDragRef=React.useRef(null);const pushToUndo=React.useCallback(prevNotes=>{undoStackRef.current.push(JSON.parse(JSON.stringify(prevNotes)));redoStackRef.current=[];if(undoStackRef.current.length>50)undoStackRef.current.shift();},[]);// ── Humanize: ngẫu nhiên hóa velocity + timing theo cường độ ── const[humanizeStrength,setHumanizeStrength]=React.useState(0.10);// 0.05 nhẹ / 0.10 vừa / 0.18 mạnh const applyHumanize=React.useCallback(()=>{if(!notes||!notes.length){showToast('Không có nốt nào để humanize.','warning');return;}const velAmt=humanizeStrength;const timeAmt=humanizeStrength*0.15;// ±0.015 beat @ vừa (~12ms @120bpm) @@ -408,8 +412,10 @@ let bestIdx=0,bestDist=99;for(let i=0;i<7;i++){let d=Math.abs(pc-srcTones[i]);if React.useEffect(()=>{const handleWheelRaw=e=>{if(e.ctrlKey){e.preventDefault();const zoomFactor=e.deltaY<0?1.15:0.85;setRollZoom(prev=>Math.max(15,Math.min(250,prev*zoomFactor)));}};const container=gridScrollRef.current;if(container){container.addEventListener('wheel',handleWheelRaw,{passive:false});}return()=>{if(container){container.removeEventListener('wheel',handleWheelRaw);}};},[]);// Alt + Scroll event listener: fast‑forward playhead + play notes React.useEffect(()=>{const handleCanvasWheel=e=>{const canvas=canvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const mx=e.clientX-rect.left;const my=e.clientY-rect.top;const pitch=127-Math.floor(my/NoteHeight);if(e.shiftKey){e.preventDefault();// Shift+scroll on note → change velocity of single note or all selected const scrollBeat=mx/pixelsPerBeat-renderBeatOffset;const clickedNote=notes.find(n=>pitch===n.pitch&&scrollBeat>=n.start_beat&&scrollBeat0){setNotes(prev=>prev.map(n=>selectedNoteIds.includes(n.id)?{...n,velocity:Math.max(0.05,Math.min(1,(n.velocity||0.8)+delta))}:n));}else{setNotes(prev=>prev.map(n=>n.id===clickedNote.id?{...n,velocity:Math.max(0.05,Math.min(1,(n.velocity||0.8)+delta))}:n));}}else{// Shift+scroll on empty space → horizontal scroll -const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat=n.start_beat);var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtx=resolveTrackInstrumentCtx(pvTrk,activeTracks);ensureSonicInstrument(pvCtx);playing.forEach(n=>{window.SonicSF.playNote(n.pitch,(n.velocity||0.8)*127,200,ctx.currentTime,pvCtx.program,null,pvCtx.ch,pvCtx.synthEngine);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Draw background rows -for(let pitch=0;pitch<128;pitch++){const y=(127-pitch)*NoteHeight;const isBlack=[1,3,6,8,10].includes(pitch%12);ctx.fillStyle=isBlack?'#1a1a1e':'#25252a';ctx.fillRect(0,y,viewWidth,NoteHeight);ctx.strokeStyle='#2d2d35';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,y+NoteHeight);ctx.lineTo(viewWidth,y+NoteHeight);ctx.stroke();}// Draw snap lines +const container=gridScrollRef.current;if(container)container.scrollLeft+=e.deltaY;}return;}if(e.altKey){e.preventDefault();const scrollDelta=e.deltaY;const beatSec=60.0/(parseInt(bpm)||120);const step=scrollDelta<0?-0.25:0.25;const currentBeat=(st.currentTime||0)/beatSec;const maxBeats=totalBeats;const newBeat=Math.max(0,Math.min(maxBeats,currentBeat+step));const newTime=newBeat*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:newTime}:s));if(window.SonicSF){const ctx=getAudioContext();const playing=notes.filter(n=>currentBeat=n.start_beat);var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtx=resolveTrackInstrumentCtx(pvTrk,activeTracks);ensureSonicInstrument(pvCtx);playing.forEach(n=>{window.SonicSF.playNote(n.pitch,(n.velocity||0.8)*127,200,ctx.currentTime,pvCtx.program,null,pvCtx.ch,pvCtx.synthEngine);});}}};const canvas=canvasRef.current;if(canvas){canvas.addEventListener('wheel',handleCanvasWheel,{passive:false});}return()=>{if(canvas){canvas.removeEventListener('wheel',handleCanvasWheel);}};},[notes,st.currentTime,pixelsPerBeat,st.id,totalBeats,bpm]);React.useLayoutEffect(()=>{const canvas=canvasRef.current;if(!canvas)return;const ctx=canvas.getContext('2d');const dpr=window.devicePixelRatio||1;const h=128*NoteHeight;canvas.width=viewWidth*dpr;canvas.height=h*dpr;ctx.scale(dpr,dpr);// Scale highlight: rows thuộc scale (root+scale) sáng vàng; ngoài scale +// dim hơn (spec 20:12 — Scale Highlight & Lock) +const scHighlight=scaleWithRoot();// Draw background rows +for(let pitch=0;pitch<128;pitch++){const y=(127-pitch)*NoteHeight;const isBlack=[1,3,6,8,10].includes(pitch%12);const inScale=scHighlight?scHighlight.includes(pitch%12):null;ctx.fillStyle=isBlack?inScale===false?'#141419':'#1f1f25':inScale===false?'#1b1b20':inScale===true?'#2c2a1e':'#25252a';ctx.fillRect(0,y,viewWidth,NoteHeight);ctx.strokeStyle='#2d2d35';ctx.lineWidth=0.5;ctx.beginPath();ctx.moveTo(0,y+NoteHeight);ctx.lineTo(viewWidth,y+NoteHeight);ctx.stroke();}// Draw snap lines let snapBeats=0.25;if(snapValue==='1')snapBeats=1.0;else if(snapValue==='1/2')snapBeats=0.5;else if(snapValue==='1/4')snapBeats=0.25;else if(snapValue==='1/8')snapBeats=0.125;else if(snapValue==='1/16')snapBeats=0.0625;else if(snapValue==='4')snapBeats=4.0;else if(snapValue==='1/32')snapBeats=0.03125;for(let beat=0;beat<=viewBeats;beat+=snapBeats){const x=beat*pixelsPerBeat;if(x>viewWidth)break;const isBar=beat%timeSigNum===0;ctx.strokeStyle=isBar?'#444450':'#2d2d35';ctx.lineWidth=isBar?1.2:0.6;ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,h);ctx.stroke();}// Determine which MIDI item is focused (clicked/selected note → its item; playing → item under playhead) var focusedItemId=focusItemId||st.target_id;if(selectedNoteIds&&selectedNoteIds.length>0){// Most recently selected note wins: main note → opened item; dim same-track note → its item focusedItemId=st.target_id;for(var s2=selectedNoteIds.length-1;s2>=0;s2--){var sid=selectedNoteIds[s2];if(notes.some(function(n){return n.id===sid;})){focusedItemId=st.target_id;break;}var foundGhost2=false;for(var sg2=0;sg2=fStart&&curSecselectedNoteIds.includes(n.id)).m if(hoveredResizeIdx!==-1&&e.altKey){pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const allSelected=[...new Set(selectedNoteIds.length>0?selectedNoteIds:[notes[hoveredResizeIdx].id])];const selectedNotes=notes.filter(n=>allSelected.includes(n.id));const firstStart=Math.min(...selectedNotes.map(n=>n.start_beat));const draggedNote=notes[hoveredResizeIdx];setDraggedNote({mode:'scale',idx:hoveredResizeIdx,originalEnd:draggedNote.start_beat+draggedNote.duration_beats,firstStart:firstStart,selectedNoteIds:allSelected});return;}// Hovered resize edge (normal resize) if(hoveredResizeIdx!==-1){pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'resize',idx:hoveredResizeIdx,originalStart:notes[hoveredResizeIdx].start_beat});return;}if(clickedNoteIdx!==-1){// Click on existing note: drag-move → focus its MIDI item setFocusItemId(st.target_id);const clickedNote=notes[clickedNoteIdx];let nextSelectedIds;if(!selectedNoteIds.includes(clickedNote.id)){nextSelectedIds=[clickedNote.id];setSelectedNoteIds(nextSelectedIds);}else{nextSelectedIds=selectedNoteIds;}pushToUndo(notes);notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));const selectedNotesOffset=notes.filter(n=>nextSelectedIds.includes(n.id)).map(n=>({id:n.id,originalStartBeat:n.start_beat,originalPitch:n.pitch}));setDraggedNote({mode:'move',idx:clickedNoteIdx,startOffsetBeat:beat-clickedNote.start_beat,startOffsetPitch:pitch,selectedNotesOffset:selectedNotesOffset,clickedOriginalStartBeat:clickedNote.start_beat});}else{// Click on a same-track ghost note → focus that MIDI item (no drawing) -if(!e.ctrlKey&&!e.shiftKey&&!e.altKey){var ghostHit=null;for(var gi=0;gi=gnote.relative_start_beat&&beat=gnote.relative_start_beat&&beat[...prev,newNote]);setSelectedNoteIds([noteId]);setDraggedNote({mode:'draw',idx:-1,startOffsetBeat:start,startOffsetPitch:snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch,drawNoteId:noteId,drawDuration:initialDur,initialBeat:start,initialPitch:snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch,noteStartBeats:[start],lastDrawnPitch:snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch});// Play the note with SoundFont - stop previous preview first if(previewNodesRef.current){try{previewNodesRef.current.osc.stop();}catch(e){}try{previewNodesRef.current.osc.disconnect();}catch(e){}try{previewNodesRef.current.gain.disconnect();}catch(e){}previewNodesRef.current=null;}if(window.SonicSF&&window.SonicSF.playNote){const ctx=getAudioContext();var dwTrk=activeTracks.find(function(t){return t.id===st.trackId;});var dwCh=dwTrk?assignTrackMidiChannel(dwTrk,activeTracks):0;var dwPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;var dwDurMs=Math.max(100,Math.round(initialDur*(60/bpm)*1000));// playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ // là oscillator beep (sai âm với percussion/soundfont — user: note vẽ @@ -445,7 +452,23 @@ return n.start_beat>=minBeat&¬eEnd<=maxBeat;}}).map(n=>n.id);setSelectedNoteI const rc=rightClickDragRef.current;if(rc.active&&(Math.abs(e.clientX-rc.startX)>5||Math.abs(e.clientY-rc.startY)>5)){rc.active=false;swallowContextMenuRef.current=true;notesBeforeDragRef.current=JSON.parse(JSON.stringify(notes));setDraggedNote({mode:'erase_sweep',visitedPitches:[]});return;}if(!draggedNote){let foundIdx=-1;for(let i=0;i=n.start_beat){foundIdx=i;break;}}}if(foundIdx!==-1){canvas.style.cursor='ew-resize';setHoveredResizeIdx(foundIdx);}else{canvas.style.cursor=activeRollTool==='eraser'?'pointer':'crosshair';setHoveredResizeIdx(-1);}return;}if(draggedNote.mode==='draw'){const snappedPitch=snapToScaleRef.current?snapPitchToScale(pitch,selectedScaleRef.current):pitch;const lastPitch=draggedNote.lastDrawnPitch!==undefined?draggedNote.lastDrawnPitch:draggedNote.startOffsetPitch;const pitchChanged=snappedPitch!==lastPitch;const noteBeats=draggedNote.noteStartBeats||[];const defaultDur=getSnapDuration(snapValue);function playDrawPreview(p,durMs){stopPreviewNote();if(window.SonicSF&&window.SonicSF.playNote){var pvCtx=getAudioContext();var pvTrk=activeTracks.find(function(t){return t.id===st.trackId;});var pvCtxInst=resolveTrackInstrumentCtx(pvTrk,activeTracks);var pvVel=Math.round(brushVelocityRef.current*127);// playNote (FluidSynth — nhạc cụ THẬT). _playNoteFallback = oscillator // beep sai âm (percussion/soundfont). window.SonicSF.playNote(p,pvVel,durMs,pvCtx.currentTime,pvCtxInst.program,null,pvCtxInst.ch,pvCtxInst.synthEngine);previewPitchRef.current=p;}}if(pitchChanged){const brushIds=draggedNote.brushIds||[];if(brushIds.length>0&¬eBeats.length>0){const prevNoteId=brushIds[brushIds.length-1];const prevNoteBeat=noteBeats[noteBeats.length-1];const prevDur=Math.max(0.125,beat-prevNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==prevNoteId)return n;return{...n,duration_beats:prevDur};}));}const newNote={id:'note_'+Date.now()+Math.random().toString(36).substr(2,8)+'_'+(noteBeats.length+1),pitch:snappedPitch,start_beat:beat,duration_beats:defaultDur,velocity:brushVelocityRef.current,pan:0.0};setNotes(prev=>[...prev,newNote]);setSelectedNoteIds(prev=>[...prev,newNote.id]);draggedNote.brushIds=[...brushIds,newNote.id];draggedNote.lastDrawnPitch=snappedPitch;draggedNote.noteStartBeats=[...noteBeats,beat];playDrawPreview(snappedPitch,Math.max(100,Math.round(defaultDur*(60/bpm)*1000)));}else{const brushIds=draggedNote.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:draggedNote.drawNoteId;const lastNoteBeat=noteBeats.length>0?noteBeats[noteBeats.length-1]:draggedNote.startOffsetBeat;if(lastBrushId){const extDur=Math.max(0.125,beat-lastNoteBeat);setNotes(prev=>prev.map(n=>{if(n.id!==lastBrushId)return n;return{...n,duration_beats:extDur};}));playDrawPreview(snappedPitch,Math.max(100,Math.round(extDur*(60/bpm)*1000)));}}const container=gridScrollRef.current;if(container){const cr=container.getBoundingClientRect();const visTop=container.scrollTop;const visBot=visTop+container.clientHeight;const pitchPixel=(127-snappedPitch)*NoteHeight;const safeMargin=NoteHeight*2;if(pitchPixel{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.max(0,gridScrollRef.current.scrollTop-Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else if(pitchPixel+NoteHeight>visBot-safeMargin){const target=Math.min(container.scrollHeight-container.clientHeight,pitchPixel-container.clientHeight+safeMargin+NoteHeight);if(container.scrollTop!==target)container.scrollTop=target;if(!brushAutoScrollRef.current||brushAutoScrollRef.current.direction!=='down'){if(brushAutoScrollRef.current)clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current={direction:'down',id:setInterval(()=>{if(gridScrollRef.current)gridScrollRef.current.scrollTop=Math.min(gridScrollRef.current.scrollHeight-gridScrollRef.current.clientHeight,gridScrollRef.current.scrollTop+Math.max(1,Math.floor(NoteHeight*0.5)));},16)};}}else{if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}}}return;}if(draggedNote.mode==='erase_sweep'){const erased=draggedNote.erasedIds||[];const target=notes.find(n=>n.pitch===pitch&&beat>=n.start_beat&&beatprev.filter(n=>n.id!==target.id));setSelectedNoteIds(prev=>prev.filter(id=>id!==target.id));}return;}if(draggedNote.mode==='scale'){const newEnd=getSnapBeat(Math.max(draggedNote.firstStart+0.125,beat),snapValue);const range=draggedNote.originalEnd-draggedNote.firstStart;if(range<=0)return;const scaleFactor=Math.max(0.01,(newEnd-draggedNote.firstStart)/range);const ids=draggedNote.selectedNoteIds||[];const firstStart=draggedNote.firstStart;const notesBefore=notesBeforeDragRef.current;if(!notesBefore)return;setNotes(prev=>prev.map(n=>{if(!ids.includes(n.id))return n;const orig=notesBefore?notesBefore.find(o=>o.id===n.id):null;const origStart=orig?orig.start_beat:n.start_beat;const origDur=orig?orig.duration_beats:n.duration_beats;const relStart=origStart-firstStart;const relEnd=relStart+origDur;return{...n,start_beat:firstStart+relStart*scaleFactor,duration_beats:Math.max(0.125,relEnd*scaleFactor-relStart*scaleFactor)};}));return;}if(draggedNote.mode==='resize'){const newDuration=getSnapBeat(Math.max(0.125,beat-draggedNote.originalStart),snapValue);setNotes(prev=>prev.map((n,idx)=>{if(idx!==draggedNote.idx)return n;return{...n,duration_beats:newDuration};}));}else if(draggedNote.mode==='move'){const refOrigStart=draggedNote.clickedOriginalStartBeat;if(refOrigStart===undefined)return;const deltaBeat=getSnapBeat(beat-draggedNote.startOffsetBeat,snapValue)-refOrigStart;// Clamp so no note goes past beat 0 -const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+clampedDeltaBeat),snapValue),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{if(draggedNote&&draggedNote.mode==='draw'){const dn=draggedNote;const brushIds=dn.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:dn.drawNoteId;if(lastBrushId){const lastNote=notes.find(n=>n.id===lastBrushId);if(lastNote)lastNoteDurationRef.current=lastNote.duration_beats;}}setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}stopPreviewNote();previewPitchRef.current=null;};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const findCCNoteIndex=(b,mouseY,ccH)=>{const snapped=getSnapBeat(b,snapValue);const hits=[];notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+n.duration_beats){const nv=ccMode==='pan'?(n.pan||0)*0.5+0.5:n.velocity!==undefined?n.velocity:0.8;const stemTop=ccH-(nv*(ccH-20)+10);hits.push({idx,dist:Math.abs(stemTop-mouseY)});}});if(hits.length>0){hits.sort((a,b)=>a.dist-b.dist);return hits[0].idx;}let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-snapped);if(d{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCCNoteIndex(beat,y,h);const val=Math.max(0,Math.min(1,(h-y)/h));if(e.ctrlKey){if(selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1&&selectedNoteIds.includes(notes[cursorNoteIdx]?notes[cursorNoteIdx].id:-1)){const currentNote=notes[cursorNoteIdx];const currentVal=ccMode==='pan'?(currentNote.pan||0)/2.0+0.5:currentNote.velocity!==undefined?currentNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[cursorNoteIdx]};}else{ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[]};}}else{ccDragRef.current={active:true,lastBeat:beat,lastPainted:noteIdx!==-1?[noteIdx]:[]};}return;}if(noteIdx!==-1){const currentVal=ccMode==='pan'?(notes[noteIdx].pan||0)/2.0+0.5:notes[noteIdx].velocity!==undefined?notes[noteIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==noteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}}};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const val=Math.max(0,Math.min(1,(h-y)/h));const drag=ccDragRef.current;const painted=drag.lastPainted||[];if(drag.selectedMode&&selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1){const cursorNote=notes[cursorNoteIdx];if(cursorNote&&selectedNoteIds.includes(cursorNote.id)&&!painted.includes(cursorNoteIdx)){const currentVal=ccMode==='pan'?(cursorNote.pan||0)/2.0+0.5:cursorNote.velocity!==undefined?cursorNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,cursorNoteIdx];}}return;}const candidateIdx=findCCNoteIndex(beat,y,h);if(candidateIdx!==-1&&!painted.includes(candidateIdx)){const currentVal=ccMode==='pan'?(notes[candidateIdx].pan||0)/2.0+0.5:notes[candidateIdx].velocity!==undefined?notes[candidateIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==candidateIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,candidateIdx];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(st.trackId,100);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(st.trackId,100);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;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[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);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 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 to Scale"),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s)),className:`w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale!==undefined?st.snapToScale:true)?'bg-yellow-600':'bg-zinc-700'}`,style:{padding:0}},React.createElement("div",{className:`w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale!==undefined?st.snapToScale:true)?'translate-x-3.5':'translate-x-0.5'}`}))),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),React.createElement("select",{value:snapValue,onChange:e=>{onSnapChange(e.target.value);setRenderTick(t=>t+1);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>React.createElement("option",{key:v,value:v},v)))),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},React.createElement("option",{value:""},"Input"),React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),React.createElement("button",{onClick:()=>onInstrumentSelect&&onInstrumentSelect(st.trackId),title:st.instrumentName||"Synth",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName?'bg-violet-900 text-violet-300 border-violet-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},React.createElement("i",{"data-lucide":"music",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[9px]"},activeParentTrackName?'('+activeParentTrackName+') '+(st.instrumentName||'Synth'):st.instrumentName||'Synth')),React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},React.createElement("span",{className:"text-zinc-500"},"AI:"),React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"-"),React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"bar")),React.createElement("select",{value:ccMode,onChange:e=>setCcMode(e.target.value),className:"bg-zinc-800 text-zinc-200 border border-zinc-700 rounded px-1.5 py-1 text-xs capitalize cursor-pointer"},React.createElement("option",{value:"velocity"},"Velocity"),React.createElement("option",{value:"sustain"},"Sustain"),React.createElement("option",{value:"modulation"},"Modulation"),React.createElement("option",{value:"pitch_bend"},"Pitch Bend"),React.createElement("option",{value:"pan"},"Pan"))),React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='velocity'?'Vel':ccMode==='sustain'?'Sus':ccMode==='modulation'?'Mod':ccMode==='pitch_bend'?'Bend':ccMode==='pan'?'Pan':'CC'),React.createElement("button",{onClick:function(){setSessionSyncMode(function(p){return!p;});},className:function(){var base='px-2 py-1 rounded text-xs ';return sessionSyncMode?base+'bg-cyan-900/60 text-cyan-300 border border-cyan-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:sessionSyncMode?"Session-synced mode (ghost visible)":"Isolated mode (bar 0, no ghost)"},sessionSyncMode?"\uD83C\uDF10 Session":"\uD83D\uDCCB Isolated"),React.createElement("button",{onClick:function(){setShowGhostNotes(function(p){return!p;});},disabled:!sessionSyncMode,className:function(){if(!sessionSyncMode)return'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';var base='px-2 py-1 rounded text-xs ';return showGhostNotes?base+'bg-purple-900/60 text-purple-300 border border-purple-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:"Toggle ghost notes visibility"},"👻 MIDI ghost notes"),React.createElement("button",{onClick:onClose,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition ml-auto"},React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"),React.createElement("div",{style:{flexBasis:"100%",height:0}}),React.createElement("button",{onClick:applyHumanize,className:"px-2 py-1 rounded text-xs bg-amber-900/40 text-amber-300 border border-amber-700/60 hover:bg-amber-800/50 transition",title:"Humanize: randomize velocity + timing"},"\uD83C\uDF9A Humanize"),React.createElement("select",{key:"humstr",value:humanizeStrength,onChange:function(e){setHumanizeStrength(parseFloat(e.target.value));},className:"px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",title:"Cường độ humanize"},React.createElement("option",{key:"l",value:0.05},"Nh\u1EB9"),React.createElement("option",{key:"m",value:0.10},"V\u1EEBa"),React.createElement("option",{key:"s",value:0.18},"M\u1EA1nh")),React.createElement("button",{onClick:function(){if(notes&¬es.length){if(onCopyNotes)onCopyNotes(notes);showToast('Đã copy '+notes.length+' nốt vào clipboard — mở tab khác + Paste.','success');}else{showToast('Không có nốt để copy.','warning');}},className:"px-2 py-1 rounded text-xs bg-cyan-900/40 text-cyan-300 border border-cyan-700/60 hover:bg-cyan-800/50 transition",title:"Copy toàn bộ notes vào clipboard (paste sang PIANO ROLL TAB khác)"},"\uD83D\uDCCB Copy"),React.createElement("button",{onClick:function(){if(!clipboardNotes||!clipboardNotes.length){showToast('Clipboard trống — bấm Copy trước.','warning');return;}pushToUndo(notes);setNotes(prev=>[...(prev||[]),...clipboardNotes.map(function(n){return{...n,id:'note_cp_'+Date.now()+'_'+Math.floor(Math.random()*100000)};})]);showToast('Đã paste '+clipboardNotes.length+' nốt từ clipboard.','success');},className:"px-2 py-1 rounded text-xs bg-teal-900/40 text-teal-300 border border-teal-700/60 hover:bg-teal-800/50 transition",title:"Paste notes từ clipboard vào tab này (append cuối)"},"\uD83D\uDCE5 Paste"),React.createElement("div",{key:"transpose",className:"flex items-center gap-1"},React.createElement("input",{key:"in",type:"number",step:1,min:-24,max:24,value:transposeSemis,onChange:function(e){setTransposeSemis(e.target.value);},className:"w-12 px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-center text-zinc-200",title:"Semitone offset (vd 2 = cao hơn 1 tone)"}),React.createElement("button",{key:"btn",onClick:function(){applyTranspose(transposeSemis);},className:"px-2 py-1 rounded text-xs bg-sky-900/40 text-sky-300 border border-sky-700/60 hover:bg-sky-800/50 transition",title:"Transpose all notes by the semitone offset"},"Transpose")),React.createElement("div",{key:"keyshift",className:"flex items-center gap-1"},React.createElement("select",{key:"root",value:keyTargetRoot,onChange:function(e){setKeyTargetRoot(e.target.value);},className:"px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",title:"Giọng đích (root)"},["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"].map(function(r){return React.createElement("option",{key:r,value:r},r);})),React.createElement("select",{key:"scale",value:keyTargetScale,onChange:function(e){setKeyTargetScale(e.target.value);},className:"px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",title:"Thể scale đích"},React.createElement("option",{key:"maj",value:"major"},"major"),React.createElement("option",{key:"min",value:"minor"},"minor")),React.createElement("button",{key:"btn",onClick:applyTransposeToKey,className:"px-2 py-1 rounded text-xs bg-violet-900/40 text-violet-300 border border-violet-700/60 hover:bg-violet-800/50 transition",title:"Chuyển giọng: map degree hiện tại sang giọng đích (auto-detect key nguồn)"},"🎵 Chuyển giọng")),React.createElement("div",{className:"flex items-center gap-1 ml-auto"},React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"L\u01B0u"),React.createElement("button",{onClick:()=>{const ppq=480;const bpmNum=parseInt(bpm)||120;const ticksPerBeat=ppq;const events=[];(notes||[]).forEach(n=>{const startTick=Math.round((n.start_beat||0)*ticksPerBeat);const durTick=Math.round((n.duration_beats||1)*ticksPerBeat);const pitch=n.pitch||60;const vel=Math.round((n.velocity||0.8)*127);events.push({tick:startTick,type:'note_on',pitch,velocity:vel});events.push({tick:startTick+durTick,type:'note_off',pitch,velocity:0});});events.sort((a,b)=>a.tick-b.tick||(a.type==='note_off'?-1:1));const writeVLQ=(bytes,v)=>{let val=Math.max(0,v);const buf=[];buf.push(val&0x7F);while(val>0x7F){val>>=7;buf.push(0x80|val&0x7F);}for(let i=buf.length-1;i>=0;i--)bytes.push(buf[i]);};const trackBytes=[];let lastTick=0;events.forEach(ev=>{const delta=Math.max(0,ev.tick-lastTick);writeVLQ(trackBytes,delta);trackBytes.push(ev.type==='note_on'?0x90:0x80,ev.pitch,ev.velocity);lastTick=ev.tick;});writeVLQ(trackBytes,0);trackBytes.push(0xFF,0x2F,0x00);const trackData=[0x4D,0x54,0x72,0x6B];const len=trackBytes.length;trackData.push(len>>24&0xFF,len>>16&0xFF,len>>8&0xFF,len&0xFF);trackData.push(...trackBytes);const header=[0x4D,0x54,0x68,0x64,0x00,0x00,0x00,0x06,0x00,0x01,0x00,0x01,ppq>>8&0xFF,ppq&0xFF];const all=header.concat(trackData);const blob=new Blob([new Uint8Array(all)],{type:'audio/midi'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=(st.label||'midi')+'.mid';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);showToast('Đã xuất file MIDI!','success');},className:"px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"file-down",className:"w-3 h-3"}),"Export MIDI"))),/* 2. BAR RULER */React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},React.createElement("div",{className:"w-[120px] bg-[#1e1e22] border-r border-zinc-800 shrink-0 flex items-end"},React.createElement("span",{className:"text-[8px] text-zinc-600 font-mono px-1.5 pb-0.5 uppercase tracking-wider"},"Tracks")),React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),React.createElement("div",{ref:rulerScrollRef,className:"flex-1 overflow-hidden",onMouseDown:e=>{const rect=e.currentTarget.getBoundingClientRect();const x=e.clientX-rect.left+e.currentTarget.scrollLeft;const clickBeat=x/pixelsPerBeat;const clickTime=clickBeat*beatSec;const clickInRange=loopStartBeat!==null&&loopEndBeat!==null&&clickBeat>=loopStartBeat&&clickBeat<=loopEndBeat;if(e.ctrlKey||e.metaKey){setLoopStartBeat(null);setLoopEndBeat(null);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:null,selectionEnd:null}:s));return;}if(e.shiftKey){const beatSnap=getSnapBeat(clickBeat,snapValue);if(loopStartBeat===null){setLoopStartBeat(Math.max(0,beatSnap-4));setLoopEndBeat(Math.max(4,beatSnap));}else{setLoopEndBeat(Math.max(loopStartBeat+4,beatSnap));}return;}if(clickTime>=0){if(onSeekPlayhead){onSeekPlayhead(clickTime);}else{setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}const snappedStartBeat=getSnapBeat(clickBeat,snapValue);rulerDragRef.current={startX:e.clientX,startBeat:snappedStartBeat,scrollLeft:e.currentTarget.scrollLeft};const onMove=ev=>{const r=rulerScrollRef.current;if(!r||!rulerDragRef.current)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+rulerDragRef.current.scrollLeft;const rawBeat=Math.max(0,bx/pixelsPerBeat);const beat=getSnapBeat(rawBeat,snapValue);if(Math.abs(ev.clientX-rulerDragRef.current.startX)>5){if(clickInRange){const rangeWidth=loopEndBeat-loopStartBeat;const offset=rulerDragRef.current.startBeat-loopStartBeat;const centerBeat=beat-offset;const halfRange=rangeWidth/2;const newStart=Math.max(0,centerBeat-halfRange);setLoopStartBeat(newStart);setLoopEndBeat(newStart+rangeWidth);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:newStart*beatSec,selectionEnd:(newStart+rangeWidth)*beatSec}:s));}else{const sBeat=Math.max(0,Math.min(rulerDragRef.current.startBeat,beat));const eBeat=Math.max(sBeat+1,Math.max(rulerDragRef.current.startBeat,beat));setLoopStartBeat(sBeat);setLoopEndBeat(eBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:sBeat*beatSec,selectionEnd:eBeat*beatSec}:s));}}};const onUp=()=>{rulerDragRef.current=null;document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels(),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/15 border-l border-r border-emerald-400"},React.createElement("div",{style:{position:'absolute',left:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const startBeat=loopStartBeat;const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(0,Math.min(loopEndBeat-1,getSnapBeat(bx/pixelsPerBeat,snapValue)));setLoopStartBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}),React.createElement("div",{style:{position:'absolute',right:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(loopStartBeat+1,getSnapBeat(bx/pixelsPerBeat,snapValue));setLoopEndBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionEnd:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}))))),/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/* Track column */React.createElement("div",{className:"w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",style:{height:KeybedPixelHeight+'px'}},allMidiItems.length>0?function(){var seenTracks={};var els=[];allMidiItems.forEach(function(m){if(seenTracks[m._trackId])return;seenTracks[m._trackId]=true;var track=(activeTracks||[]).find(function(t){return t.id===m._trackId;});var isActive=m._trackId===st.trackId&&m.id===st.target_id;var isPlayOn=activePlayTrackIds&&activePlayTrackIds.indexOf(m._trackId)!==-1;els.push(React.createElement("button",{key:m._trackId,onClick:function(){var prevList=activePlayTrackIds||[];var nextList=prevList.indexOf(m._trackId)!==-1?prevList.filter(function(id){return id!==m._trackId;}):prevList.concat([m._trackId]);setActivePlayTrackIds(nextList);if(onRealtimePlay)onRealtimePlay(nextList);},className:"flex items-center justify-center h-[20px] border border-zinc-600 rounded-md cursor-pointer outline-none mx-1 my-[2px] "+(isActive?'bg-yellow-600 text-black font-bold':isPlayOn?'bg-red-700 text-white font-semibold':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},React.createElement("span",{className:"text-[14px] font-sans truncate px-1",title:track?track.name:m._trackName},track?track.name:m._trackName)));});return els;}():null),React.createElement("div",{className:"w-[60px] shrink-0 flex flex-col border-r border-zinc-900 overflow-y-auto",ref:keybedRef,onScroll:handleKeybedScroll,style:{scrollbarWidth:'none',msOverflowStyle:'none'}},renderKeybed()),React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */showCC&&React.createElement("div",{style:{height:`${ccHeight}px`},className:"bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"},React.createElement("div",{onMouseDown:e=>{e.preventDefault();const startY=e.clientY;const startH=ccHeight;const onMove=ev=>{setCcHeight(Math.max(40,startH+startY-ev.clientY));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);},className:"absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"}),React.createElement("div",{className:"w-[120px] shrink-0"}),React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"},ccMode.toUpperCase()),React.createElement("div",{ref:ccWrapperRef,className:"flex-1 overflow-x-hidden min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative"},React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,onMouseMove:handleCCMouseMove,onMouseUp:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},onMouseLeave:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},className:"absolute inset-0"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 5. OVERLAY / CONTEXT MENU */scaleMenuPos&&renderScaleContextMenu());};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.type==='MIDI'||t.type==='soundfont'||t.type==='vst3')trackType="MIDI";else if(t.type==='SECTION')trackType="SECTION";else if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];// Serialize EVERY item type present on the track (a track can hold audio +const minOrigStart=Math.min(...draggedNote.selectedNotesOffset.map(o=>o.originalStartBeat));const clampedDeltaBeat=minOrigStart+deltaBeat<0?-minOrigStart:deltaBeat;const deltaPitch=Math.round(pitch-draggedNote.startOffsetPitch);setNotes(prev=>prev.map(n=>{const offset=draggedNote.selectedNotesOffset.find(o=>o.id===n.id);if(!offset)return n;return{...n,start_beat:getSnapBeat(Math.max(0,offset.originalStartBeat+clampedDeltaBeat),snapValue),pitch:Math.max(0,Math.min(127,offset.originalPitch+deltaPitch))};}));}};const handleGridMouseUp=()=>{if(draggedNote&&draggedNote.mode==='draw'){const dn=draggedNote;const brushIds=dn.brushIds||[];const lastBrushId=brushIds.length>0?brushIds[brushIds.length-1]:dn.drawNoteId;if(lastBrushId){const lastNote=notes.find(n=>n.id===lastBrushId);if(lastNote)lastNoteDurationRef.current=lastNote.duration_beats;}}setDraggedNote(null);setSelectionMarquee(null);rightClickDragRef.current={active:false,startX:0,startY:0};if(brushAutoScrollRef.current){clearInterval(brushAutoScrollRef.current.id);brushAutoScrollRef.current=null;}stopPreviewNote();previewPitchRef.current=null;};const handleContextMenu=e=>{e.preventDefault();if(swallowContextMenuRef.current){swallowContextMenuRef.current=false;return;}const pos={x:e.clientX,y:e.clientY,parentKey:null};scaleMenuOriginRef.current={x:pos.x,y:pos.y};setScaleMenuPos(pos);};const ccDragRef=React.useRef(null);const brushAutoScrollRef=React.useRef(null);const rightClickDragRef=React.useRef({active:false,startX:0,startY:0});const swallowContextMenuRef=React.useRef(false);const findCCNoteIndex=(b,mouseY,ccH)=>{const snapped=getSnapBeat(b,snapValue);const hits=[];notes.forEach((n,idx)=>{if(snapped>=n.start_beat&&snapped<=n.start_beat+n.duration_beats){const nv=ccMode==='pan'?(n.pan||0)*0.5+0.5:n.velocity!==undefined?n.velocity:0.8;const stemTop=ccH-(nv*(ccH-20)+10);hits.push({idx,dist:Math.abs(stemTop-mouseY)});}});if(hits.length>0){hits.sort((a,b)=>a.dist-b.dist);return hits[0].idx;}let nearest=-1;let minDist=Infinity;notes.forEach((n,idx)=>{const center=n.start_beat+n.duration_beats/2;const d=Math.abs(center-snapped);if(d{const snapped=getSnapBeat(b,snapValue);const out=[];notes.forEach((n,idx)=>{if(Math.abs(n.start_beat-snapped)<0.01)out.push(idx);});return out;};const handleCCMouseDown=e=>{const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const noteIdx=findCCNoteIndex(beat,y,h);const val=Math.max(0,Math.min(1,(h-y)/h));if(e.ctrlKey){if(selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1&&selectedNoteIds.includes(notes[cursorNoteIdx]?notes[cursorNoteIdx].id:-1)){const currentNote=notes[cursorNoteIdx];const currentVal=ccMode==='pan'?(currentNote.pan||0)/2.0+0.5:currentNote.velocity!==undefined?currentNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[cursorNoteIdx]};}else{ccDragRef.current={active:true,lastBeat:beat,selectedMode:true,lastPainted:[]};}}else{// Không chọn notes: vẽ TẤT CẢ notes cùng beat (chord — user 08:25) +const idxs=findCCNoteIndicesAtBeat(beat);ccDragRef.current={active:true,lastBeat:beat,lastPainted:idxs};}return;}if(noteIdx!==-1){const currentVal=ccMode==='pan'?(notes[noteIdx].pan||0)/2.0+0.5:notes[noteIdx].velocity!==undefined?notes[noteIdx].velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==noteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}}};const handleCCMouseMove=e=>{if(!ccDragRef.current||!ccDragRef.current.active)return;const canvas=ccCanvasRef.current;if(!canvas)return;const rect=canvas.getBoundingClientRect();const x=e.clientX-rect.left;const y=e.clientY-rect.top;const h=rect.height;const beat=x/pixelsPerBeat-renderBeatOffset;const val=Math.max(0,Math.min(1,(h-y)/h));const drag=ccDragRef.current;const painted=drag.lastPainted||[];if(drag.selectedMode&&selectedNoteIds.length>0){const cursorNoteIdx=findCCNoteIndex(beat,y,h);if(cursorNoteIdx!==-1){const cursorNote=notes[cursorNoteIdx];if(cursorNote&&selectedNoteIds.includes(cursorNote.id)&&!painted.includes(cursorNoteIdx)){const currentVal=ccMode==='pan'?(cursorNote.pan||0)/2.0+0.5:cursorNote.velocity!==undefined?cursorNote.velocity:0.8;if(Math.abs(currentVal-val)>0.001){const updatedNotes=notes.map((n,i)=>{if(i!==cursorNoteIdx)return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);}drag.lastPainted=[...painted,cursorNoteIdx];}}return;}// Vẽ TẤT CẢ notes cùng beat (chord — user 08:25) — trước đây chỉ 1 note +const candidateIdxs=findCCNoteIndicesAtBeat(beat);const unpainted=candidateIdxs.filter(ci=>!painted.includes(ci));if(unpainted.length>0){const updatedNotes=notes.map((n,i)=>{if(!unpainted.includes(i))return n;if(ccMode==='pan')return{...n,pan:(val-0.5)*2.0};return{...n,velocity:val};});setNotes(updatedNotes);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updatedNotes);drag.lastPainted=[...painted,...unpainted];}};const renderKeybed=()=>{var kbTrk=activeTracks.find(function(t){return t.id===st.trackId;});var kbCtx=resolveTrackInstrumentCtx(kbTrk,activeTracks);ensureSonicInstrument(kbCtx);const keys=[];for(let pitch=127;pitch>=PITCH_START;pitch--){const isBlack=[1,3,6,8,10].includes(pitch%12);const notesArray=['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'];const octave=Math.floor(pitch/12)-1;const label=`${notesArray[pitch%12]}${octave}`;const showLabel=pitch%12===0;keys.push(/*#__PURE__*/React.createElement("div",{key:pitch,style:{height:`${NoteHeight}px`},className:`w-[60px] shrink-0 border-b border-zinc-800 text-[9px] font-bold flex items-center justify-end pr-2 transition cursor-pointer ${activeMidiPitches&&activeMidiPitches.has(pitch)?'bg-emerald-500 text-white border-emerald-400':isBlack?'bg-zinc-950 text-slate-500 border-zinc-900 hover:bg-zinc-800':'bg-white text-zinc-800 border-r border-zinc-400 hover:bg-amber-100'}`,onMouseDown:e=>{e.stopPropagation();keybedMouseDownRef.current=true;try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(st.trackId,100);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,500,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}}catch(err){console.error('playNote error:',err);}},onMouseEnter:e=>{if(keybedMouseDownRef.current){try{if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(st.trackId,100);}if(window.SonicSF){window.SonicSF.playNote(pitch,100,200,undefined,kbCtx.program,null,kbCtx.ch,kbCtx.synthEngine);}}catch(err){console.error('playNote error:',err);}}},onMouseUp:()=>{keybedMouseDownRef.current=false;}},showLabel&&label));}return keys;};const handleScroll=e=>{if(keybedRef.current){keybedRef.current.scrollTop=e.currentTarget.scrollTop;}if(rulerScrollRef.current){rulerScrollRef.current.scrollLeft=e.currentTarget.scrollLeft;}if(ccWrapperRef.current){ccWrapperRef.current.scrollLeft=e.currentTarget.scrollLeft;}const el=e.currentTarget;const THRESHOLD=200;if(el.scrollLeft+el.clientWidth>=el.scrollWidth-THRESHOLD){const newBeats=rollBeatsRef.current+16;setRollBeats(newBeats);}};const handleKeybedScroll=e=>{if(gridScrollRef.current){gridScrollRef.current.scrollTop=e.currentTarget.scrollTop;}};const SCALES={"None":null,"Diatonic":{"Major":[0,2,4,5,7,9,11],"Minor":[0,2,3,5,7,8,10],"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10],"Locrian":[0,1,3,5,6,8,10]},"Pentatonic":{"Major":[0,2,4,7,9],"Minor":[0,3,5,7,10],"Blues":[0,3,5,6,7,10],"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"India":[0,2,5,7,9],"Japan":[0,2,5,7,9],"Africa":[0,3,5,7,10]},"Church":{"Dorian":[0,2,3,5,7,9,10],"Phrygian":[0,1,3,5,7,8,10],"Lydian":[0,2,4,6,7,9,11],"Mixolydian":[0,2,4,5,7,9,10]},"Jazz":{"Blues":[0,3,5,6,7,10],"Bebop":[0,2,4,5,7,9,10,11],"Diminished":[0,2,3,5,6,8,9,11]},"Asian Traditional":{"Chinese":[0,2,4,7,9],"Vietnam":[0,2,6,7,10],"Japan":[0,2,5,7,9],"India":[0,2,5,7,9],"Gamelan":[0,2,4,7,9],"Korea":[0,2,4,5,7,9]},"Middle Eastern":{"Hijaz":[0,1,4,5,7,8,11],"Nikriz":[0,1,4,5,7,8,10],"Rast":[0,2,4,5,7,8,10],"Saba":[0,1,3,4,7,8,10],"Bayati":[0,2,3,4,7,8,10]}};const[selectedScale,setSelectedScale]=React.useState(null);const selectedScaleRef=React.useRef(null);selectedScaleRef.current=selectedScale;// Root note (0-11 — C..B) — transpose scale highlight + snap (spec 20:12) +const[scaleRoot,setScaleRoot]=React.useState(0);const scaleRootRef=React.useRef(0);scaleRootRef.current=scaleRoot;// Modal states: Arpeggiator / Strummer / Humanize (spec 20:12) +const[arpModal,setArpModal]=React.useState(null);// { pattern, rate, octaves, gate } +const[strumModal,setStrumModal]=React.useState(null);// { ms, direction } +const[humanizeModal,setHumanizeModal]=React.useState(null);// { timingMs, velRange, durRange } +const[chordType,setChordType]=React.useState('triad');// chord stamp (spec) +const[chordStampMode,setChordStampMode]=React.useState(false);// stamp mode toggle +const[velocityTarget,setVelocityTarget]=React.useState(80);// compress target (spec) +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[showCC,setShowCC]=React.useState(true);const[ccHeight,setCcHeight]=React.useState(80);const snapPitchToScale=(pitch,scale)=>{if(!scale)return pitch;const octave=Math.floor(pitch/12);const noteInOctave=pitch%12;if(scale.includes(noteInOctave))return pitch;let best=noteInOctave;let minDist=12;scale.forEach(s=>{const dist=Math.abs(s-noteInOctave);if(dist{const sc=selectedScaleRef.current;if(!sc)return null;return sc.map(s=>(s+scaleRootRef.current)%12);};const commitNotes=updated=>{setNotes(updated);if(isPlaying&&onRescheduleMidi)onRescheduleMidi(updated);};const applyArpeggiate=p=>{const sc=scaleWithRoot();const beatSec=60.0/(parseInt(bpm)||120);const rateDiv=p.rate==='1/4'?1:p.rate==='1/8'?0.5:p.rate==='1/16'?0.25:p.rate==='1/32'?0.125:1;const rateBeats=rateDiv*(p.triplet?2/3:p.dotted?1.5:1);const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const chords=[];targets.forEach(n=>{const k=n.start_beat.toFixed(2);const g=chords.find(c=>c.k===k);if(g)g.notes.push(n);else chords.push({k,notes:[n]});});const result=[];const rangeNotes=[];// arpeggiated sequence (pitch per step) +chords.forEach(ch=>{const sorted=ch.notes.slice().sort((a,b)=>a.pitch-b.pitch);const notesOut=[];for(let o=0;o{const octPitch=n.pitch+o*12;if(!rangeNotes.includes(octPitch))rangeNotes.push(octPitch);});}const seq=[];rangeNotes.splice(0,rangeNotes.length);chords.forEach(()=>{});const sortedAsc=ch.notes.slice().sort((a,b)=>a.pitch-b.pitch);const sortedDesc=sortedAsc.slice().reverse();const pool=p.pattern==='DOWN'?sortedDesc:p.pattern==='UP-DOWN'?[...sortedAsc,...sortedDesc.slice(1,-1)]:p.pattern==='RANDOM'?sortedAsc.slice().sort(()=>Math.random()-0.5):sortedAsc;// UP / CHORD +const steps=p.pattern==='CHORD'?1:pool.length*p.octaves;const seqPitches=[];for(let i=0;iseqPitches.push(n.pitch));break;}seqPitches.push(pool[i%pool.length].pitch+Math.floor(i/pool.length)*12);}const stepDur=p.pattern==='CHORD'?rateBeats*pool.length:rateBeats;const total=seqPitches.length*stepDur;seqPitches.forEach((pitch,i)=>{const dur=stepDur*(p.gate/100);const vel=ch.notes[0]?ch.notes[0].velocity:0.8;notesOut.push({id:'note_'+Math.random().toString(36).substr(2,9),pitch,start_beat:ch.notes[0].start_beat+i*stepDur,duration_beats:Math.max(0.05,dur),velocity:vel});});result.push(...notesOut);});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result].sort((a,b)=>a.start_beat-b.start_beat));setArpModal(null);showToast('Đã arpeggiate '+targets.length+' nốt.','success');};const applyStrum=p=>{const secPerBeat=60.0/(parseInt(bpm)||120);const strumBeats=p.ms/1000.0/secPerBeat;const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const groups=[];targets.forEach(n=>{const k=n.start_beat.toFixed(2);const g=groups.find(g2=>g2.k===k);if(g)g.notes.push(n);else groups.push({k,notes:[n]});});let alternateFlip=false;const result=targets.map(n=>({...n}));groups.forEach(g=>{const sorted=g.notes.slice().sort((a,b)=>a.pitch-b.pitch);const asc=p.direction==='UP'?sorted.slice().reverse():p.direction==='ALTERNATE'?alternateFlip?sorted.slice().reverse():sorted.slice():sorted;alternateFlip=!alternateFlip;const firstStart=sorted[0].start_beat;asc.forEach((note,index)=>{result.forEach(r=>{if(r.id===note.id){r.start_beat=parseFloat((firstStart+index*strumBeats).toFixed(3));r.velocity=Math.max(0.1,Math.min(1.0,parseFloat((r.velocity-index*0.03).toFixed(2))));}});});});commitNotes(result);setStrumModal(null);showToast('Đã strum '+targets.length+' nốt.','success');};const applyHumanizeModal=p=>{const spb=60.0/(parseInt(bpm)||120);const maxJitter=p.timingMs/1000.0/spb;const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const result=targets.map(n=>{const tj=(Math.random()-0.5)*2*maxJitter;const vj=(Math.random()-0.5)*2*(p.velRange/127);const dj=p.durRange?(Math.random()-0.5)*2*(p.durRange/100):0;return{...n,start_beat:Math.max(0,parseFloat((n.start_beat+tj).toFixed(3))),velocity:Math.max(0.05,Math.min(1.0,parseFloat((n.velocity+vj).toFixed(2)))),duration_beats:Math.max(0.05,parseFloat((n.duration_beats*(1+dj)).toFixed(3)))};});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);setHumanizeModal(null);showToast('Đã humanize '+targets.length+' nốt.','success');};const applyForceToScale=()=>{const sc=scaleWithRoot();if(!sc){showToast('Chưa chọn scale (nhấp chuột phải chọn Scale).','warning');return;}const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;const result=targets.map(n=>({...n,pitch:snapPitchToScale(n.pitch,sc)}));const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã force '+targets.length+' nốt về scale.','success');};const CHORD_SHAPES={triad:[0,4,7],min7:[0,3,7,10],sus4:[0,5,7],add9:[0,4,7,14]};const applyChordStamp=(beat,pitch)=>{const shape=CHORD_SHAPES[chordType]||CHORD_SHAPES.triad;const beatSec=60.0/(parseInt(bpm)||120);const barBeats=4;const newNotes=shape.map(iv=>({id:'note_'+Math.random().toString(36).substr(2,9),pitch:pitch+iv,start_beat:beat,duration_beats:1,velocity:0.8}));commitNotes([...notes,...newNotes]);showToast('Đã stamp chord ('+shape.length+' nốt).','success');};const applyHarmonize=interval=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const dups=targets.map(n=>({...n,id:'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch+interval}));commitNotes([...notes,...dups]);showToast('Đã harmonize +'+interval+' ('+dups.length+' nốt).','success');};const applyVelocityCompress=()=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const mean=targets.reduce((s,n)=>s+(n.velocity!==undefined?n.velocity:0.8),0)/targets.length;const target=velocityTarget/127;const result=targets.map(n=>{const v=n.velocity!==undefined?n.velocity:0.8;return{...n,velocity:Math.max(0.05,Math.min(1.0,v+(target-mean)))};});const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã compress velocity về '+velocityTarget+' ('+targets.length+' nốt).','success');};const applyVelocityNormalize=()=>{const base=notes.filter(n=>selectedNoteIds.includes(n.id));const targets=base.length>0?base:notes;if(!targets.length)return;const maxV=Math.max(...targets.map(n=>n.velocity!==undefined?n.velocity:0.8));if(maxV<=0)return;const result=targets.map(n=>({...n,velocity:Math.max(0.05,Math.min(1.0,(n.velocity!==undefined?n.velocity:0.8)/maxV))}));const others=notes.filter(n=>!targets.includes(n));commitNotes([...others,...result]);showToast('Đã normalize velocity (max → 127).','success');};const renderScaleContextMenu=()=>{const closeMenu=()=>setScaleMenuPos(null);const origin=scaleMenuOriginRef.current||scaleMenuPos;const items=[];let subMenu=null;const isSameScale=(a,b)=>{if(!a||!b)return a===b;if(a.length!==b.length)return false;return a.every((v,i)=>v===b[i]);};const pushItem=(label,onClick,onHover)=>{const isActive=onClick._scale&&isSameScale(onClick._scale,selectedScale);items.push(React.createElement("div",{key:label,onClick:()=>{onClick();closeMenu();},onMouseEnter:onHover||undefined,className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isActive?"bg-amber-800/40 text-amber-300":"text-zinc-300")},label));};Object.keys(SCALES).forEach(key=>{const val=SCALES[key];if(val===null){pushItem("None",()=>setSelectedScale(null));return;}if(Array.isArray(val)){pushItem(key,()=>setSelectedScale(val));}else{const parentKey=key;const isOpen=scaleMenuPos&&scaleMenuPos.parentKey===parentKey;pushItem(key+" ▸",()=>setSelectedScale(null),()=>{setScaleMenuPos({x:origin.x,y:origin.y,parentKey});});if(isOpen){const subs=[];Object.keys(val).forEach(subKey=>{subs.push(React.createElement("div",{key:subKey,onClick:()=>{setSelectedScale(val[subKey]);closeMenu();},className:"px-3 py-1.5 text-xs cursor-pointer hover:bg-amber-600 hover:text-white whitespace-nowrap "+(isSameScale(val[subKey],selectedScale)?"bg-amber-800/40 text-amber-300":"text-zinc-300")},subKey));});var subH=subs.length*30+16;var subTop=origin.y+subH+20>window.innerHeight?origin.y-subH:origin.y;subMenu=React.createElement("div",{style:{position:"fixed",left:origin.x+150,top:subTop,zIndex:10000},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[120px]"},...subs);}}});var menuH=items.length*30+16;var menuTop=origin.y+menuH+20>window.innerHeight?Math.max(10,origin.y-menuH):origin.y;return React.createElement(React.Fragment,null,React.createElement("div",{style:{position:"fixed",left:origin.x,top:menuTop,zIndex:9999},className:"bg-[#2a2a2a] border border-zinc-600 rounded shadow-xl py-1 min-w-[140px]"},...items),subMenu);};const renderBarLabels=()=>{const labels=[];const barsCount=Math.ceil(viewBeats/4);const barOffset=Math.floor(sessionStartBar);for(let bar=0;bar{e.stopPropagation();const barTime=bar*4*beatSec;setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:barTime}:s));}},`Bar ${displayBar}`));}return labels;};const beatSec=60.0/(parseInt(bpm)||120);const playHeadX=(st.currentTime||0)/beatSec*pixelsPerBeat;return React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 bg-[#1e1e1e] flex-col h-full"},/* 1. TOOLBAR HEADER — 2 hàng (wrap tự nhiên; spacer 100% ép hàng mới) */React.createElement("div",{className:"bg-[#282828] border-b border-zinc-900 flex flex-wrap items-center gap-x-2 gap-y-1 px-4 py-1.5 shrink-0 text-slate-200"},React.createElement("div",{className:"flex items-center gap-4"},React.createElement("select",{value:st.trackId||'',onChange:function(e){var trkId=e.target.value;var trkSel=(activeTracks||[]).find(function(t){return t.id===trkId;});if(trkSel&&trkSel.midiItems&&trkSel.midiItems.length)handleSwitchMidiItem(trkSel.midiItems[0].id);},className:"bg-zinc-800 border border-zinc-700 text-yellow-500 font-bold text-xs rounded px-1.5 py-0.5 outline-none focus:border-yellow-500 max-w-[180px] uppercase"},function(){var seenTrackOpts={};var trackOpts=[];(activeTracks||[]).forEach(function(t){if(!t.midiItems||!t.midiItems.length)return;if(seenTrackOpts[t.id])return;seenTrackOpts[t.id]=true;trackOpts.push(React.createElement("option",{key:t.id,value:t.id},t.name||t.id));});return trackOpts;}()),activeParentTrackName?React.createElement("span",{className:"text-[9px] text-zinc-500 ml-1"},"(Belongs to: ",React.createElement("span",{className:"text-zinc-400 font-semibold"},activeParentTrackName),")"):null,React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap:"),React.createElement("select",{value:snapValue,onChange:e=>{onSnapChange(e.target.value);setRenderTick(t=>t+1);},className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1.5 py-0.5 outline-none focus:border-yellow-500"},['free','4','1','1/2','1/4','1/8','1/16','1/32'].map(v=>React.createElement("option",{key:v,value:v},v)))),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,isArmed:!s.isArmed}:s)),className:`px-2 py-1 rounded text-xs font-bold ${st.isArmed?'bg-red-600 text-white':'bg-zinc-800 text-zinc-400'}`},"ARM"),React.createElement("select",{value:selectedMidiInputId||'',onChange:e=>onMidiInputSelect(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-xs outline-none max-w-[100px]"},React.createElement("option",{value:""},"Input"),React.createElement("option",{value:"ALL"},"Omni"),(midiDevices||[]).map(d=>React.createElement("option",{key:d.id,value:d.id},d.name||d.id))),React.createElement("button",{onClick:()=>onInstrumentSelect&&onInstrumentSelect(st.trackId),title:st.instrumentName||"Synth",className:`px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 max-w-[70px] ${st.instrumentName?'bg-violet-900 text-violet-300 border-violet-700':'bg-zinc-800 text-zinc-500 border-transparent hover:text-zinc-300'}`},React.createElement("i",{"data-lucide":"music",className:"w-3 h-3 shrink-0"}),React.createElement("span",{className:"truncate text-[9px]"},activeParentTrackName?'('+activeParentTrackName+') '+(st.instrumentName||'Synth'):st.instrumentName||'Synth')),React.createElement("div",{className:"flex items-center gap-1 ml-1 text-xs"},React.createElement("span",{className:"text-zinc-500"},"AI:"),React.createElement("input",{type:"number",value:aiBarStart,onChange:e=>setAiBarStart(parseInt(e.target.value)||0),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"-"),React.createElement("input",{type:"number",value:aiBarEnd,onChange:e=>setAiBarEnd(parseInt(e.target.value)||1),className:"w-8 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-xs text-center"}),React.createElement("span",{className:"text-zinc-500"},"bar")),React.createElement("select",{value:ccMode,onChange:e=>setCcMode(e.target.value),className:"bg-zinc-800 text-zinc-200 border border-zinc-700 rounded px-1.5 py-1 text-xs capitalize cursor-pointer"},React.createElement("option",{value:"velocity"},"Velocity"),React.createElement("option",{value:"sustain"},"Sustain"),React.createElement("option",{value:"modulation"},"Modulation"),React.createElement("option",{value:"pitch_bend"},"Pitch Bend"),React.createElement("option",{value:"pan"},"Pan"))),React.createElement("button",{onClick:()=>setShowCC(!showCC),className:`px-2 py-1 rounded text-xs ${showCC?'bg-purple-900/60 text-purple-300 border border-purple-700':'text-zinc-500 hover:text-zinc-300'}`},ccMode==='velocity'?'Vel':ccMode==='sustain'?'Sus':ccMode==='modulation'?'Mod':ccMode==='pitch_bend'?'Bend':ccMode==='pan'?'Pan':'CC'),React.createElement("button",{onClick:function(){setSessionSyncMode(function(p){return!p;});},className:function(){var base='px-2 py-1 rounded text-xs ';return sessionSyncMode?base+'bg-cyan-900/60 text-cyan-300 border border-cyan-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:sessionSyncMode?"Session-synced mode (ghost visible)":"Isolated mode (bar 0, no ghost)"},sessionSyncMode?"\uD83C\uDF10 Session":"\uD83D\uDCCB Isolated"),React.createElement("button",{onClick:function(){setShowGhostNotes(function(p){return!p;});},disabled:!sessionSyncMode,className:function(){if(!sessionSyncMode)return'px-2 py-1 rounded text-xs opacity-30 cursor-not-allowed';var base='px-2 py-1 rounded text-xs ';return showGhostNotes?base+'bg-purple-900/60 text-purple-300 border border-purple-700':base+'text-zinc-500 hover:text-zinc-300';}(),title:"Toggle ghost notes visibility"},"👻 MIDI ghost notes"),React.createElement("button",{onClick:onClose,className:"px-2.5 py-1 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 border border-zinc-700 rounded text-xs flex items-center gap-1 transition ml-auto"},React.createElement("i",{"data-lucide":"x",className:"w-3 h-3"}),"Đóng"),React.createElement("div",{style:{flexBasis:"100%",height:0}}),React.createElement("button",{onClick:()=>setArpModal({pattern:'UP',rate:'1/16',octaves:2,gate:80,triplet:false,dotted:false}),className:"px-2 py-1 rounded text-xs bg-cyan-900/40 text-cyan-300 border border-cyan-700/60 hover:bg-cyan-800/50 transition",title:"Arpeggiate (Alt+A)"},"ARP"),React.createElement("button",{onClick:()=>setStrumModal({ms:30,direction:'DOWN'}),className:"px-2 py-1 rounded text-xs bg-teal-900/40 text-teal-300 border border-teal-700/60 hover:bg-teal-800/50 transition",title:"Strum (Alt+S)"},"STRUM"),React.createElement("button",{onClick:()=>setHumanizeModal({timingMs:Math.round(humanizeStrength*100),velRange:Math.round(humanizeStrength*127),durRange:Math.round(humanizeStrength*50)}),className:"px-2 py-1 rounded text-xs bg-amber-900/40 text-amber-300 border border-amber-700/60 hover:bg-amber-800/50 transition",title:"Humanize (Alt+R)"},"HUMANIZE"),React.createElement("select",{key:"humstr",value:humanizeStrength,onChange:function(e){var v=parseFloat(e.target.value);setHumanizeStrength(v);setHumanizeModal({timingMs:Math.round(v*100),velRange:Math.round(v*127),durRange:Math.round(v*50)});},className:"px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",title:"Mức humanize (mở modal theo mức)"},React.createElement("option",{key:"l",value:0.05},"Nh\u1EB9"),React.createElement("option",{key:"m",value:0.10},"V\u1EEBa"),React.createElement("option",{key:"s",value:0.18},"M\u1EA1nh")),React.createElement("div",{key:"transpose",className:"flex items-center gap-1"},React.createElement("input",{key:"in",type:"number",step:1,min:-24,max:24,value:transposeSemis,onChange:function(e){setTransposeSemis(e.target.value);},className:"w-12 px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-center text-zinc-200",title:"Semitone offset (vd 2 = cao hơn 1 tone)"}),React.createElement("button",{key:"btn",onClick:function(){applyTranspose(transposeSemis);},className:"px-2 py-1 rounded text-xs bg-sky-900/40 text-sky-300 border border-sky-700/60 hover:bg-sky-800/50 transition",title:"Transpose all notes by the semitone offset"},"Transpose")),React.createElement("div",{key:"keyshift",className:"flex items-center gap-1"},React.createElement("select",{key:"root",value:keyTargetRoot,onChange:function(e){setKeyTargetRoot(e.target.value);},className:"px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",title:"Giọng đích (root)"},["C","C#","D","D#","E","F","F#","G","G#","A","A#","B"].map(function(r){return React.createElement("option",{key:r,value:r},r);})),React.createElement("select",{key:"scale",value:keyTargetScale,onChange:function(e){setKeyTargetScale(e.target.value);},className:"px-1 py-1 rounded text-xs bg-zinc-900 border border-zinc-700 text-zinc-300 cursor-pointer",title:"Thể scale đích"},React.createElement("option",{key:"maj",value:"major"},"major"),React.createElement("option",{key:"min",value:"minor"},"minor")),React.createElement("button",{key:"btn",onClick:applyTransposeToKey,className:"px-2 py-1 rounded text-xs bg-violet-900/40 text-violet-300 border border-violet-700/60 hover:bg-violet-800/50 transition",title:"Chuyển giọng: map degree hiện tại sang giọng đích (auto-detect key nguồn)"},"🎵 Chuyển giọng")),/* ── CÙNG HÀNG (sau Chuyển giọng — user 08:40: gộp 1 hàng, bỏ spacer) ── */React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Snap to Scale"),React.createElement("button",{onClick:()=>setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,snapToScale:!(s.snapToScale!==undefined?s.snapToScale:true)}:s)),className:`w-7 h-4 rounded-full transition-colors relative ${(st.snapToScale!==undefined?st.snapToScale:true)?'bg-yellow-600':'bg-zinc-700'}`,style:{padding:0}},React.createElement("div",{className:`w-3 h-3 rounded-full bg-white absolute top-0.5 transition-transform ${(st.snapToScale!==undefined?st.snapToScale:true)?'translate-x-3.5':'translate-x-0.5'}`}))),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Scale:"),React.createElement("select",{value:JSON.stringify(selectedScale||null),onChange:e=>{const v=e.target.value;setSelectedScale(v==='null'?null:JSON.parse(v));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 max-w-[110px]"},React.createElement("option",{value:"null"},"None"),function(){const opts=[];const pushKey=(label,val)=>opts.push(React.createElement("option",{key:label,value:JSON.stringify(val)},label));Object.keys(SCALES||{}).forEach(k=>{const v=SCALES[k];if(v===null)return;if(Array.isArray(v))pushKey(k,v);else Object.keys(v).forEach(sk=>pushKey(sk,v[sk]));});return opts;}()),React.createElement("select",{value:scaleRoot,onChange:e=>{setScaleRoot(parseInt(e.target.value)||0);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"},['C','C#','D','D#','E','F','F#','G','G#','A','A#','B'].map((r,i)=>React.createElement("option",{key:r,value:i},r))),React.createElement("button",{onClick:()=>applyForceToScale(),title:"Force selected notes to scale",className:"px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-amber-400 border border-zinc-700 hover:border-amber-600"},"Force")),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold ml-1"},"Chord:"),React.createElement("select",{value:chordType,onChange:e=>setChordType(e.target.value),className:"bg-zinc-800 border border-zinc-700 text-zinc-300 rounded px-1 py-0.5 text-[10px] outline-none"},React.createElement("option",{value:"triad"},"Triad"),React.createElement("option",{value:"min7"},"Min7"),React.createElement("option",{value:"sus4"},"Sus4"),React.createElement("option",{value:"add9"},"Add9")),React.createElement("button",{onClick:()=>setChordStampMode(m=>!m),title:"Chord stamp mode — click canvas to stamp chord (Shift+C)",className:`px-1.5 py-0.5 rounded text-[10px] border ${chordStampMode?'bg-amber-800/60 text-amber-300 border-amber-600':'bg-zinc-800 text-zinc-400 border-zinc-700 hover:border-amber-600'}`},"Stamp"),React.createElement("button",{onClick:()=>applyHarmonize(3),title:"Harmonize +3rd",className:"px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"},"+3"),React.createElement("button",{onClick:()=>applyHarmonize(5),title:"Harmonize +5th",className:"px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"},"+5"),React.createElement("button",{onClick:()=>applyHarmonize(7),title:"Harmonize +7th",className:"px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-emerald-600"},"+7")),React.createElement("div",{className:"flex items-center gap-1 text-xs"},React.createElement("span",{className:"text-zinc-500 font-semibold"},"Vel:"),React.createElement("button",{onClick:()=>applyVelocityCompress(),title:"Compress velocity toward target",className:"px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"},"Comp"),React.createElement("input",{type:"number",value:velocityTarget,onChange:e=>setVelocityTarget(parseInt(e.target.value)||80),className:"w-9 bg-zinc-900 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 text-[10px] text-center"}),React.createElement("button",{onClick:()=>applyVelocityNormalize(),title:"Normalize (max → 127)",className:"px-1.5 py-0.5 rounded text-[10px] bg-zinc-800 text-zinc-300 border border-zinc-700 hover:border-purple-600"},"Norm")),React.createElement("div",{className:"flex items-center gap-1 ml-auto"},React.createElement("button",{onClick:()=>onSaveNotes(st.id,st.trackId,st.target_id,notes),className:"px-2.5 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"save",className:"w-3 h-3"}),"L\u01B0u"),React.createElement("button",{onClick:()=>{const ppq=480;const bpmNum=parseInt(bpm)||120;const ticksPerBeat=ppq;const events=[];(notes||[]).forEach(n=>{const startTick=Math.round((n.start_beat||0)*ticksPerBeat);const durTick=Math.round((n.duration_beats||1)*ticksPerBeat);const pitch=n.pitch||60;const vel=Math.round((n.velocity||0.8)*127);events.push({tick:startTick,type:'note_on',pitch,velocity:vel});events.push({tick:startTick+durTick,type:'note_off',pitch,velocity:0});});events.sort((a,b)=>a.tick-b.tick||(a.type==='note_off'?-1:1));const writeVLQ=(bytes,v)=>{let val=Math.max(0,v);const buf=[];buf.push(val&0x7F);while(val>0x7F){val>>=7;buf.push(0x80|val&0x7F);}for(let i=buf.length-1;i>=0;i--)bytes.push(buf[i]);};const trackBytes=[];let lastTick=0;events.forEach(ev=>{const delta=Math.max(0,ev.tick-lastTick);writeVLQ(trackBytes,delta);trackBytes.push(ev.type==='note_on'?0x90:0x80,ev.pitch,ev.velocity);lastTick=ev.tick;});writeVLQ(trackBytes,0);trackBytes.push(0xFF,0x2F,0x00);const trackData=[0x4D,0x54,0x72,0x6B];const len=trackBytes.length;trackData.push(len>>24&0xFF,len>>16&0xFF,len>>8&0xFF,len&0xFF);trackData.push(...trackBytes);const header=[0x4D,0x54,0x68,0x64,0x00,0x00,0x00,0x06,0x00,0x01,0x00,0x01,ppq>>8&0xFF,ppq&0xFF];const all=header.concat(trackData);const blob=new Blob([new Uint8Array(all)],{type:'audio/midi'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=(st.label||'midi')+'.mid';document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);showToast('Đã xuất file MIDI!','success');},className:"px-2.5 py-1 bg-amber-700 hover:bg-amber-600 text-white rounded text-xs flex items-center gap-1 transition font-semibold"},React.createElement("i",{"data-lucide":"file-down",className:"w-3 h-3"}),"Export MIDI"))),/* 2. BAR RULER */React.createElement("div",{className:"h-6 bg-[#1a1a1f] border-b border-zinc-900 flex shrink-0"},React.createElement("div",{className:"w-[120px] bg-[#1e1e22] border-r border-zinc-800 shrink-0 flex items-end"},React.createElement("span",{className:"text-[8px] text-zinc-600 font-mono px-1.5 pb-0.5 uppercase tracking-wider"},"Tracks")),React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 shrink-0"}),React.createElement("div",{ref:rulerScrollRef,className:"flex-1 overflow-hidden",onMouseDown:e=>{const rect=e.currentTarget.getBoundingClientRect();const x=e.clientX-rect.left+e.currentTarget.scrollLeft;const clickBeat=x/pixelsPerBeat;const clickTime=clickBeat*beatSec;const clickInRange=loopStartBeat!==null&&loopEndBeat!==null&&clickBeat>=loopStartBeat&&clickBeat<=loopEndBeat;if(e.ctrlKey||e.metaKey){setLoopStartBeat(null);setLoopEndBeat(null);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:null,selectionEnd:null}:s));return;}if(e.shiftKey){const beatSnap=getSnapBeat(clickBeat,snapValue);if(loopStartBeat===null){setLoopStartBeat(Math.max(0,beatSnap-4));setLoopEndBeat(Math.max(4,beatSnap));}else{setLoopEndBeat(Math.max(loopStartBeat+4,beatSnap));}return;}if(clickTime>=0){if(onSeekPlayhead){onSeekPlayhead(clickTime);}else{setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,currentTime:clickTime}:s));}}const snappedStartBeat=getSnapBeat(clickBeat,snapValue);rulerDragRef.current={startX:e.clientX,startBeat:snappedStartBeat,scrollLeft:e.currentTarget.scrollLeft};const onMove=ev=>{const r=rulerScrollRef.current;if(!r||!rulerDragRef.current)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+rulerDragRef.current.scrollLeft;const rawBeat=Math.max(0,bx/pixelsPerBeat);const beat=getSnapBeat(rawBeat,snapValue);if(Math.abs(ev.clientX-rulerDragRef.current.startX)>5){if(clickInRange){const rangeWidth=loopEndBeat-loopStartBeat;const offset=rulerDragRef.current.startBeat-loopStartBeat;const centerBeat=beat-offset;const halfRange=rangeWidth/2;const newStart=Math.max(0,centerBeat-halfRange);setLoopStartBeat(newStart);setLoopEndBeat(newStart+rangeWidth);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:newStart*beatSec,selectionEnd:(newStart+rangeWidth)*beatSec}:s));}else{const sBeat=Math.max(0,Math.min(rulerDragRef.current.startBeat,beat));const eBeat=Math.max(sBeat+1,Math.max(rulerDragRef.current.startBeat,beat));setLoopStartBeat(sBeat);setLoopEndBeat(eBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:sBeat*beatSec,selectionEnd:eBeat*beatSec}:s));}}};const onUp=()=>{rulerDragRef.current=null;document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative h-full font-mono text-[9px] text-zinc-500 font-bold"},renderBarLabels(),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/15 border-l border-r border-emerald-400"},React.createElement("div",{style:{position:'absolute',left:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const startBeat=loopStartBeat;const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(0,Math.min(loopEndBeat-1,getSnapBeat(bx/pixelsPerBeat,snapValue)));setLoopStartBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionStart:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}),React.createElement("div",{style:{position:'absolute',right:0,top:0,bottom:0,width:'4px',cursor:'ew-resize'},onMouseDown:e=>{e.stopPropagation();const onMove=ev=>{const r=rulerScrollRef.current;if(!r)return;const rRect=r.getBoundingClientRect();const bx=ev.clientX-rRect.left+r.scrollLeft;const nBeat=Math.max(loopStartBeat+1,getSnapBeat(bx/pixelsPerBeat,snapValue));setLoopEndBeat(nBeat);setSubTabs(prev=>prev.map(s=>s.id===st.id?{...s,selectionEnd:nBeat*beatSec}:s));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);}}))))),/* 3. MAIN PIANO ROLL GRID (KEYBOARD + TRACK COL + CANVAS) */React.createElement("div",{className:"flex-1 flex overflow-hidden min-h-0 relative"},/* Track column */React.createElement("div",{className:"w-[120px] bg-[#16161a] border-r border-zinc-800 shrink-0 flex flex-col z-10",style:{height:KeybedPixelHeight+'px'}},allMidiItems.length>0?function(){var seenTracks={};var els=[];allMidiItems.forEach(function(m){if(seenTracks[m._trackId])return;seenTracks[m._trackId]=true;var track=(activeTracks||[]).find(function(t){return t.id===m._trackId;});var isActive=m._trackId===st.trackId&&m.id===st.target_id;var isPlayOn=activePlayTrackIds&&activePlayTrackIds.indexOf(m._trackId)!==-1;els.push(React.createElement("button",{key:m._trackId,onClick:function(){var prevList=activePlayTrackIds||[];var nextList=prevList.indexOf(m._trackId)!==-1?prevList.filter(function(id){return id!==m._trackId;}):prevList.concat([m._trackId]);setActivePlayTrackIds(nextList);if(onRealtimePlay)onRealtimePlay(nextList);},className:"flex items-center justify-center h-[20px] border border-zinc-600 rounded-md cursor-pointer outline-none mx-1 my-[2px] "+(isActive?'bg-yellow-600 text-black font-bold':isPlayOn?'bg-red-700 text-white font-semibold':'bg-zinc-700 text-zinc-300 hover:bg-zinc-600')},React.createElement("span",{className:"text-[14px] font-sans truncate px-1",title:track?track.name:m._trackName},track?track.name:m._trackName)));});return els;}():null),React.createElement("div",{className:"w-[60px] shrink-0 flex flex-col border-r border-zinc-900 overflow-y-auto",ref:keybedRef,onScroll:handleKeybedScroll,style:{scrollbarWidth:'none',msOverflowStyle:'none'}},renderKeybed()),React.createElement("div",{ref:gridScrollRef,onScroll:handleScroll,className:"flex-1 overflow-auto bg-[#141414] min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:`${(128-PITCH_START)*NoteHeight}px`},className:"relative"},React.createElement("canvas",{ref:canvasRef,onMouseDown:handleGridMouseDown,onMouseMove:handleGridMouseMove,onMouseUp:handleGridMouseUp,onMouseLeave:handleGridMouseUp,onContextMenu:handleContextMenu,className:"absolute inset-0 cursor-crosshair"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 4. CC / VELOCITY LANE (ĐẶT Ở ĐÁY) */showCC&&React.createElement("div",{style:{height:`${ccHeight}px`},className:"bg-[#161616] border-t border-zinc-900 flex shrink-0 relative"},React.createElement("div",{onMouseDown:e=>{e.preventDefault();const startY=e.clientY;const startH=ccHeight;const onMove=ev=>{setCcHeight(Math.max(40,startH+startY-ev.clientY));};const onUp=()=>{document.removeEventListener('mousemove',onMove);document.removeEventListener('mouseup',onUp);};document.addEventListener('mousemove',onMove);document.addEventListener('mouseup',onUp);},className:"absolute top-0 left-0 right-0 h-1.5 cursor-n-resize z-10 hover:bg-purple-600/30"}),React.createElement("div",{className:"w-[120px] shrink-0"}),React.createElement("div",{className:"w-[60px] bg-[#222222] border-r border-zinc-900 flex items-center justify-center text-[10px] text-zinc-500 font-mono shrink-0 font-bold"},ccMode.toUpperCase()),React.createElement("div",{ref:ccWrapperRef,className:"flex-1 overflow-x-hidden min-w-0"},React.createElement("div",{style:{width:`${viewWidth}px`,height:'100%'},className:"relative"},React.createElement("canvas",{ref:ccCanvasRef,onMouseDown:handleCCMouseDown,onMouseMove:handleCCMouseMove,onMouseUp:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},onMouseLeave:()=>{if(ccDragRef.current)ccDragRef.current.selectedMode=false;ccDragRef.current=null;},className:"absolute inset-0"}),loopStartBeat!==null&&loopEndBeat!==null&&loopEndBeat>loopStartBeat&&React.createElement("div",{style:{left:`${loopStartBeat*pixelsPerBeat}px`,width:`${(loopEndBeat-loopStartBeat)*pixelsPerBeat}px`,top:0,bottom:0},className:"absolute bg-emerald-500/10 border-l border-r border-emerald-400/50 pointer-events-none"})))),/* 5. OVERLAY / CONTEXT MENU */arpModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Arpeggiate"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Pattern"),React.createElement("select",{value:arpModal.pattern,onChange:e=>setArpModal({...arpModal,pattern:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['UP','DOWN','UP-DOWN','RANDOM','CHORD'].map(p=>React.createElement("option",{key:p,value:p},p))),React.createElement("label",{className:"text-zinc-500"},"Rate"),React.createElement("select",{value:arpModal.rate,onChange:e=>setArpModal({...arpModal,rate:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['1/4','1/8','1/16','1/32'].map(r=>React.createElement("option",{key:r,value:r},r))),React.createElement("label",{className:"text-zinc-500"},"Octaves"),React.createElement("input",{type:"number",min:1,max:4,value:arpModal.octaves,onChange:e=>setArpModal({...arpModal,octaves:parseInt(e.target.value)||1}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14"}),React.createElement("label",{className:"text-zinc-500"},"Gate %"),React.createElement("input",{type:"number",min:10,max:200,value:arpModal.gate,onChange:e=>setArpModal({...arpModal,gate:parseInt(e.target.value)||80}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-14"})),React.createElement("div",{className:"flex gap-1 mb-3"},React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-zinc-400"},React.createElement("input",{type:"checkbox",checked:!!arpModal.triplet,onChange:e=>setArpModal({...arpModal,triplet:e.target.checked})}),"Triplet"),React.createElement("label",{className:"flex items-center gap-1 text-[10px] text-zinc-400"},React.createElement("input",{type:"checkbox",checked:!!arpModal.dotted,onChange:e=>setArpModal({...arpModal,dotted:e.target.checked})}),"Dotted")),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyArpeggiate(arpModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setArpModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),strumModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Strum"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Strum ms"),React.createElement("input",{type:"number",min:0,max:120,value:strumModal.ms,onChange:e=>setStrumModal({...strumModal,ms:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Direction"),React.createElement("select",{value:strumModal.direction,onChange:e=>setStrumModal({...strumModal,direction:e.target.value}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5"},['DOWN','UP','ALTERNATE'].map(d=>React.createElement("option",{key:d,value:d},d)))),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyStrum(strumModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setStrumModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),humanizeModal&&React.createElement("div",{className:"absolute inset-0 z-40 flex items-center justify-center bg-black/60",onMouseDown:e=>e.stopPropagation(),onClick:e=>e.stopPropagation()},React.createElement("div",{className:"bg-zinc-900 border border-zinc-700 rounded-lg p-4 w-72 shadow-2xl"},React.createElement("div",{className:"text-sm font-bold text-cyan-300 mb-3"},"Humanize"),React.createElement("div",{className:"grid grid-cols-2 gap-2 text-xs mb-3"},React.createElement("label",{className:"text-zinc-500"},"Timing ±ms"),React.createElement("input",{type:"number",min:0,max:30,value:humanizeModal.timingMs,onChange:e=>setHumanizeModal({...humanizeModal,timingMs:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Vel ±(0-127)"),React.createElement("input",{type:"number",min:0,max:20,value:humanizeModal.velRange,onChange:e=>setHumanizeModal({...humanizeModal,velRange:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"}),React.createElement("label",{className:"text-zinc-500"},"Dur ±%"),React.createElement("input",{type:"number",min:0,max:50,value:humanizeModal.durRange,onChange:e=>setHumanizeModal({...humanizeModal,durRange:parseInt(e.target.value)||0}),className:"bg-zinc-800 border border-zinc-700 text-zinc-200 rounded px-1 py-0.5 w-16"})),React.createElement("div",{className:"flex gap-2"},React.createElement("button",{onClick:()=>applyHumanizeModal(humanizeModal),className:"flex-1 px-3 py-1.5 rounded text-xs font-bold bg-cyan-700 hover:bg-cyan-600 text-white"},"Apply"),React.createElement("button",{onClick:()=>setHumanizeModal(null),className:"px-3 py-1.5 rounded text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-300"},"Cancel")))),scaleMenuPos&&renderScaleContextMenu());};const serializeTracksList=(tracksList,secondsPerBar)=>{return(tracksList||[]).map(t=>{let trackType="AUDIO";if(t.type==='MIDI'||t.type==='soundfont'||t.type==='vst3')trackType="MIDI";else if(t.type==='SECTION')trackType="SECTION";else if(t.sections&&t.sections.length>0)trackType="SECTION";else if(t.midiItems&&t.midiItems.length>0)trackType="MIDI";const items=[];// Serialize EVERY item type present on the track (a track can hold audio // clips + MIDI items + section items at once). The old if/else-if chain // dropped all but one type per track — silent data loss on save. if(t.clips&&t.clips.length>0){t.clips.forEach(c=>{const durationSec=c.buffer?c.buffer.duration:4.0;const clipFileId=c.serverFileId||t.serverFileId;items.push({id:c.id,name:c.name||"Audio Clip",type:"AUDIO_ITEM",start_bar:c.startTime/secondsPerBar,duration_bars:durationSec/(c.speed||1.0)/secondsPerBar,clip_start_offset_bars:0.0,source_data:{audio_file_url:clipFileId?`/static/audio/uploads/${clipFileId}`:"",sample_rate:c.buffer?c.buffer.sampleRate:44100,channels:c.buffer?c.buffer.numberOfChannels:2,gain:1.0,server_file_id:clipFileId}});});}if(t.midiItems&&t.midiItems.length>0){t.midiItems.forEach(m=>{items.push({id:m.id,name:m.name||"MIDI Item",type:"MIDI_ITEM",start_bar:m.startTime/secondsPerBar,duration_bars:m.duration?m.duration/secondsPerBar:4.0,clip_start_offset_bars:0.0,source_data:{total_buffer_bars:m.duration?m.duration/secondsPerBar:8.0,notes:(m.notes||[]).map(n=>({id:n.id||'note_'+Math.random().toString(36).substr(2,9),pitch:n.pitch||60,start_beat:n.start_beat||0.0,duration_beats:n.duration_beats||1.0,velocity:n.velocity||0.8,pan:n.pan||0.0}))}});});}if(t.sections&&t.sections.length>0){t.sections.forEach(s=>{items.push({id:s.id,name:s.name||"Section Item",type:"SECTION_ITEM",start_bar:s.start/secondsPerBar,duration_bars:s.duration/secondsPerBar,clip_start_offset_bars:0.0,source_data:{// KHÔNG fallback s.id: section item thiếu sectionId (insert thiếu diff --git a/app/templates/index.html b/app/templates/index.html index 3b3ec4f..2c82bf9 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -24,7 +24,7 @@ - +