fix: sửa lỗi audio clip dán từ clipboard không đúng dung lượng
This commit is contained in:
+129
-1
@@ -67,6 +67,27 @@
|
||||
return `${m}:${s.toString().padStart(2, '0')}.${ms}`;
|
||||
};
|
||||
|
||||
const formatBeat = (secs, bpmVal) => {
|
||||
if (isNaN(secs) || secs < 0) return "1.1.1";
|
||||
const beatDuration = 60 / bpmVal;
|
||||
const barDuration = beatDuration * 4;
|
||||
const bar = Math.floor(secs / barDuration) + 1;
|
||||
const beat = Math.floor((secs % barDuration) / beatDuration) + 1;
|
||||
const sub = Math.floor((secs % beatDuration) / (beatDuration / 4)) + 1;
|
||||
return `${bar}.${beat}.${sub}`;
|
||||
};
|
||||
|
||||
const getBeatMarkers = (maxDur, bpmVal) => {
|
||||
const beatDuration = 60 / bpmVal;
|
||||
const barDuration = beatDuration * 4;
|
||||
const markers = [];
|
||||
for (let t = 0; t <= maxDur; t += beatDuration) {
|
||||
const isBar = Math.abs(t % barDuration) < 0.001 || Math.abs(t % barDuration - barDuration) < 0.001;
|
||||
markers.push({ time: t, isBar, beatNum: Math.floor(t / beatDuration) + 1 });
|
||||
}
|
||||
return markers;
|
||||
};
|
||||
|
||||
const findZeroCrossing = (buffer, targetTime) => {
|
||||
if (!buffer) return targetTime;
|
||||
const sampleRate = buffer.sampleRate;
|
||||
@@ -288,6 +309,80 @@
|
||||
);
|
||||
};
|
||||
|
||||
const TempoTrackLane = ({ bpm, zoom, timelineWidth, onPlayheadSet }) => {
|
||||
const canvasRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const width = timelineWidth;
|
||||
const parent = canvas.parentElement;
|
||||
const height = parent ? parent.clientHeight : 40;
|
||||
|
||||
canvas.width = width * dpr;
|
||||
canvas.height = height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
ctx.fillStyle = '#1a1a2e';
|
||||
ctx.fillRect(0, 0, width, height);
|
||||
|
||||
const beatDuration = 60 / bpm;
|
||||
const barDuration = beatDuration * 4;
|
||||
const totalSec = width / zoom;
|
||||
|
||||
for (let t = 0; t <= totalSec; t += beatDuration) {
|
||||
const beatNum = Math.floor(t / beatDuration) + 1;
|
||||
const isBar = beatNum % 4 === 1;
|
||||
const x = t * zoom;
|
||||
|
||||
if (isBar) {
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
|
||||
ctx.lineWidth = 1.5;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
|
||||
ctx.font = 'bold 9px Inter, sans-serif';
|
||||
ctx.textAlign = 'left';
|
||||
ctx.fillText(`${Math.ceil(beatNum / 4)}`, x + 3, 11);
|
||||
} else {
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.35)';
|
||||
ctx.font = 'bold 10px Inter, sans-serif';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(`${bpm} BPM`, width - 6, 12);
|
||||
|
||||
}, [bpm, zoom, timelineWidth]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
className="w-full h-full cursor-crosshair"
|
||||
onMouseDown={(e) => {
|
||||
const wrapper = canvasRef.current?.parentElement?.parentElement;
|
||||
const scrollLeft = wrapper ? wrapper.scrollLeft : 0;
|
||||
const rect = canvasRef.current.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left + scrollLeft;
|
||||
const time = Math.max(0, x / zoom);
|
||||
onPlayheadSet(time);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
// ── Sub-Tab Waveform Component (LOOP_EDITOR_2.md §1.2) ──
|
||||
const SubTabWaveform = ({ buffer }) => {
|
||||
const canvasRef = useRef(null);
|
||||
@@ -337,6 +432,8 @@
|
||||
{ id: '1', name: 'Track 01', buffer: null, volume: 0.8, muted: false, solo: false, color: '#0f766e', markers: [], serverFileId: null },
|
||||
{ id: '2', name: 'Track 02', buffer: null, volume: 0.8, muted: false, solo: false, color: '#1d4ed8', markers: [], serverFileId: null },
|
||||
]);
|
||||
// BMP for Tempo Track - LOOP_EDITOR_2.md §6
|
||||
const [bpm, setBpm] = useState(localStorage.getItem('studio_bpm') || '120');
|
||||
const [selectedTrackId, setSelectedTrackId] = useState('1');
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [isPlaying, setIsPlaying] = useState(false);
|
||||
@@ -978,6 +1075,9 @@
|
||||
return max;
|
||||
}, [tracks]);
|
||||
|
||||
const maxDurationRef = useRef(maxDuration);
|
||||
maxDurationRef.current = maxDuration;
|
||||
|
||||
const timelineWidth = useMemo(() => Math.max(zoom * maxDuration, 1200), [zoom, maxDuration]);
|
||||
|
||||
const minZoom = useMemo(() => Math.max(20, 1200 / maxDuration), [maxDuration]);
|
||||
@@ -1126,7 +1226,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedTime >= maxDuration) {
|
||||
if (updatedTime >= maxDurationRef.current) {
|
||||
stopAllPlayback();
|
||||
setCurrentTime(0);
|
||||
return;
|
||||
@@ -2159,6 +2259,28 @@
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col divide-y divide-[#141414]">
|
||||
{/* Tempo Track TCP - LOOP_EDITOR_2.md §6 */}
|
||||
<div className="h-[40px] p-2 flex flex-col justify-between bg-[#1a1a2e] border-l-4 border-purple-500">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[10px] font-bold text-purple-400 font-mono">TM</span>
|
||||
<span className="text-xs font-semibold text-zinc-300">Tempo Track</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<input
|
||||
type="number"
|
||||
value={bpm}
|
||||
onChange={(e) => setBpm(e.target.value)}
|
||||
onBlur={() => localStorage.setItem('studio_bpm', bpm)}
|
||||
className="w-12 bg-zinc-800 border border-zinc-700 rounded text-[10px] text-zinc-300 text-center font-mono focus:outline-none focus:border-purple-500"
|
||||
min="40"
|
||||
max="300"
|
||||
/>
|
||||
<span className="text-[9px] text-zinc-500">BPM</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tracks.map((track, idx) => {
|
||||
const isSelected = selectedTrackId === track.id;
|
||||
return (
|
||||
@@ -2266,6 +2388,12 @@
|
||||
|
||||
{/* Stacked Lanes */}
|
||||
<div className="flex-1 flex flex-col relative divide-y divide-[#141414]">
|
||||
{/* Tempo Track Lane - LOOP_EDITOR_2.md §6 */}
|
||||
<div className="h-[40px] w-full relative flex-shrink-0 border-b border-purple-500/30">
|
||||
<TempoTrackLane bpm={parseInt(bpm) || 120} zoom={zoom} timelineWidth={timelineWidth}
|
||||
onPlayheadSet={setCurrentTime} />
|
||||
</div>
|
||||
|
||||
{tracks.map((track) => {
|
||||
const isSelected = selectedTrackId === track.id;
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user