fix&feat: hiển thị tools chỉnh sửa audioclip
This commit is contained in:
+135
-18
@@ -779,6 +779,41 @@
|
||||
);
|
||||
};
|
||||
|
||||
const SubTabToolbar = ({ st, activeTool, setActiveTool, handleSubTabNormalizeWithValue, handleSubTabGainWithValue, handleSubTabPitch, handleSubTabStretch, handleSubTabFade }) => (
|
||||
<div className="daw-header flex h-16 items-center px-4 border-b daw-border gap-3 bg-zinc-800">
|
||||
<div className="flex items-center space-x-1 border-r border-zinc-700 pr-3">
|
||||
<button className={`p-1.5 rounded ${activeTool === 'select' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('select')} title="Select"><i data-lucide="mouse-pointer" className="w-4 h-4"></i></button>
|
||||
<button className={`p-1.5 rounded ${activeTool === 'grab' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('grab')} title="Grab"><i data-lucide="hand" className="w-4 h-4"></i></button>
|
||||
<button className={`p-1.5 rounded ${activeTool === 'razor' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('razor')} title="Razor"><i data-lucide="scissors" className="w-4 h-4"></i></button>
|
||||
<button className={`p-1.5 rounded ${activeTool === 'pen' ? 'bg-cyan-700' : 'bg-zinc-700'}`} onClick={() => setActiveTool('pen')} title="Pen Tool"><i data-lucide="pen-tool" className="w-4 h-4"></i></button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 flex-1 overflow-x-auto no-scrollbar">
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-zinc-400">Normalize:</span>
|
||||
<input type="range" min="-12" max="0" value={st.effects?.normalizeDb || 0} step="0.1" onChange={(e) => handleSubTabNormalizeWithValue(st.id, parseFloat(e.target.value))} className="w-20 h-1.5"/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-zinc-400">Gain:</span>
|
||||
<input type="range" min="-40" max="24" value={st.effects?.gainDb || 0} step="0.1" onChange={(e) => handleSubTabGainWithValue(st.id, parseFloat(e.target.value))} className="w-20 h-1.5"/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-zinc-400">Pitch:</span>
|
||||
<input type="range" min="-12" max="12" value={st.effects?.pitch || 0} step="0.1" onChange={(e) => handleSubTabPitch(st.id, parseFloat(e.target.value))} className="w-20 h-1.5"/>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-[10px] text-zinc-400">Stretch:</span>
|
||||
<input type="range" min="50" max="200" value={st.effects?.speedStretch || 100} step="1" onChange={(e) => handleSubTabStretch(st.id, parseInt(e.target.value))} className="w-20 h-1.5"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-1 pl-3 border-l border-zinc-700">
|
||||
<button onClick={() => handleSubTabFade(st.id, 'in')} className="px-2 py-1 text-[10px] rounded hover:bg-zinc-700">Fade In</button>
|
||||
<button onClick={() => handleSubTabFade(st.id, 'out')} className="px-2 py-1 text-[10px] rounded hover:bg-zinc-700">Fade Out</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const App = () => {
|
||||
// ── State Definitions ──
|
||||
const [tracks, setTracks] = useState([
|
||||
@@ -810,14 +845,6 @@
|
||||
snapValueRef.current = snapValue;
|
||||
const bpmRef = useRef(bpm);
|
||||
bpmRef.current = bpm;
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
}, 50);
|
||||
}, [activeTool]);
|
||||
// BMP for Tempo Track - LOOP_EDITOR_2.md §6
|
||||
const [selectedTrackId, setSelectedTrackId] = useState('1');
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
@@ -945,6 +972,15 @@
|
||||
fadeOutMs: 0,
|
||||
});
|
||||
|
||||
// Lucide icons initialization
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (window.lucide) {
|
||||
window.lucide.createIcons();
|
||||
}
|
||||
}, 50);
|
||||
}, [activeTool, activeTab]);
|
||||
|
||||
const timelineWrapperRef = useRef(null);
|
||||
const tcpContainerRef = useRef(null);
|
||||
|
||||
@@ -1380,7 +1416,7 @@
|
||||
startTime: selLeft,
|
||||
endTime: selRight,
|
||||
buffer: subBuffer,
|
||||
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0 },
|
||||
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0, normalizeDb: 0, pitch: 0, speedStretch: 100 },
|
||||
currentTime: 0,
|
||||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
@@ -1421,7 +1457,7 @@
|
||||
startTime: clip.startTime,
|
||||
endTime: clip.startTime + clip.buffer.duration,
|
||||
buffer: subBuffer,
|
||||
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0 },
|
||||
effects: { reverse: false, gainDb: 0, fadeInMs: 0, fadeOutMs: 0, normalizeDb: 0, pitch: 0, speedStretch: 100 },
|
||||
currentTime: 0,
|
||||
selectionStart: null,
|
||||
selectionEnd: null,
|
||||
@@ -1466,14 +1502,58 @@
|
||||
const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeInMs / 1000 * sr));
|
||||
for (let i = 0; i < fadeSamples; i++) edData[i] = edData[i] * (i / fadeSamples);
|
||||
}
|
||||
// Fade out
|
||||
if (fx.fadeOutMs > 0) {
|
||||
const sr = edBuffer.sampleRate;
|
||||
const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeOutMs / 1000 * sr));
|
||||
for (let i = edData.length - fadeSamples; i < edData.length; i++) {
|
||||
edData[i] = edData[i] * ((edData.length - 1 - i) / fadeSamples);
|
||||
}
|
||||
}
|
||||
// Fade out
|
||||
if (fx.fadeOutMs > 0) {
|
||||
const sr = edBuffer.sampleRate;
|
||||
const fadeSamples = Math.min(edData.length, Math.floor(fx.fadeOutMs / 1000 * sr));
|
||||
for (let i = edData.length - fadeSamples; i < edData.length; i++) {
|
||||
edData[i] = edData[i] * ((edData.length - 1 - i) / fadeSamples);
|
||||
}
|
||||
}
|
||||
// Normalize (apply to selection or entire buffer)
|
||||
if (fx.normalizeDb !== 0) {
|
||||
const targetDb = fx.normalizeDb;
|
||||
let maxVal = 0;
|
||||
for (let i = 0; i < edData.length; i++) {
|
||||
const abs = Math.abs(edData[i]);
|
||||
if (abs > maxVal) maxVal = abs;
|
||||
}
|
||||
if (maxVal > 0) {
|
||||
const targetAmp = Math.pow(10, targetDb / 20);
|
||||
const scale = targetAmp / maxVal;
|
||||
for (let i = 0; i < edData.length; i++) {
|
||||
edData[i] = Math.max(-1, Math.min(1, edData[i] * scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pitch shift (simple resample)
|
||||
if (fx.pitch !== 0) {
|
||||
const ratio = Math.pow(2, fx.pitch / 12);
|
||||
const newLength = Math.round(edData.length / ratio);
|
||||
const newData = new Float32Array(newLength);
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
const srcIdx = i * ratio;
|
||||
const idx0 = Math.floor(srcIdx);
|
||||
const idx1 = Math.min(idx0 + 1, edData.length - 1);
|
||||
const frac = srcIdx - idx0;
|
||||
newData[i] = edData[idx0] * (1 - frac) + edData[idx1] * frac;
|
||||
}
|
||||
edData.set(newData);
|
||||
}
|
||||
// Speed stretch
|
||||
if (fx.speedStretch !== 100) {
|
||||
const ratio = fx.speedStretch / 100;
|
||||
const newLength = Math.round(edData.length * ratio);
|
||||
const newData = new Float32Array(newLength);
|
||||
for (let i = 0; i < newLength; i++) {
|
||||
const srcIdx = i / ratio;
|
||||
const idx0 = Math.floor(srcIdx);
|
||||
const idx1 = Math.min(idx0 + 1, edData.length - 1);
|
||||
const frac = srcIdx - idx0;
|
||||
newData[i] = edData[idx0] * (1 - frac) + edData[idx1] * frac;
|
||||
}
|
||||
edData.set(newData);
|
||||
}
|
||||
|
||||
if (subTab.clipId) {
|
||||
// Clip-based merge
|
||||
@@ -1549,6 +1629,33 @@
|
||||
setSubTabs(prev => prev.map(s => s.id === tabId ? { ...s, effects: { ...s.effects, ...effects } } : s));
|
||||
};
|
||||
|
||||
const handleSubTabNormalizeWithValue = (tabId, normalizeDb) => {
|
||||
updateSubTabEffects(tabId, { normalizeDb });
|
||||
};
|
||||
|
||||
const handleSubTabGainWithValue = (tabId, gainDb) => {
|
||||
updateSubTabEffects(tabId, { gainDb });
|
||||
};
|
||||
|
||||
const handleSubTabPitch = (tabId, pitch) => {
|
||||
updateSubTabEffects(tabId, { pitch });
|
||||
};
|
||||
|
||||
const handleSubTabStretch = (tabId, speedStretch) => {
|
||||
updateSubTabEffects(tabId, { speedStretch });
|
||||
};
|
||||
|
||||
const handleSubTabFadeState = (tabId, type) => {
|
||||
const st = subTabs.find(s => s.id === tabId);
|
||||
if (!st) return;
|
||||
const fx = st.effects || {};
|
||||
if (type === 'in') {
|
||||
updateSubTabEffects(tabId, { fadeInMs: (fx.fadeInMs || 0) + 10 });
|
||||
} else if (type === 'out') {
|
||||
updateSubTabEffects(tabId, { fadeOutMs: (fx.fadeOutMs || 0) + 10 });
|
||||
}
|
||||
};
|
||||
|
||||
// ── Context Menu Handlers ──
|
||||
const handleContextMenu = (e, trackId, clickTime) => {
|
||||
e.preventDefault();
|
||||
@@ -4318,6 +4425,16 @@
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 flex flex-col p-3 gap-3 overflow-y-auto">
|
||||
<SubTabToolbar
|
||||
st={st}
|
||||
activeTool={activeTool}
|
||||
setActiveTool={setActiveTool}
|
||||
handleSubTabNormalizeWithValue={handleSubTabNormalizeWithValue}
|
||||
handleSubTabGainWithValue={handleSubTabGainWithValue}
|
||||
handleSubTabPitch={handleSubTabPitch}
|
||||
handleSubTabStretch={handleSubTabStretch}
|
||||
handleSubTabFade={handleSubTabFadeState}
|
||||
/>
|
||||
<SubTabWaveform
|
||||
buffer={st.buffer}
|
||||
subTabId={st.id}
|
||||
|
||||
Reference in New Issue
Block a user