fix: chỉnh sửa hiển thị của audio clip trong subtab
This commit is contained in:
+248
-117
@@ -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 && (
|
||||||
|
|||||||
Reference in New Issue
Block a user