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