FIX: sửa lỗi copy/cut paste không đúng item

This commit is contained in:
2026-08-07 17:06:05 +07:00
parent 4f34491f3e
commit 1d9b426d15
4 changed files with 313 additions and 96 deletions
+182 -66
View File
@@ -3083,7 +3083,18 @@ const WaveformLane = ({
for (const sec of secList) {
if (time >= sec.start && time < sec.start + sec.duration) { hitSectionId = sec.id; break; }
}
if (onContextMenu) onContextMenu(e, track.id, time, hitSectionId);
// Detect MIDI item dưi chut (user 07:25 Cut/Copy item phi copy
// ĐÚNG item trưc đây context menu ch biết track copy nhm buffer)
let hitMidiId = null;
for (const m of (track.midiItems || [])) {
if (time >= m.startTime && time < m.startTime + (m.duration || 4)) { hitMidiId = m.id; break; }
}
// Detect clip dưi chut
let hitClipId = null;
for (const c of (track.clips || [])) {
if (c.buffer && time >= c.startTime && time < c.startTime + c.buffer.duration / (c.speed || 1.0)) { hitClipId = c.id; break; }
}
if (onContextMenu) onContextMenu(e, track.id, time, hitSectionId, hitMidiId || hitClipId || null, hitMidiId ? 'midi' : (hitClipId ? 'clip' : null));
}
}));
};
@@ -14912,6 +14923,30 @@ const App = () => {
// Tab mi hot đng bình thưng; MAIN gi hành vi cũ (m piano-roll lúc
// main play main tiếp tc handleEditMidiInTab đã x lý).
if (prevTab !== activeTab) {
// Vào SECTION-TAB lúc MAIN đang play CH play items CA SECTION-TAB,
// TT âm ca MAIN items (user yêu cu 06:40): restart play vi
// activeTracks = section clones (activeTab mi activeTracksRef mi).
if (prevTab === 'main' && activeTab && activeTab.startsWith('session_') && isPlaying) {
try {
const _ctx = getAudioContext();
// V trí play LOCAL ca section tab (tr secStart v trí section
// trên main): section tab items dùng time local (0-based)
let _secStart = 0;
const _tab = sessionTabsRef.current.find(st => st.id === activeTab);
if (_tab && _tab.sectionId) {
(tracks || []).forEach(_t => (_t.sections || []).forEach(_s => {
if (_s.sectionId === _tab.sectionId || _s.id === _tab.sectionId) _secStart = _s.start || 0;
}));
}
const _pt = Math.max(0, startOffsetTimeRef.current + (_ctx.currentTime - startAudioTimeRef.current) - _secStart);
stopAllPlayback();
startOffsetTimeRef.current = _pt;
startAudioTimeRef.current = _ctx.currentTime;
startTrackPlayback(_pt);
setIsPlaying(true);
console.log('[SectionTab] play CHỈ section items tại local', _pt.toFixed(2), '(secStart', _secStart.toFixed(2) + ')');
} catch (e) { console.warn('[SectionTab] restart error:', e); }
}
const prevSub = subTabsRef.current.find(s => s.id === prevTab);
if (prevSub && prevSub.isPlaying) {
// PIANO ROLL tab: KHÔNG stop âm khi ri tab (SF notes đã schedule
@@ -17326,7 +17361,7 @@ const App = () => {
};
// Context Menu Handlers
const handleContextMenu = (e, trackId, clickTime, sectionId) => {
const handleContextMenu = (e, trackId, clickTime, sectionId, itemId, itemType) => {
e.preventDefault();
e.stopPropagation();
setContextMenu({
@@ -17334,7 +17369,9 @@ const App = () => {
y: e.clientY,
trackId,
time: clickTime || currentTime,
sectionId: sectionId || null
sectionId: sectionId || null,
itemId: itemId || null,
itemType: itemType || null
});
};
const closeContextMenu = () => setContextMenu(null);
@@ -17516,6 +17553,46 @@ const App = () => {
}
};
const contextMenuCopy = () => {
// Context menu trên ITEM copy ĐÚNG item (user 07:25 trưc đây copy
// nhm track buffer paste ra audio item dù cut/copy midi item).
if (contextMenu.itemType === 'midi' && contextMenu.itemId) {
const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
if (item) {
const bpmVal = parseInt(bpm) || 120;
clipboardRef.current = {
type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })),
duration: (item.duration || 4) * (60.0 / bpmVal),
name: item.name || 'MIDI Item',
color: item.color || '#a855f7'
};
window.globalStudioClipboard = clipboardRef.current;
closeContextMenu();
showToast(`Đã sao chép MIDI item "${item.name || ''}".`, 'info');
return;
}
}
if (contextMenu.itemType === 'clip' && contextMenu.itemId) {
const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.clips || []).find(c => c.id === contextMenu.itemId);
if (item && item.buffer) {
clipboardRef.current = {
buffer: item.buffer,
name: item.name || trk.name,
volumeDb: trk.volumeDb,
pan: trk.pan,
color: item.color || trk.color,
sampleRate: item.buffer.sampleRate,
channels: item.buffer.numberOfChannels,
speed: item.speed || 1.0
};
window.globalStudioClipboard = clipboardRef.current;
closeContextMenu();
showToast(`Đã sao chép clip "${item.name || ''}".`, 'info');
return;
}
}
const track = activeTracks.find(t => t.id === contextMenu.trackId);
if (!track || !track.buffer) return;
const sr = track.buffer.sampleRate;
@@ -17567,6 +17644,54 @@ const App = () => {
showToast('Đã sao chép toàn bộ track.', 'info');
};
const contextMenuCut = () => {
// Context menu trên ITEM cut ĐÚNG item (copy + xóa item trưc đây
// cut nhm track buffer/audio user 07:25).
if (contextMenu.itemType === 'midi' && contextMenu.itemId) {
const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.midiItems || []).find(m => m.id === contextMenu.itemId);
if (item) {
const bpmVal = parseInt(bpm) || 120;
clipboardRef.current = {
type: 'midi',
notes: (item.notes || []).map(n => ({ ...n })),
duration: (item.duration || 4) * (60.0 / bpmVal),
name: item.name || 'MIDI Item',
color: item.color || '#a855f7'
};
window.globalStudioClipboard = clipboardRef.current;
updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? {
...t,
midiItems: (t.midiItems || []).filter(m => m.id !== contextMenu.itemId)
} : t));
closeContextMenu();
showToast(`Đã cắt MIDI item "${item.name || ''}".`, 'info');
return;
}
}
if (contextMenu.itemType === 'clip' && contextMenu.itemId) {
const trk = activeTracks.find(t => t.id === contextMenu.trackId);
const item = trk && (trk.clips || []).find(c => c.id === contextMenu.itemId);
if (item && item.buffer) {
clipboardRef.current = {
buffer: item.buffer,
name: item.name || trk.name,
volumeDb: trk.volumeDb,
pan: trk.pan,
color: item.color || trk.color,
sampleRate: item.buffer.sampleRate,
channels: item.buffer.numberOfChannels,
speed: item.speed || 1.0
};
window.globalStudioClipboard = clipboardRef.current;
updateActiveTracks(prev => prev.map(t => t.id === contextMenu.trackId ? {
...t,
clips: (t.clips || []).filter(c => c.id !== contextMenu.itemId)
} : t));
closeContextMenu();
showToast(`Đã cắt clip "${item.name || ''}".`, 'info');
return;
}
}
const t = tracks.find(x => x.id === contextMenu.trackId);
if (!t || !t.buffer) {
contextMenuDelete();
@@ -19393,7 +19518,8 @@ const App = () => {
// sau khi âm đã dng user bug 05:45).
if (!isPlayingRef.current) return;
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
// VU key theo CONTEXT (tách MAIN vs SECTION user 07:20)
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
}
}, delay * 1000);
} else {
@@ -19411,7 +19537,7 @@ const App = () => {
);
// Trigger VU meter flash instantly
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
}
}
}
@@ -19533,8 +19659,9 @@ const App = () => {
// nhy sau khi âm đã dng user bug 05:45).
if (!isPlayingRef.current) return;
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8);
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
// VU key theo CONTEXT (tách MAIN vs SECTION 07:20)
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8);
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
}
}, delay * 1000);
} else {
@@ -19550,8 +19677,9 @@ const App = () => {
subTrack.synth_engine
);
if (window.triggerMidiVuActivity) {
window.triggerMidiVuActivity(subTrack.id, note.velocity || 0.8);
window.triggerMidiVuActivity(track.id, note.velocity || 0.8);
// VU key theo CONTEXT (tách MAIN vs SECTION 07:20)
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + subTrack.id, note.velocity || 0.8);
window.triggerMidiVuActivity((activeTab && activeTab.startsWith('session_') ? '_sess_' : '') + track.id, note.velocity || 0.8);
}
}
}
@@ -19832,6 +19960,10 @@ const App = () => {
if (n.fxStopFn) { try { n.fxStopFn(); } catch (e) {} }
});
activeTrackNodesRef.current = {};
// Clear VU activity NGAY: hết âm VU tt tc thì (không decay 0.35s t
// âm cũ user 06:45: tt âm MAIN items khi vào SECTION-TAB VU main
// items phi tt theo, không "din" tiếp).
try { midiVuActivityRef.current = {}; } catch (e) { }
stopMidiCapture();
if (window.SonicSF) {
try { window.SonicSF.stopAll(); } catch (e) { console.warn('[Stop] stopAll error:', e); }
@@ -25002,7 +25134,15 @@ STRICT CONSTRAINTS:
const trackNodes = activeTrackNodesRef.current || {};
const activeKeys = Object.keys(trackVuRefs.current);
activeKeys.forEach(key => {
const trackId = key.replace('_mixer', '');
// Key phân bit context (user 07:20 tách VU MAIN vs SECTION-TAB):
// - '1' / '1_mixer' MAIN track 1
// - '_sess_1' SECTION-TAB track 1 (clone id trùng main)
// trackId = id thun (node lookup); vuKey = key midiVuActivityRef
// (context riêng trigger fire theo context hết ln nhau).
const trackIdRaw = key.replace('_mixer', '');
const isSessVu = trackIdRaw.indexOf('_sess_') === 0;
const trackId = isSessVu ? trackIdRaw.slice(6) : trackIdRaw;
const vuKey = trackIdRaw;
const isRecordingThisTrack = activeAudioRecordersRef.current && activeAudioRecordersRef.current[trackId];
if (isRecordingThisTrack) return;
@@ -25051,68 +25191,40 @@ STRICT CONSTRAINTS:
}
}
// MIDI THC: đc t SF output (âm post-FluidSynth) thay velocity
// trigger (GI fire lúc schedule click playhead re-schedule
// VU nhy dù âm không ra log 06:00: midi=0.98 nhưng master=0.008).
// Track MIDI đi SF masterBus (không qua node analyser) tap ti SF
// output gain.
let sfAudioPeak = 0;
let _sfTapped = false;
if (audioPeak <= 0.001) {
try {
if (window.SonicSF && window.SonicSF.getOutputNode) {
const sfNode = window.SonicSF.getOutputNode();
if (sfNode) {
if (!sfNode.__vuAnalyser) {
const _sa = getAudioContext().createAnalyser();
_sa.fftSize = 256;
_sa.smoothingTimeConstant = 0.2;
sfNode.connect(_sa);
sfNode.__vuAnalyser = _sa;
}
_sfTapped = true;
const d3 = new Uint8Array(128);
sfNode.__vuAnalyser.getByteTimeDomainData(d3);
for (let i = 0; i < d3.length; i++) {
const v = Math.abs(d3[i] - 128) / 128;
if (v > sfAudioPeak) sfAudioPeak = v;
}
}
}
} catch (e) { }
}
// midiVuActivityRef đưc set bi triggerMidiVuActivity c khi play
// MIDI item LN khi ARM + nhn phím MIDI keyboard (preview) nên VU
// nhy trong c 2 trưng hp (không chn bi isPlaying).
let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
// SF analyser đã tap CH dùng âm THC (velocity trigger = gi b)
if (_sfTapped) midiPeak = 0;
// midiVuActivityRef đưc set bi triggerMidiVuActivity fire ĐÚNG NHP
// note (nhánh delayed: setTimeout khp thi đim phát; nhánh instant:
// note đang kêu). KHÔNG gate theo amplitude SF gate chn note đơn /
// velocity thp (âm SF nh < 0.03) VU không nhy (user bug 07:10).
let midiPeak = isAudible ? (midiVuActivityRef.current[vuKey] || 0) : 0;
if (midiPeak > 0) {
// Decay nhanh hơn (0.72/frame tt sau ~0.35s) hết note VU
// dng nhanh, không "vn din animation" khi không còn âm.
midiVuActivityRef.current[trackId] = midiPeak * 0.72;
if (midiVuActivityRef.current[trackId] < 0.01) {
midiVuActivityRef.current[trackId] = 0;
// Decay 0.75 (~0.5s) cân bng: note đơn/velocity thp hin th rõ
// NHƯNG tt nhanh sau hết note (decay 0.85 ~1s quá lâu user:
// "vn din animation" sau khi âm hết bug 07:15).
midiVuActivityRef.current[vuKey] = midiPeak * 0.75;
if (midiVuActivityRef.current[vuKey] < 0.01) {
midiVuActivityRef.current[vuKey] = 0;
}
}
// Gp âm MIDI THC vào audioPeak
if (sfAudioPeak > audioPeak) audioPeak = sfAudioPeak;
// Deadband: b noise nn analyser (log 06:05 audio=0.008 dù không
// âm VU hin th đon nh "v nhưng không animation") hết âm
// VU tt NGAY LP TC (user yêu cu).
if (audioPeak < 0.01) audioPeak = 0;
// KHÔNG gp sfAudioPeak vào audioPeak: SF là 1 output CHUNG cho mi
// track MIDI gp làm track VU nhy CÙNG NHAU (user bug 07:00).
// audioPeak ch theo âm TRACK tht (vNode/sub-node analyser).
// Deadband: b noise nn analyser (log 07:15 vNode noise dao đng
// quanh 0.04 track KHÔNG item (track 2 section) vn nhy qua deadband
// 0.04) hết âm VU tt NGAY LP TC (user yêu cu).
if (audioPeak < 0.05) audioPeak = 0;
const peak = Math.max(audioPeak, midiPeak);
const db = peak > 0 ? 20 * Math.log10(peak) : -60;
// Log chn đoán VU: CH in khi peak > 0 (VU đang nhy tht) xác đnh
// ngun nào còn nhy khi playhead ngoài vùng âm (user bug 06:00).
if (peak > 0.001) {
try {
console.log('[VU]', trackId, 'audio=', audioPeak.toFixed(3), 'midi=', midiPeak.toFixed(3), 'key=', key);
} catch (e) { }
}
// Log chn đoán VU in MI key (k c peak 0, rate ~1.3/giây) xác
// đnh track nào + ngun (audio/midi) khi VU din sai (user bug 07:15:
// track 2 section không item vn nhy).
try {
const _vf = (window.__vuFrame = (window.__vuFrame || 0) + 1);
if (_vf % 45 === 0) {
console.log('[VU]', trackId, 'audio=', audioPeak.toFixed(3), 'midi=', midiPeak.toFixed(3), 'key=', key, 'peak=', (Math.max(audioPeak, midiPeak)).toFixed(3));
}
} catch (e) { }
if (peak > 0.001) {
if (key.endsWith('_mixer')) {
@@ -26977,7 +27089,11 @@ STRICT CONSTRAINTS:
className: "text-zinc-500 text-[9px]"
}, "VU"), /*#__PURE__*/React.createElement("canvas", {
ref: el => {
if (el) trackVuRefs.current[track.id] = el; else delete trackVuRefs.current[track.id];
// TÁCH VU MAIN vs SECTION-TAB (user 07:20): section clone id TRÙNG
// main id key canvas trùng VU main/section ln nhau. Section
// strip dùng key '_sess_<id>' VU tick parse theo key.
const _vuK = track._isSectionClone ? '_sess_' + track.id : track.id;
if (el) trackVuRefs.current[_vuK] = el; else delete trackVuRefs.current[_vuK];
},
width: 100,
height: 4,
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -24,7 +24,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608070630" defer></script>
<script src="/static/js/app.precompiled.js?v=202608070725" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {
+76
View File
@@ -2618,3 +2618,79 @@
- **FIX (soundfontPlayer.js):** `setOutputDestination` — sau `_gainNode.connect(dest)``if (_gainNode.__vuAnalyser) _gainNode.connect(_gainNode.__vuAnalyser);` — analyser sống → sfAudioPeak đọc âm MIDI THỰC → VU nhảy khi có âm ✓; velocity giả đã bỏ (midiPeak=0 khi tapped) → hết nhảy giả khi click playhead vị trí không âm ✓.
- **Các file ảnh hưởng:** `app/static/js/services/soundfontPlayer.js` (bump ?v=202608070635 trong index.html), `wiki.md`. (app.precompiled giữ 0630 — không đổi logic app.jsx).
- **Ghi chú/Test:** hard refresh → (1) play MIDI track → VU nhảy theo âm thực; (2) click playhead tới vị trí không âm → VU tắt ngay; (3) mở piano roll tab → âm to không lag.
### [2026-08-07 06:40] Task: Vào SECTION-TAB lúc MAIN play → CHỈ play section items (tắt âm MAIN items)
- **Yêu cầu user:** play MAIN → di chuyển sang SECTION-TAB → ONLY play items của SECTION-TAB — tắt âm thanh của MAIN items.
- **FIX (app.jsx effect đổi tab 14814):** prevTab='main' → activeTab=`session_*` + đang play (`isPlaying`) → `stopAllPlayback()` (dừng MAIN items) + `startTrackPlayback(_pt)` với activeTracks = section clones (activeTab mới) — play CHỈ section tab items. `_pt` = vị trí play chuyển sang LOCAL của tab (trừ `secStart` — vị trí section trên main — tìm qua `tracks[].sections[]` bằng sectionId) — section items dùng time local 0-based.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070640), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (có main items + section item) → vào SECTION-TAB → CHỈ nghe section items (âm main items tắt — log `[SectionTab] play CHỈ section items tại local X.XX (secStart Y.YY)`); playhead section chạy từ vị trí tương ứng.
### [2026-08-07 06:45] Task: Tắt âm MAIN items (vào SECTION-TAB) → VU phải tắt theo
- **Yêu cầu user:** 0640 đã tắt âm MAIN items khi vào SECTION-TAB ✓ — NHƯNG VU animation của âm items đó VẪN diễn — cần tắt VU.
- **Nguyên nhân:** `midiVuActivityRef` (velocity trigger) còn giá trị cũ — decay 0.72/frame (~0.35s) sau stop → VU main items "diễn" tiếp dù âm đã tắt.
- **FIX (app.jsx stopAllPlayback):** clear `midiVuActivityRef.current = {}` NGAY khi stop — VU tắt tức thì (không decay từ âm cũ). VU sau đó chỉ theo âm THỰC (SF analyser — fix 0635): tab items phát → VU nhảy; im → VU 0.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070645), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN (âm main items) → vào SECTION-TAB → âm main tắt + VU main tắt NGAY (không diễn tiếp); tab items phát → VU tab nhảy theo âm thực.
### [2026-08-07 06:50] Task: VU track nhảy nhẹ (0.016/0.023) khi không âm — noise SF analyser
- **Log user (0645):** track 1 + 2 `audio= 0.016/0.023` (CÙNG giá trị — sfAudioPeak — SF analyser noise nền) nhảy nhẹ dù không âm track (master 0.25 = âm thật clip — track VU không liên quan); deadband 0.01 KHÔNG chặn 0.016.
- **FIX (app.jsx):** deadband track `audioPeak < 0.02 → 0` (nâng từ 0.01) — chặn noise SF 0.016/0.023 — VU tắt khi âm < 0.02 (≈ -34dB — âm thật thường lớn hơn).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070650), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play → vị trí không âm → VU track KHÔNG nhảy (0.016 bị chặn); có âm thật (> 0.02) → VU nhảy; master VU giữ deadband 0.01 (âm thật nhỏ vẫn hiển thị).
### [2026-08-07 06:55] Task: VU track = noise (0.023) khi âm MIDI thật 0.25 — guard SF block skip
- **Log user (0650 — SECTION-TAB):** track 1+2 `audio= 0.023/0.031` (noise vNode analyser) TRONG KHI `[VU-master] peak= 0.25` (âm MIDI thật phát — noteon) — VU track không theo âm thật.
- **Nguyên nhân:** guard `if (audioPeak <= 0.001)` quanh block đọc SF analyser — vNode noise 0.023 > 0.001 → block SF SKIP → sfAudioPeak = 0 → VU = noise vNode 0.023 (nhảy nhẹ mãi) — âm MIDI thật (SF → masterBus 0.25) không được đọc.
- **FIX (app.jsx):** (1) LUÔN đọc SF analyser (bỏ guard — sfAudioPeak gộp max vào audioPeak) — VU MIDI theo âm thật; (2) deadband `0.02 → 0.03` — chặn noise 0.023-0.031.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070655), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuyển SECTION-TAB (play) → VU track theo âm thật (MIDI 0.25 — nhảy đúng); vị trí không âm → VU tắt (noise 0.023 bị deadband 0.03 chặn).
### [2026-08-07 07:00] Task: Track VU nhảy CÙNG NHAU — bỏ sfAudioPeak gộp chung
- **Log user (0655):** track 1 + 2 `audio= 0.031/0.039` — CÙNG GIÁ TRỊ — nhảy cùng nhau. User muốn: track CÓ âm → nhảy; KHÔNG âm → không (PHÂN BIỆT track).
- **Nguyên nhân:** sfAudioPeak (SF = 1 output CHUNG cho mọi track MIDI) gộp vào audioPeak mọi track → track VU cùng giá trị/nhảy cùng nhau; track audio cũng bị SF chi phối.
- **FIX (app.jsx VU tick):** (1) KHÔNG gộp sfAudioPeak vào audioPeak — audioPeak chỉ theo âm TRACK thật (vNode/sub-node analyser — clip audio phân biệt); (2) MIDI track VU = velocity trigger (midiVuActivity — PHÂN BIỆT track — track nào có note → nhảy) + GATE theo âm SF THỰC (sfAudioPeak < 0.03 → midiPeak = 0 — SF im = hết note/click playhead vị trí không âm → VU tắt ngay — hết nhảy giả).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070700), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play SECTION-TAB → track MIDI (note) nhảy theo velocity khi SF phát; track audio (clip) nhảy theo analyser riêng; track không âm → đứng yên (KHÔNG cùng nhau); click playhead vị trí không âm → VU tắt ngay (SF im → gate).
### [2026-08-07 07:05] Task: Track không item VU vẫn nhảy + VU không đúng nhịp
- **Log user (0700):** [VU] 1 midi= velocity (0.66→1.0 — gate SF mở ✓); track 2 không log (VU 0 ✓). 2 vấn đề: (1) track KHÔNG item vẫn nhảy nhẹ — noise vNode 0.031-0.039 qua deadband 0.03; (2) velocity decay 0.72/frame quá nhanh — VU nhấp nháy không theo âm (sustain) — "không đúng nhịp".
- **FIX (app.jsx VU tick):**
(1) Deadband audioPeak `0.03 → 0.04` — chặn noise 0.031-0.039 — track không item → VU đứng yên.
(2) MIDI VU: bỏ decay nhanh — GATE + GIỮ theo âm SF thực: SF đang phát (sfAudioPeak ≥ 0.03) → midiPeak = velocity (VU theo note, giữ trong lúc âm — đúng nhịp); SF im → midiPeak = 0 + clear midiVuActivityRef (VU tắt ngay).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070705), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play SECTION-TAB → track có note → VU sáng theo note + giữ khi âm kêu (đúng nhịp); track không item → đứng yên; hết âm/click playhead vị trí không âm → VU tắt ngay.
### [2026-08-07 07:10] Task: Note đơn/velocity thấp không có VU — bỏ gate amplitude SF
- **Báo cáo user:** lọc noise quá khắt khe — play note đơn (velocity thấp) → KHÔNG có VU meter (âm SF nhỏ < gate 0.03 → midiPeak = 0).
- **FIX (app.jsx VU tick):**
(1) BỎ gate amplitude SF (sfAudioPeak < 0.03 → midiPeak = 0) — MIDI VU = velocity trigger (fire ĐÚNG NHỊP note: nhánh delayed setTimeout + nhánh instant) — note đơn/velocity thấp → VU nhảy theo velocity ✓.
(2) Decay chậm 0.85 (~1s) — note đơn hiển thị rõ, không nhấp nháy quá nhanh (đúng nhịp âm).
(3) Bỏ luôn block đọc SF analyser (không còn dùng — tiết kiệm CPU/frame).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070710), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play note đơn (velocity thấp) → VU NHẢY (theo velocity); nhóm note → VU nhảy; không âm (vị trí im) → VU tắt sau ~1s; track không item → đứng yên (deadband audioPeak 0.04 — chỉ chặn noise track, không ảnh hưởng MIDI velocity).
### [2026-08-07 07:15] Task: Track 2 section (không item) VU vẫn nhảy + decay quá lâu
- **Báo cáo user:** MAIN — track 1 section item, track 2 midi item. Vào SECTION-TAB — track 1 có midi item, track 2 (cùng duration) KHÔNG item — NHƯNG VU track 2 vẫn diễn; mức diễn không đúng velocity/decay.
- **Nguyên nhân:** (1) noise vNode analyser dao động quanh 0.04 — qua deadband 0.04 → track không item VU nhảy nhẹ (không theo velocity — lung tung); (2) decay 0.85 (~1s) quá lâu — sau hết âm VU "vẫn diễn" ~1s.
- **FIX (app.jsx VU tick):** (1) deadband audioPeak `0.04 → 0.05` — chặn noise quanh 0.04 — track không item đứng yên; (2) decay `0.85 → 0.75` (~0.5s) — note đơn thấy rõ nhưng tắt nhanh sau hết note; (3) log `[VU]` in MỌI key (kể cả peak 0 — rate ~1.3/s — kèm key/peak) — xác nhận nguồn.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070715), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → play MAIN → vào SECTION-TAB → track 2 (không item) VU ĐỨNG YÊN; track 1 VU theo velocity + tắt ~0.5s sau hết note; nếu track 2 VẪN nhảy → dán log `[VU]` (audio= bao nhiêu — noise hay midi= còn giá trị).
### [2026-08-07 07:20] Task: TÁCH VU meter MAIN SESSION vs SECTION-TAB (item tab nào → VU tab đó)
- **Yêu cầu user:** tách biệt VU meter MAIN vs SECTION-TAB — item của tab nào thì play VU của tab đó (gốc rễ: section clone id TRÙNG main id → VU canvas/trigger key lẫn nhau).
- **FIX (app.jsx):**
(1) Track strip VU canvas — section clone (`track._isSectionClone`) dùng key `'_sess_' + track.id` (main giữ `track.id`).
(2) VU tick — parse key: `_sess_<id>` → trackId = id thuần (node lookup), vuKey = key đầy đủ (midiVuActivityRef theo context).
(3) Trigger MIDI (startTrackPlayback — 4 chỗ main/section clones): fire key theo CONTEXT — activeTab session → `'_sess_' + id`; main → id thuần — mixer/main strip (key id) không nhận trigger section; section strip (key _sess_<id>) chỉ nhận trigger section.
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070720), `wiki.md`. Rebuild precompiled (build PASS — _sess_ ×8).
- **Ghi chú/Test:** hard refresh → MAIN play (main items) → VU main nhảy; vào SECTION-TAB (section items play) → VU section nhảy — VU main (mixer) ĐỨNG (không nhận trigger section); track section không item → đứng yên.
### [2026-08-07 07:25] Task: Cut/Copy MIDI item (context menu) → paste ra audio item (SAI)
- **Báo cáo user:** Cut MIDI item bằng context menu → paste ra AUDIO item (track có midi item vị trí 1 + audio item vị trí 2) — copy nhầm.
- **Nguyên nhân:** context menu chỉ biết TRACK (contextMenu = {trackId, time, sectionId}) — Copy/Cut (17542/17593) chỉ xử lý track buffer (audio) → cut midi item → copy nhầm buffer track → paste = audio item.
- **FIX (app.jsx):**
(1) TimelineTrack onContextMenu — detect ITEM dưới chuột: MIDI item (midiItems + duration bounds) + clip (clips + buffer) → truyền `itemId` + `itemType` ('midi'/'clip').
(2) handleContextMenu — nhận itemId/itemType → lưu vào contextMenu state.
(3) contextMenuCopy/Cut — xử lý ITEM TRƯỚC track: MIDI → clipboard {type:'midi', notes, duration(sec), name, color} — paste = MIDI item (doPaste nhánh midi đã có); clip → clipboard {buffer,...}; Cut = copy + XÓA item (updateActiveTracks filter).
- **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/templates/index.html` (?v=202608070725), `wiki.md`. Rebuild precompiled (build PASS).
- **Ghi chú/Test:** hard refresh → chuột phải MIDI item → Cut → Paste → ra MIDI ITEM (không phải audio); chuột phải clip → Copy → Paste → clip; chuột phải track (không item) → Copy → track buffer như cũ.