FIX: sửa lỗi copy/cut paste không đúng item
This commit is contained in:
+182
-66
@@ -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 chuột (user 07:25 — Cut/Copy item phải copy
|
||||
// ĐÚNG item — trước đây context menu chỉ biết track → copy nhầm 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 chuột
|
||||
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 mới hoạt động bình thường; MAIN giữ hành vi cũ (mở piano-roll lúc
|
||||
// main play → main tiếp tục — handleEditMidiInTab đã xử lý).
|
||||
if (prevTab !== activeTab) {
|
||||
// Vào SECTION-TAB lúc MAIN đang play → CHỈ play items CỦA SECTION-TAB,
|
||||
// TẮT âm của MAIN items (user yêu cầu 06:40): restart play với
|
||||
// activeTracks = section clones (activeTab mới → activeTracksRef mới).
|
||||
if (prevTab === 'main' && activeTab && activeTab.startsWith('session_') && isPlaying) {
|
||||
try {
|
||||
const _ctx = getAudioContext();
|
||||
// Vị trí play → LOCAL của 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 rời 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
|
||||
// nhầm 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 nhầm 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 đã dừng — 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 = () => {
|
||||
// nhảy sau khi âm đã dừng — 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 tắt tức thì (không decay 0.35s từ
|
||||
// âm cũ — user 06:45: tắt âm MAIN items khi vào SECTION-TAB → VU main
|
||||
// items phải tắt theo, không "diễn" 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 biệt 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 thuần (node lookup); vuKey = key midiVuActivityRef
|
||||
// (context riêng — trigger fire theo context — hết lẫn 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 THỰC: đọc từ SF output (âm post-FluidSynth) — thay velocity
|
||||
// trigger (GIẢ — fire lúc schedule — click playhead → re-schedule →
|
||||
// VU nhảy 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 tại 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 bởi triggerMidiVuActivity — cả khi play
|
||||
// MIDI item LẪN khi ARM + nhấn phím MIDI keyboard (preview) — nên VU
|
||||
// nhảy trong cả 2 trường hợp (không chặn bởi isPlaying).
|
||||
let midiPeak = isAudible ? (midiVuActivityRef.current[trackId] || 0) : 0;
|
||||
// SF analyser đã tap → CHỈ dùng âm THỰC (velocity trigger = giả — bỏ)
|
||||
if (_sfTapped) midiPeak = 0;
|
||||
// midiVuActivityRef được set bởi triggerMidiVuActivity — fire ĐÚNG NHỊP
|
||||
// note (nhánh delayed: setTimeout khớp thời điểm phát; nhánh instant:
|
||||
// note đang kêu). KHÔNG gate theo amplitude SF — gate chặn note đơn /
|
||||
// velocity thấp (âm SF nhỏ < 0.03) → VU không nhảy (user bug 07:10).
|
||||
let midiPeak = isAudible ? (midiVuActivityRef.current[vuKey] || 0) : 0;
|
||||
if (midiPeak > 0) {
|
||||
// Decay nhanh hơn (0.72/frame ≈ tắt sau ~0.35s) — hết note → VU
|
||||
// dừng nhanh, không "vẫn diễn 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 bằng: note đơn/velocity thấp hiển thị rõ
|
||||
// NHƯNG tắt nhanh sau hết note (decay 0.85 ~1s quá lâu — user:
|
||||
// "vẫn diễn 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;
|
||||
}
|
||||
}
|
||||
// Gộp âm MIDI THỰC vào audioPeak
|
||||
if (sfAudioPeak > audioPeak) audioPeak = sfAudioPeak;
|
||||
// Deadband: bỏ noise nền analyser (log 06:05 — audio=0.008 dù không
|
||||
// âm → VU hiển thị đoạn nhỏ "vẽ nhưng không animation") — hết âm →
|
||||
// VU tắt NGAY LẬP TỨC (user yêu cầu).
|
||||
if (audioPeak < 0.01) audioPeak = 0;
|
||||
// ⚠️ KHÔNG gộp sfAudioPeak vào audioPeak: SF là 1 output CHUNG cho mọi
|
||||
// track MIDI → gộp làm track VU nhảy CÙNG NHAU (user bug 07:00).
|
||||
// audioPeak chỉ theo âm TRACK thật (vNode/sub-node analyser).
|
||||
// Deadband: bỏ noise nền analyser (log 07:15 — vNode noise dao động
|
||||
// quanh 0.04 — track KHÔNG item (track 2 section) vẫn nhảy qua deadband
|
||||
// 0.04) — hết âm → VU tắt NGAY LẬP TỨC (user yêu cầu).
|
||||
if (audioPeak < 0.05) audioPeak = 0;
|
||||
|
||||
const peak = Math.max(audioPeak, midiPeak);
|
||||
const db = peak > 0 ? 20 * Math.log10(peak) : -60;
|
||||
|
||||
// Log chẩn đoán VU: CHỈ in khi peak > 0 (VU đang nhảy thật) — xác định
|
||||
// nguồn nào còn nhảy 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 chẩn đoán VU — in MỌI key (kể cả peak 0, rate ~1.3/giây) — xác
|
||||
// định track nào + nguồn (audio/midi) khi VU diễn sai (user bug 07:15:
|
||||
// track 2 section không item vẫn nhảy).
|
||||
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 lẫn 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
Reference in New Issue
Block a user