fix: chỉnh sửa hiển thị của audio clip trong subtab

This commit is contained in:
2026-07-19 16:56:17 +07:00
parent 2894fae19b
commit e1b6f47ad0
+248 -117
View File
@@ -668,7 +668,7 @@
}; };
// ── Sub-Tab Waveform Component (LOOP_EDITOR_2.md §1.2 & SUB_EDITOR.md) ── // ── Sub-Tab Waveform Component (LOOP_EDITOR_2.md §1.2 & SUB_EDITOR.md) ──
const SubTabWaveform = ({ buffer, subTabId, activeTab, currentTime, selectionStart, selectionEnd, onSelectRange, onPlayheadSet, onContextMenu, activeTool, zoom, timelineWidth }) => { const SubTabWaveform = ({ buffer, subTabId, activeTab, currentTime, selectionStart, selectionEnd, onSelectRange, onPlayheadSet, onContextMenu, activeTool, zoom, timelineWidth, color, name }) => {
const canvasRef = useRef(null); const canvasRef = useRef(null);
useEffect(() => { useEffect(() => {
const canvas = canvasRef.current; const canvas = canvasRef.current;
@@ -689,10 +689,33 @@
const len = data.length; const len = data.length;
if (len === 0) return; if (len === 0) return;
// Draw waveform only in viewport range for performance & zero-line accuracy // Draw clip container (like main session clips)
const xStart = 0; const xStart = 0;
const wClip = buffer.duration * zoom; const wClip = buffer.duration * zoom;
const xEnd = xStart + wClip; const xEnd = xStart + wClip;
const clipTop = 8;
const clipHeight = h - 16;
const clipColor = color || '#06b6d4';
ctx.fillStyle = clipColor + '22';
ctx.strokeStyle = clipColor;
ctx.lineWidth = 1.5;
if (ctx.roundRect) {
ctx.beginPath();
ctx.roundRect(xStart, clipTop, wClip, clipHeight, 4);
ctx.fill();
ctx.stroke();
} else {
ctx.fillRect(xStart, clipTop, wClip, clipHeight);
ctx.strokeRect(xStart, clipTop, wClip, clipHeight);
}
// Draw clip label
ctx.fillStyle = '#e4e4e7';
ctx.font = 'bold 10px sans-serif';
ctx.fillText(name || 'Audio Clip', xStart + 8, clipTop + 14);
// Draw waveform inside clip
const drawXStart = Math.max(0, Math.floor(xStart)); const drawXStart = Math.max(0, Math.floor(xStart));
const drawXEnd = Math.min(w, Math.ceil(xEnd)); const drawXEnd = Math.min(w, Math.ceil(xEnd));
const samplesPerPixel = buffer.sampleRate / zoom; const samplesPerPixel = buffer.sampleRate / zoom;
@@ -715,8 +738,8 @@
if (val > maxVal) maxVal = val; if (val > maxVal) maxVal = val;
if (val < minVal) minVal = val; if (val < minVal) minVal = val;
} }
const yTop = mid + (minVal * (h * 0.45)); const yTop = mid + (minVal * (clipHeight * 0.45));
const yBottom = mid + (maxVal * (h * 0.45)); const yBottom = mid + (maxVal * (clipHeight * 0.45));
ctx.beginPath(); ctx.beginPath();
ctx.moveTo(px, yTop); ctx.moveTo(px, yTop);
ctx.lineTo(px, yBottom); ctx.lineTo(px, yBottom);
@@ -750,24 +773,27 @@
ctx.stroke(); ctx.stroke();
} }
}, [buffer, currentTime, selectionStart, selectionEnd, zoom, timelineWidth]); }, [buffer, currentTime, selectionStart, selectionEnd, zoom, timelineWidth, color, name]);
const handleMouseDown = (e) => { const handleMouseDown = (e) => {
if (e.button === 2) return; // ignore right click if (e.button === 2) return; // ignore right click
const rect = canvasRef.current.getBoundingClientRect(); const canvas = canvasRef.current;
const startX = e.clientX - rect.left; const rect = canvas.getBoundingClientRect();
const parent = canvas.parentElement;
const scrollContainer = parent ? parent.parentElement : null;
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
const duration = buffer.duration; const duration = buffer.duration;
const startTime = (startX / rect.width) * duration; const mouseX = e.clientX - rect.left + scrollLeft;
const startTime = Math.max(0, Math.min(duration, mouseX / zoom));
// Tool-specific behavior // Tool-specific behavior
if (activeTool === 'grab') { if (activeTool === 'grab') {
// Grab tool: move playhead on click/drag
onPlayheadSet(startTime); onPlayheadSet(startTime);
const handleMouseMove = (moveEvent) => { const handleMouseMove = (moveEvent) => {
const currentX = moveEvent.clientX - rect.left; const currentX = moveEvent.clientX - rect.left + scrollLeft;
const currentTime = Math.max(0, Math.min(duration, (currentX / rect.width) * duration)); const ct = Math.max(0, Math.min(duration, currentX / zoom));
onPlayheadSet(currentTime); onPlayheadSet(ct);
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
@@ -781,29 +807,25 @@
} }
if (activeTool === 'razor') { if (activeTool === 'razor') {
// Razor tool: add a cut marker at click position
// For sub-tab, we can store cut points in the subTab state
onPlayheadSet(startTime); onPlayheadSet(startTime);
showToast(`Cut point at ${formatTime(startTime)}`, 'info'); showToast(`Cut point at ${formatTime(startTime)}`, 'info');
return; return;
} }
if (activeTool === 'pen') { if (activeTool === 'pen') {
// Pen tool: for volume automation drawing
// For now, just move playhead and show indicator
onPlayheadSet(startTime); onPlayheadSet(startTime);
canvasRef.current.style.cursor = 'crosshair'; canvas.style.cursor = 'crosshair';
const handleMouseMove = (moveEvent) => { const handleMouseMove = (moveEvent) => {
const currentX = moveEvent.clientX - rect.left; const currentX = moveEvent.clientX - rect.left + scrollLeft;
const currentTime = Math.max(0, Math.min(duration, (currentX / rect.width) * duration)); const ct = Math.max(0, Math.min(duration, currentX / zoom));
onPlayheadSet(currentTime); onPlayheadSet(ct);
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('mouseup', handleMouseUp);
canvasRef.current.style.cursor = 'crosshair'; canvas.style.cursor = 'crosshair';
}; };
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
@@ -816,9 +838,9 @@
onPlayheadSet(startTime); onPlayheadSet(startTime);
const handleMouseMove = (moveEvent) => { const handleMouseMove = (moveEvent) => {
const currentX = moveEvent.clientX - rect.left; const currentX = moveEvent.clientX - rect.left + scrollLeft;
const currentTime = Math.max(0, Math.min(duration, (currentX / rect.width) * duration)); const ct = Math.max(0, Math.min(duration, currentX / zoom));
onSelectRange(startTime, currentTime); onSelectRange(startTime, ct);
}; };
const handleMouseUp = () => { const handleMouseUp = () => {
@@ -833,16 +855,20 @@
const handleContextMenuInternal = (e) => { const handleContextMenuInternal = (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const rect = canvasRef.current.getBoundingClientRect(); const canvas = canvasRef.current;
const x = e.clientX - rect.left; const rect = canvas.getBoundingClientRect();
const clickTime = (x / rect.width) * buffer.duration; const parent = canvas.parentElement;
const scrollContainer = parent ? parent.parentElement : null;
const scrollLeft = scrollContainer ? scrollContainer.scrollLeft : 0;
const x = e.clientX - rect.left + scrollLeft;
const clickTime = Math.max(0, Math.min(buffer.duration, x / zoom));
onContextMenu(e, clickTime); onContextMenu(e, clickTime);
}; };
return ( return (
<canvas <canvas
ref={canvasRef} ref={canvasRef}
className="w-full h-48 rounded border border-zinc-800 cursor-crosshair" className="w-full h-full rounded border border-zinc-800 cursor-crosshair"
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onContextMenu={handleContextMenuInternal} onContextMenu={handleContextMenuInternal}
/> />
@@ -1031,6 +1057,7 @@
const [beginBar, setBeginBar] = useState(1); const [beginBar, setBeginBar] = useState(1);
const [endBar, setEndBar] = useState(1); const [endBar, setEndBar] = useState(1);
const [numberBar, setNumberBar] = useState(1); const [numberBar, setNumberBar] = useState(1);
const [subTabHeight, setSubTabHeight] = useState(96);
const [isExporting, setIsExporting] = useState(false); const [isExporting, setIsExporting] = useState(false);
const [soloedTrackId, setSoloedTrackId] = useState(null); const [soloedTrackId, setSoloedTrackId] = useState(null);
const [toastMessage, setToastMessage] = useState(null); const [toastMessage, setToastMessage] = useState(null);
@@ -1038,7 +1065,7 @@
const [showExportPanel, setShowExportPanel] = useState(true); const [showExportPanel, setShowExportPanel] = useState(true);
const [showAIPanel, setShowAIPanel] = useState(true); const [showAIPanel, setShowAIPanel] = useState(true);
const [showSelectionPanel, setShowSelectionPanel] = useState(true); const [showSelectionPanel, setShowSelectionPanel] = useState(true);
const [panelPositions, setPanelPositions] = useState({ export: 'bottom', ai: 'bottom', selection: 'right' }); const [panelPositions, setPanelPositions] = useState({ export: 'bottom', ai: 'bottom', selection: 'bottom' });
const [panelDropZone, setPanelDropZone] = useState(null); const [panelDropZone, setPanelDropZone] = useState(null);
const [dragGhostPos, setDragGhostPos] = useState(null); const [dragGhostPos, setDragGhostPos] = useState(null);
const [dragGhostPanel, setDragGhostPanel] = useState(null); const [dragGhostPanel, setDragGhostPanel] = useState(null);
@@ -1630,6 +1657,15 @@
showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.', 'warning'); showToast('Vui lòng chọn một khoảng thời gian trên sóng âm.', 'warning');
return; return;
} }
// Check if a subtab for this track+range already exists
const existing = subTabs.find(s => s.trackId === trackId && s.startTime === selLeft && s.endTime === selRight);
if (existing) {
setActiveTab(existing.id);
showToast(`Sub Tab already open.`, 'info');
return;
}
const sr = t.buffer.sampleRate; const sr = t.buffer.sampleRate;
const trackStart = t.startTime || 0; const trackStart = t.startTime || 0;
const relSelLeft = Math.max(0, selLeft - trackStart); const relSelLeft = Math.max(0, selLeft - trackStart);
@@ -1666,46 +1702,55 @@
showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info'); showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info');
}; };
const handleEditClipInSubTab = (trackId, clipId) => { const handleEditClipInSubTab = (trackId, clipId) => {
const track = tracks.find(t => t.id === trackId); const track = tracks.find(t => t.id === trackId);
if (!track) return; if (!track) return;
const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{
id: 'default_' + track.id, id: 'default_' + track.id,
buffer: track.buffer, buffer: track.buffer,
startTime: track.startTime || 0, startTime: track.startTime || 0,
name: track.name name: track.name
}] : []); }] : []);
const clip = clips.find(c => c.id === clipId || (clipId === 'default' && c.id === 'default_' + trackId)); const clip = clips.find(c => c.id === clipId || (clipId === 'default' && c.id === 'default_' + trackId));
if (!clip || !clip.buffer) return; if (!clip || !clip.buffer) return;
const sr = clip.buffer.sampleRate; const resolvedClipId = clip.id === 'default' ? 'default_' + trackId : clip.id;
const len = clip.buffer.length; // Check if a subtab for this clip already exists
const ctx = getAudioContext(); const existing = subTabs.find(s => s.clipId === resolvedClipId && s.trackId === trackId);
const subBuffer = ctx.createBuffer(1, len, sr); if (existing) {
subBuffer.copyToChannel(clip.buffer.getChannelData(0), 0); setActiveTab(existing.id);
showToast(`Sub Tab for "${clip.name}" already open.`, 'info');
return;
}
const tabId = 'subtab_' + Date.now(); const sr = clip.buffer.sampleRate;
const tabLabel = `Edit_${clip.name.replace('.wav','').slice(0,10)}`; const len = clip.buffer.length;
const ctx = getAudioContext();
const subBuffer = ctx.createBuffer(1, len, sr);
subBuffer.copyToChannel(clip.buffer.getChannelData(0), 0);
setSubTabs(prev => [...prev, { const tabId = 'subtab_' + Date.now();
id: tabId, const tabLabel = `Edit_${clip.name.replace('.wav','').slice(0,10)}`;
label: tabLabel,
trackId: trackId, setSubTabs(prev => [...prev, {
clipId: clip.id === 'default' ? 'default_' + trackId : clip.id, id: tabId,
startTime: clip.startTime, label: tabLabel,
endTime: clip.startTime + clip.buffer.duration, trackId: trackId,
buffer: subBuffer, clipId: resolvedClipId,
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0, normalizeDb: 0, pitch: 0, speedStretch: 100 }, startTime: clip.startTime,
currentTime: 0, endTime: clip.startTime + clip.buffer.duration,
selectionStart: null, buffer: subBuffer,
selectionEnd: null, effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0, normalizeDb: 0, pitch: 0, speedStretch: 100 },
isPlaying: false currentTime: 0,
}]); selectionStart: null,
setActiveTab(tabId); selectionEnd: null,
showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info'); isPlaying: false
}; }]);
setActiveTab(tabId);
showToast(`Đã tạo Sub Tab: ${tabLabel}. Chỉnh sửa và Apply để gộp lại.`, 'info');
};
// ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ── // ── Sub Tab: apply effects & merge back (LOOP_EDITOR_2.md §1.2) ──
const applySubTab = (tabId) => { const applySubTab = (tabId) => {
@@ -2710,6 +2755,27 @@
} }
}; };
const handleSubTabResizeMouseDown = (e) => {
e.preventDefault();
e.stopPropagation();
const startY = e.clientY;
const startHeight = subTabHeight;
const handleMouseMove = (moveEvent) => {
const deltaY = moveEvent.clientY - startY;
const newHeight = Math.max(48, Math.min(400, startHeight + deltaY));
setSubTabHeight(newHeight);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
// ── Selection ── // ── Selection ──
const clearLocalSelection = () => { const clearLocalSelection = () => {
setSelectionMode(null); setSelectionMode(null);
@@ -2718,36 +2784,38 @@
setLocalSelectionEnd(null); setLocalSelectionEnd(null);
}; };
const handleRulerMouseDown = (e) => { const handleRulerMouseDown = (e) => {
const wrapper = timelineWrapperRef.current; const wrapper = timelineWrapperRef.current;
if (!wrapper) return; if (!wrapper) return;
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const time = mouseX / zoom; const rawTime = mouseX / zoom;
const time = snapValue !== 'free' ? snapTime(rawTime, snapValue, bpm) : rawTime;
clearLocalSelection();
setSelectionMode('global'); clearLocalSelection();
rulerDragStartRef.current = time; setSelectionMode('global');
isDraggingRulerRef.current = true; rulerDragStartRef.current = time;
setSelectionStart(time); isDraggingRulerRef.current = true;
setSelectionEnd(time); setSelectionStart(time);
setCurrentTime(time); setSelectionEnd(time);
}; setCurrentTime(time);
};
// Global Ruler mousemove is tracked via document listener set up in useEffect // Global Ruler mousemove is tracked via document listener set up in useEffect
useEffect(() => { useEffect(() => {
const handleMouseMove = (e) => { const handleMouseMove = (e) => {
if (!isDraggingRulerRef.current) return; if (!isDraggingRulerRef.current) return;
const wrapper = timelineWrapperRef.current; const wrapper = timelineWrapperRef.current;
if (!wrapper) return; if (!wrapper) return;
const rect = wrapper.getBoundingClientRect(); const rect = wrapper.getBoundingClientRect();
const scrollLeft = wrapper.scrollLeft; const scrollLeft = wrapper.scrollLeft;
const mouseX = e.clientX - rect.left + scrollLeft; const mouseX = e.clientX - rect.left + scrollLeft;
const time = Math.max(0, Math.min(maxDuration, mouseX / zoom)); const rawTime = Math.max(0, Math.min(maxDuration, mouseX / zoom));
const time = snapValueRef.current !== 'free' ? snapTime(rawTime, snapValueRef.current, bpmRef.current) : rawTime;
setSelectionEnd(time);
}; setSelectionEnd(time);
};
const handleMouseUp = () => { const handleMouseUp = () => {
if (isDraggingRulerRef.current) { if (isDraggingRulerRef.current) {
isDraggingRulerRef.current = false; isDraggingRulerRef.current = false;
@@ -4302,41 +4370,70 @@
}`} title="Bật/Tắt Lặp vùng chọn"> }`} title="Bật/Tắt Lặp vùng chọn">
<i data-lucide="repeat" className="w-3.5 h-3.5"></i> <i data-lucide="repeat" className="w-3.5 h-3.5"></i>
</button> </button>
<span className="text-[9px] text-zinc-500 font-bold uppercase ml-2">Snap</span> <span className="text-[14px] text-zinc-500 font-bold uppercase ml-2">Snap</span>
<select value={snapValue} onChange={e => setSnapValue(e.target.value)} <select value={snapValue} onChange={e => setSnapValue(e.target.value)}
className="bg-black text-white text-[9px] px-1 py-0.5 rounded border border-zinc-700 focus:outline-none focus:border-cyan-500 font-mono cursor-pointer"> className="bg-black text-white text-[14px] px-1.5 py-0.5 rounded border border-zinc-700 focus:outline-none focus:border-cyan-500 font-mono cursor-pointer">
<option value="free">Free</option><option value="1">1</option> <option value="free">Free</option><option value="1">1</option>
<option value="1/2">1/2</option><option value="1/4">1/4</option> <option value="1/2">1/2</option><option value="1/4">1/4</option>
<option value="1/8">1/8</option><option value="1/16">1/16</option><option value="1/32">1/32</option> <option value="1/8">1/8</option><option value="1/16">1/16</option><option value="1/32">1/32</option>
</select> </select>
<div className="w-[1px] h-5 bg-zinc-800 mx-1"></div> <div className="w-[1px] h-6 bg-zinc-800 mx-1.5"></div>
<span className="text-[9px] text-zinc-500 font-bold">Bars:</span> <span className="text-[14px] text-zinc-500 font-bold">Bars:</span>
<input type="number" min="1" value={beginBar} onChange={e => { <input type="number" min="1" value={beginBar} onChange={e => {
const b = parseInt(e.target.value) || 1; const b = parseInt(e.target.value) || 1;
setBeginBar(b); setBeginBar(b);
const beatDuration = 60 / parseInt(bpm || 120); const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4; const t = (b - 1) * beatDuration * 4;
setSelectionStart(t); setSelectionEnd(Math.max(t, selRight || t)); clearLocalSelection();
}} className="w-10 bg-black text-white text-[9px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" /> setSelectionMode('global');
<span className="text-[9px] text-zinc-500">-</span> setSelectionStart(t);
setSelectionEnd(t + beatDuration * 4);
}} className="w-14 bg-black text-white text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
<span className="text-[14px] text-zinc-500">-</span>
<input type="number" min="1" value={endBar} onChange={e => { <input type="number" min="1" value={endBar} onChange={e => {
const b = parseInt(e.target.value) || 1; const b = parseInt(e.target.value) || 1;
setEndBar(b); setEndBar(b);
const beatDuration = 60 / parseInt(bpm || 120); const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4; const t = (b - 1) * beatDuration * 4;
setSelectionEnd(t); setSelectionEnd(t + beatDuration * 4);
setNumberBar(b - beginBar + 1); setNumberBar(b - beginBar + 1);
}} className="w-10 bg-black text-white text-[9px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" /> }} className="w-14 bg-black text-white text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
<span className="text-[9px] text-zinc-500">#</span> <span className="text-[14px] text-zinc-500">#</span>
<input type="number" min="1" value={numberBar} readOnly <input type="number" min="1" value={numberBar} readOnly
className="w-10 bg-black text-zinc-400 text-[9px] px-1 py-0.5 rounded border border-zinc-800 text-center font-mono" /> className="w-14 bg-black text-zinc-400 text-[14px] px-1 py-0.5 rounded border border-zinc-800 text-center font-mono" />
<div className="w-[1px] h-5 bg-zinc-800 mx-1"></div> <div className="w-[1px] h-6 bg-zinc-800 mx-1.5"></div>
<span className="text-[9px] text-zinc-500">Start:</span> <span className="text-[14px] text-zinc-500">Start:</span>
<span className="text-[9px] font-mono text-zinc-200 w-14">{selLeft !== null ? formatTime(selLeft) : '--:--'}</span> <input type="text" value={selLeft !== null ? formatTime(selLeft) : ''}
<span className="text-[9px] text-zinc-500">End:</span> onChange={e => {
<span className="text-[9px] font-mono text-zinc-200 w-14">{selRight !== null ? formatTime(selRight) : '--:--'}</span> const parts = e.target.value.split(/[:.]/);
<span className="text-[9px] text-zinc-500">Len:</span> if (parts.length === 3) {
<span className="text-[9px] font-mono text-amber-400 w-14">{selLeft !== null && selRight !== null ? formatTime(Math.abs(selRight - selLeft)) : '--:--'}</span> const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
clearLocalSelection();
setSelectionMode('global');
setSelectionStart(secs);
}
}}
className="w-20 bg-black text-zinc-200 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
<span className="text-[14px] text-zinc-500">End:</span>
<input type="text" value={selRight !== null ? formatTime(selRight) : ''}
onChange={e => {
const parts = e.target.value.split(/[:.]/);
if (parts.length === 3) {
const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
setSelectionEnd(secs);
}
}}
className="w-20 bg-black text-zinc-200 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
<span className="text-[14px] text-zinc-500">Len:</span>
<input type="text" value={selLeft !== null && selRight !== null ? formatTime(Math.abs(selRight - selLeft)) : ''}
onChange={e => {
const parts = e.target.value.split(/[:.]/);
if (parts.length === 3 && selLeft !== null) {
const secs = parseInt(parts[0]) * 60 + parseInt(parts[1]) + parseFloat('0.' + (parts[2] || '0'));
setSelectionEnd(selLeft + secs);
}
}}
className="w-20 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono" />
<div className="flex-1"></div> <div className="flex-1"></div>
<div className="text-xs font-bold font-mono text-zinc-100">{formatTime(currentTime)}</div> <div className="text-xs font-bold font-mono text-zinc-100">{formatTime(currentTime)}</div>
</div> </div>
@@ -4446,6 +4543,36 @@
<span className="text-zinc-200 font-mono text-[9px] font-semibold mt-0.5">{selectionStats.length}s</span> <span className="text-zinc-200 font-mono text-[9px] font-semibold mt-0.5">{selectionStats.length}s</span>
</div> </div>
</div> </div>
<div className="grid grid-cols-3 gap-1 bg-[#141414] p-1.5 rounded border border-zinc-800 mt-1">
<div>
<span className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">Begin Bar</span>
<input type="number" min="1" value={beginBar} onChange={e => {
const b = parseInt(e.target.value) || 1;
setBeginBar(b);
const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4;
clearLocalSelection();
setSelectionMode('global');
setSelectionStart(t);
setSelectionEnd(t + beatDuration * 4);
}} className="w-full bg-[#242424] text-white text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
</div>
<div>
<span className="block text-[7px] text-zinc-500 font-bold uppercase mb-0.5">End Bar</span>
<input type="number" min="1" value={endBar} onChange={e => {
const b = parseInt(e.target.value) || 1;
setEndBar(b);
const beatDuration = 60 / parseInt(bpm || 120);
const t = (b - 1) * beatDuration * 4;
setSelectionEnd(t + beatDuration * 4);
setNumberBar(b - beginBar + 1);
}} className="w-full bg-[#242424] text-white text-center font-mono text-[9px] rounded py-0.5 border border-zinc-700 focus:outline-none" />
</div>
<div className="flex flex-col justify-center">
<span className="text-[7px] text-zinc-500 font-bold uppercase"># Bars</span>
<span className="text-zinc-200 font-mono text-[9px] font-semibold mt-0.5">{numberBar}</span>
</div>
</div>
</div> </div>
); );
return null; return null;
@@ -4591,7 +4718,7 @@
<div ref={rulerRef} className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}> <div ref={rulerRef} className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}>
{Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => { {Array.from({ length: Math.ceil(maxDuration) }).map((_, i) => {
const sec = i; const x = sec * zoom; const sec = i; const x = sec * zoom;
return (<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>); return (<div key={i} className="absolute h-full border-l border-zinc-700 pl-1 pt-1 text-[14px] font-mono text-zinc-300 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
})} })}
</div> </div>
</div> </div>
@@ -4748,10 +4875,10 @@
<div ref={timelineWrapperRef} className="flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0"> <div ref={timelineWrapperRef} className="flex-1 relative z-10 overflow-x-auto overflow-y-auto min-w-0">
<div style={{ width: `${subTabTimelineWidth}px` }} className="relative flex flex-col min-h-full"> <div style={{ width: `${subTabTimelineWidth}px` }} className="relative flex flex-col min-h-full">
<div className="sticky top-0 z-45 flex h-10 border-b border-zinc-900 bg-[#242424] shrink-0"> <div className="sticky top-0 z-45 flex h-10 border-b border-zinc-900 bg-[#242424] shrink-0">
<div className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden"> <div className="flex-1 relative h-full flex items-center select-none cursor-ew-resize overflow-hidden" onMouseDown={handleRulerMouseDown}>
{Array.from({ length: Math.ceil(st.buffer ? st.buffer.duration : 0) }).map((_, i) => { {Array.from({ length: Math.ceil(st.buffer ? st.buffer.duration : 0) }).map((_, i) => {
const sec = i; const x = sec * zoom; const sec = i; const x = sec * zoom;
return (<div key={i} className="absolute h-full border-l border-zinc-800 pl-1 pt-1 text-[9px] font-mono text-zinc-500 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>); return (<div key={i} className="absolute h-full border-l border-zinc-700 pl-1 pt-1 text-[14px] font-mono text-zinc-300 pointer-events-none" style={{ left: `${x}px` }}>{formatTime(sec)}</div>);
})} })}
</div> </div>
</div> </div>
@@ -4763,7 +4890,7 @@
</div> </div>
<div className="flex-1 flex flex-col relative bg-[#111111] min-h-full"> <div className="flex-1 flex flex-col relative bg-[#111111] min-h-full">
{vTrack && ( {vTrack && (
<div className="flex-1 relative overflow-hidden border-b border-[#141414]"> <div style={{ height: `${subTabHeight}px` }} className="relative overflow-hidden border-2 border-purple-500/40 rounded bg-[#151515] mx-1 mt-1 shrink-0">
<SubTabWaveform <SubTabWaveform
buffer={st.buffer} buffer={st.buffer}
subTabId={st.id} subTabId={st.id}
@@ -4777,7 +4904,11 @@
onContextMenu={(e, clickTime) => setContextMenu({x: e.clientX, y: e.clientY, isSubTab: true, subTabId: st.id, time: clickTime})} onContextMenu={(e, clickTime) => setContextMenu({x: e.clientX, y: e.clientY, isSubTab: true, subTabId: st.id, time: clickTime})}
zoom={zoom} zoom={zoom}
timelineWidth={subTabTimelineWidth} timelineWidth={subTabTimelineWidth}
color={vTrack.color}
name={vTrack.name}
/> />
<div onMouseDown={handleSubTabResizeMouseDown}
className="absolute bottom-0 left-0 right-0 h-1.5 cursor-ns-resize z-30 hover:bg-purple-500/50 transition-colors" />
</div> </div>
)} )}
{st.selectionStart !== null && st.selectionEnd !== null && st.selectionEnd > st.selectionStart && ( {st.selectionStart !== null && st.selectionEnd !== null && st.selectionEnd > st.selectionStart && (