From a8b484bd158d3240c48aa43f4d69e6b25407f8d2 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 22 Jul 2026 18:08:23 +0700 Subject: [PATCH] =?UTF-8?q?fix:=20gi=E1=BA=A3i=20quy=E1=BA=BFt=20c=C3=A1c?= =?UTF-8?q?=20l=E1=BB=97i=20v=E1=BB=81=20xu=E1=BA=A5t=20track,=20t?= =?UTF-8?q?=E1=BA=A3i=20c=E1=BA=A5u=20h=C3=ACnh=20AI,=20tr=C3=A0n=20dropdo?= =?UTF-8?q?wn,=20click=20h=E1=BB=A7y=20ch=E1=BB=8Dn=20v=C3=A0=20l=C3=A0m?= =?UTF-8?q?=20m=E1=BB=9Bi=20b=E1=BA=A3ng=20DSP=20Tools?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/static/js/app.jsx | 107 ++++++++++++++++++++++++++++--- app/static/js/app.precompiled.js | 104 +++++++++++++++++++++++++++--- app/storage/sonicforge.db | Bin 45056 -> 45056 bytes 3 files changed, 191 insertions(+), 20 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index affcd2e..6ad54b7 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -549,6 +549,17 @@ const WaveformLane = ({ speed: track.speed || 1.0 }] : []; + // Check if Ctrl+Click to exit selection + if (e.ctrlKey && selectionMode) { + e.preventDefault(); + e.stopPropagation(); + if (onClearLocalSelection) onClearLocalSelection(); + if (onSetSelectionMode) onSetSelectionMode(null); + if (onSetSelectionStart) onSetSelectionStart(null); + if (onSetSelectionEnd) onSetSelectionEnd(null); + return; + } + // 1. Shift+Click (TOP PRIORITY): Range selection on track waveform if (e.shiftKey) { e.preventDefault(); @@ -3192,6 +3203,45 @@ const App = () => { const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...] const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2 + const dspSelectionStats = useMemo(() => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) return null; + const numChannels = track.buffer ? (track.buffer.numberOfChannels || 1) : 0; + if (selLeft === null || selRight === null || selRight <= selLeft || !track.buffer) { + return { + trackName: track.name, + channels: numChannels, + timeRange: 'Chưa chọn vùng', + peakVolume: 'N/A' + }; + } + const buffer = track.buffer; + const sampleRate = buffer.sampleRate; + const startSample = Math.max(0, Math.min(buffer.length - 1, Math.floor(selLeft * sampleRate))); + const endSample = Math.max(0, Math.min(buffer.length, Math.floor(selRight * sampleRate))); + let maxVal = 0; + for (let c = 0; c < numChannels; c++) { + const data = buffer.getChannelData(c); + for (let i = startSample; i < endSample; i++) { + const val = Math.abs(data[i]); + if (val > maxVal) maxVal = val; + } + } + let peakDb = 'N/A'; + if (maxVal > 0) { + const db = 20 * Math.log10(maxVal); + peakDb = db.toFixed(2) + ' dB'; + } else { + peakDb = '-∞ dB'; + } + return { + trackName: track.name, + channels: numChannels, + timeRange: `${selLeft.toFixed(2)}s - ${selRight.toFixed(2)}s (${(selRight - selLeft).toFixed(2)}s)`, + peakVolume: peakDb + }; + }, [tracks, selectedTrackId, selLeft, selRight]); + // ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ── const [tempTabActive, setTempTabActive] = useState(false); const [tempTabBuffer, setTempTabBuffer] = useState(null); @@ -3306,6 +3356,14 @@ const App = () => { if (data && data.preferences) loadPrefs(data.preferences); else { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); } } catch (e) { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); } + try { + const data = await window.SonicAPI.getAIConfigs(); + if (data && data.providers) { + setAiProviders(data.providers); + const active = data.providers.find(p => p.is_active) || data.providers[0]; + if (active) setSelectedProviderId(active.id); + } + } catch (e) {} })(); } }; @@ -7139,6 +7197,8 @@ const App = () => { if (window.DAWCommandDispatcher) { window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId; window.DAWCommandDispatcher.currentTracks = tracks; + window.DAWCommandDispatcher.lastCutSourceTrackId = null; + window.DAWCommandDispatcher.lastCutNewTrackId = null; } setAiProcessing(true); setAiActionLog(prev => [...prev, { type: 'status', text: ` ⏳ Đang gửi prompt đến AI...`, time: Date.now() }]); @@ -7570,6 +7630,8 @@ const App = () => { if (window.DAWCommandDispatcher) { window.DAWCommandDispatcher.currentTracks = nextTracks; window.DAWCommandDispatcher.currentSelectedTrackId = newId; + window.DAWCommandDispatcher.lastCutSourceTrackId = tid; + window.DAWCommandDispatcher.lastCutNewTrackId = newId; } setTracks(nextTracks); setSelectedTrackId(newId); @@ -7643,8 +7705,16 @@ const App = () => { return { success: true, time: parseFloat(time.toFixed(3)) }; }, exportAudio: async (args) => { - const tid = args.track_id || selectedTrackId; - const track = tid && tracks.find(t => t.id === tid); + const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks; + const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId; + let tid = args.track_id; + if (tid && window.DAWCommandDispatcher?.lastCutSourceTrackId && + (String(tid) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) || + 'track_' + tid === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) { + tid = window.DAWCommandDispatcher.lastCutNewTrackId; + } + if (!tid) tid = currentSelTrackId; + const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid); if (!track) return { success: false, error: 'No track found' }; const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []); if (clips.length === 0) return { success: false, error: 'Track has no audio clips' }; @@ -7773,7 +7843,13 @@ const App = () => { fadeIn: (args) => { const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks; const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId; - const trackIdRaw = args.track_id || currentSelTrackId; + let trackIdRaw = args.track_id; + if (trackIdRaw && window.DAWCommandDispatcher?.lastCutSourceTrackId && + (String(trackIdRaw) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) || + 'track_' + trackIdRaw === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) { + trackIdRaw = window.DAWCommandDispatcher.lastCutNewTrackId; + } + if (!trackIdRaw) trackIdRaw = currentSelTrackId; const track = currentTracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); if (!track) return { success: false, error: `Track ${trackIdRaw} not found` }; const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []); @@ -7833,7 +7909,13 @@ const App = () => { fadeOut: (args) => { const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks; const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId; - const trackIdRaw = args.track_id || currentSelTrackId; + let trackIdRaw = args.track_id; + if (trackIdRaw && window.DAWCommandDispatcher?.lastCutSourceTrackId && + (String(trackIdRaw) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) || + 'track_' + trackIdRaw === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) { + trackIdRaw = window.DAWCommandDispatcher.lastCutNewTrackId; + } + if (!trackIdRaw) trackIdRaw = currentSelTrackId; const track = currentTracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); if (!track) return { success: false, error: `Track ${trackIdRaw} not found` }; const clips = track.clips && track.clips.length > 0 ? track.clips : (track.buffer ? [{ id: 'default_' + track.id, buffer: track.buffer, startTime: track.startTime || 0, name: track.name }] : []); @@ -8131,7 +8213,7 @@ const App = () => { icon: 'settings', action: () => setAiConfigModalOpen(true) }, { - label: 'Python DSP Tools Panel', + label: 'DSP Tools Panel', icon: 'wrench', action: () => openPanel('python_tools') }] @@ -8790,7 +8872,7 @@ const App = () => { "data-lucide": "x", className: "w-3 h-3" }))))), /*#__PURE__*/React.createElement("div", { - className: "flex items-center gap-1.5 shrink-0" + className: "flex items-center gap-1.5 w-full min-w-0 pb-1" }, /*#__PURE__*/React.createElement("span", { className: "inline-flex items-center shrink-0" }, /*#__PURE__*/React.createElement("i", { @@ -8799,7 +8881,7 @@ const App = () => { })), /*#__PURE__*/React.createElement("select", { value: selectedProviderId, onChange: e => setSelectedProviderId(e.target.value), - className: "flex-1 bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600" + className: "flex-1 min-w-0 max-w-full bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600 truncate" }, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", { value: "" }, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", { @@ -8917,7 +8999,7 @@ const App = () => { }, /*#__PURE__*/React.createElement("i", { "data-lucide": "wrench", className: "w-3.5 h-3.5 text-amber-400" - })), " Python DSP Tools"), /*#__PURE__*/React.createElement("button", { + })), " DSP Tools"), /*#__PURE__*/React.createElement("button", { onClick: () => closePanel('python_tools'), className: "text-zinc-600 hover:text-zinc-300" }, /*#__PURE__*/React.createElement("span", { @@ -8926,8 +9008,13 @@ const App = () => { "data-lucide": "x", className: "w-3 h-3" })))), /*#__PURE__*/React.createElement("div", { - className: "p-1 bg-[#141414] rounded border border-zinc-800 text-xs font-mono text-zinc-400" - }, "// Non-AI Audio Processing Tools"), /*#__PURE__*/React.createElement("div", { + className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-[10px] font-mono text-zinc-400 space-y-0.5 leading-relaxed" + }, dspSelectionStats ? [ + /*#__PURE__*/React.createElement("div", { key: "track" }, `Track: ${dspSelectionStats.trackName}`), + /*#__PURE__*/React.createElement("div", { key: "range" }, `Range: ${dspSelectionStats.timeRange}`), + /*#__PURE__*/React.createElement("div", { key: "ch" }, `Channels: ${dspSelectionStats.channels}`), + /*#__PURE__*/React.createElement("div", { key: "peak" }, `Peak Vol: ${dspSelectionStats.peakVolume}`) + ] : "Chưa chọn track"), /*#__PURE__*/React.createElement("div", { className: "grid grid-cols-2 gap-1 text-xs" }, /*#__PURE__*/React.createElement("button", { onClick: () => runPythonTool('normalize'), diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index a6b0e45..b7276bc 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -549,6 +549,17 @@ const WaveformLane = ({ speed: track.speed || 1.0 }] : []; + // Check if Ctrl+Click to exit selection + if (e.ctrlKey && selectionMode) { + e.preventDefault(); + e.stopPropagation(); + if (onClearLocalSelection) onClearLocalSelection(); + if (onSetSelectionMode) onSetSelectionMode(null); + if (onSetSelectionStart) onSetSelectionStart(null); + if (onSetSelectionEnd) onSetSelectionEnd(null); + return; + } + // 1. Shift+Click (TOP PRIORITY): Range selection on track waveform if (e.shiftKey) { e.preventDefault(); @@ -3235,6 +3246,45 @@ const App = () => { const [subTabs, setSubTabs] = useState([]); // [{id, label, trackId, startTime, endTime, buffer, effects, currentTime, selectionStart, selectionEnd, isPlaying}, ...] const [selectionCleared, setSelectionCleared] = useState(false); // LOOP_EDITOR_2.md §4.2 + const dspSelectionStats = useMemo(() => { + const track = tracks.find(t => t.id === selectedTrackId); + if (!track) return null; + const numChannels = track.buffer ? track.buffer.numberOfChannels || 1 : 0; + if (selLeft === null || selRight === null || selRight <= selLeft || !track.buffer) { + return { + trackName: track.name, + channels: numChannels, + timeRange: 'Chưa chọn vùng', + peakVolume: 'N/A' + }; + } + const buffer = track.buffer; + const sampleRate = buffer.sampleRate; + const startSample = Math.max(0, Math.min(buffer.length - 1, Math.floor(selLeft * sampleRate))); + const endSample = Math.max(0, Math.min(buffer.length, Math.floor(selRight * sampleRate))); + let maxVal = 0; + for (let c = 0; c < numChannels; c++) { + const data = buffer.getChannelData(c); + for (let i = startSample; i < endSample; i++) { + const val = Math.abs(data[i]); + if (val > maxVal) maxVal = val; + } + } + let peakDb = 'N/A'; + if (maxVal > 0) { + const db = 20 * Math.log10(maxVal); + peakDb = db.toFixed(2) + ' dB'; + } else { + peakDb = '-∞ dB'; + } + return { + trackName: track.name, + channels: numChannels, + timeRange: `${selLeft.toFixed(2)}s - ${selRight.toFixed(2)}s (${(selRight - selLeft).toFixed(2)}s)`, + peakVolume: peakDb + }; + }, [tracks, selectedTrackId, selLeft, selRight]); + // ── Temp Edit Tab (LOOP_EDITOR_2.md §1.2 Sub Tab) ── const [tempTabActive, setTempTabActive] = useState(false); const [tempTabBuffer, setTempTabBuffer] = useState(null); @@ -3356,6 +3406,14 @@ const App = () => { const cached = localStorage.getItem('sonic_preferences'); if (cached) loadPrefs(JSON.parse(cached)); } + try { + const data = await window.SonicAPI.getAIConfigs(); + if (data && data.providers) { + setAiProviders(data.providers); + const active = data.providers.find(p => p.is_active) || data.providers[0]; + if (active) setSelectedProviderId(active.id); + } + } catch (e) {} })(); } }; @@ -7277,6 +7335,8 @@ const App = () => { if (window.DAWCommandDispatcher) { window.DAWCommandDispatcher.currentSelectedTrackId = selectedTrackId; window.DAWCommandDispatcher.currentTracks = tracks; + window.DAWCommandDispatcher.lastCutSourceTrackId = null; + window.DAWCommandDispatcher.lastCutNewTrackId = null; } setAiProcessing(true); setAiActionLog(prev => [...prev, { @@ -7861,6 +7921,8 @@ const App = () => { if (window.DAWCommandDispatcher) { window.DAWCommandDispatcher.currentTracks = nextTracks; window.DAWCommandDispatcher.currentSelectedTrackId = newId; + window.DAWCommandDispatcher.lastCutSourceTrackId = tid; + window.DAWCommandDispatcher.lastCutNewTrackId = newId; } setTracks(nextTracks); setSelectedTrackId(newId); @@ -7953,8 +8015,14 @@ const App = () => { }; }, exportAudio: async args => { - const tid = args.track_id || selectedTrackId; - const track = tid && tracks.find(t => t.id === tid); + const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks; + const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId; + let tid = args.track_id; + if (tid && window.DAWCommandDispatcher?.lastCutSourceTrackId && (String(tid) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) || 'track_' + tid === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) { + tid = window.DAWCommandDispatcher.lastCutNewTrackId; + } + if (!tid) tid = currentSelTrackId; + const track = tid && currentTracks.find(t => t.id === String(tid) || t.id === 'track_' + tid); if (!track) return { success: false, error: 'No track found' @@ -8154,7 +8222,11 @@ const App = () => { fadeIn: args => { const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks; const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId; - const trackIdRaw = args.track_id || currentSelTrackId; + let trackIdRaw = args.track_id; + if (trackIdRaw && window.DAWCommandDispatcher?.lastCutSourceTrackId && (String(trackIdRaw) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) || 'track_' + trackIdRaw === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) { + trackIdRaw = window.DAWCommandDispatcher.lastCutNewTrackId; + } + if (!trackIdRaw) trackIdRaw = currentSelTrackId; const track = currentTracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); if (!track) return { success: false, @@ -8245,7 +8317,11 @@ const App = () => { fadeOut: args => { const currentTracks = window.DAWCommandDispatcher?.currentTracks || tracks; const currentSelTrackId = window.DAWCommandDispatcher?.currentSelectedTrackId || selectedTrackId; - const trackIdRaw = args.track_id || currentSelTrackId; + let trackIdRaw = args.track_id; + if (trackIdRaw && window.DAWCommandDispatcher?.lastCutSourceTrackId && (String(trackIdRaw) === String(window.DAWCommandDispatcher.lastCutSourceTrackId) || 'track_' + trackIdRaw === String(window.DAWCommandDispatcher.lastCutSourceTrackId))) { + trackIdRaw = window.DAWCommandDispatcher.lastCutNewTrackId; + } + if (!trackIdRaw) trackIdRaw = currentSelTrackId; const track = currentTracks.find(t => t.id === String(trackIdRaw) || t.id === 'track_' + trackIdRaw); if (!track) return { success: false, @@ -8582,7 +8658,7 @@ const App = () => { icon: 'settings', action: () => setAiConfigModalOpen(true) }, { - label: 'Python DSP Tools Panel', + label: 'DSP Tools Panel', icon: 'wrench', action: () => openPanel('python_tools') }] @@ -9288,7 +9364,7 @@ const App = () => { "data-lucide": "x", className: "w-3 h-3" }))))), /*#__PURE__*/React.createElement("div", { - className: "flex items-center gap-1.5 shrink-0" + className: "flex items-center gap-1.5 w-full min-w-0 pb-1" }, /*#__PURE__*/React.createElement("span", { className: "inline-flex items-center shrink-0" }, /*#__PURE__*/React.createElement("i", { @@ -9297,7 +9373,7 @@ const App = () => { })), /*#__PURE__*/React.createElement("select", { value: selectedProviderId, onChange: e => setSelectedProviderId(e.target.value), - className: "flex-1 bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600" + className: "flex-1 min-w-0 max-w-full bg-[#141414] text-zinc-200 border border-zinc-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-purple-600 truncate" }, aiProviders.length === 0 ? /*#__PURE__*/React.createElement("option", { value: "" }, "Chưa có provider") : aiProviders.map(p => /*#__PURE__*/React.createElement("option", { @@ -9423,7 +9499,7 @@ const App = () => { }, /*#__PURE__*/React.createElement("i", { "data-lucide": "wrench", className: "w-3.5 h-3.5 text-amber-400" - })), " Python DSP Tools"), /*#__PURE__*/React.createElement("button", { + })), " DSP Tools"), /*#__PURE__*/React.createElement("button", { onClick: () => closePanel('python_tools'), className: "text-zinc-600 hover:text-zinc-300" }, /*#__PURE__*/React.createElement("span", { @@ -9432,8 +9508,16 @@ const App = () => { "data-lucide": "x", className: "w-3 h-3" })))), /*#__PURE__*/React.createElement("div", { - className: "p-1 bg-[#141414] rounded border border-zinc-800 text-xs font-mono text-zinc-400" - }, "// Non-AI Audio Processing Tools"), /*#__PURE__*/React.createElement("div", { + className: "p-1.5 bg-[#141414] rounded border border-zinc-800 text-[10px] font-mono text-zinc-400 space-y-0.5 leading-relaxed" + }, dspSelectionStats ? [/*#__PURE__*/React.createElement("div", { + key: "track" + }, `Track: ${dspSelectionStats.trackName}`), /*#__PURE__*/React.createElement("div", { + key: "range" + }, `Range: ${dspSelectionStats.timeRange}`), /*#__PURE__*/React.createElement("div", { + key: "ch" + }, `Channels: ${dspSelectionStats.channels}`), /*#__PURE__*/React.createElement("div", { + key: "peak" + }, `Peak Vol: ${dspSelectionStats.peakVolume}`)] : "Chưa chọn track"), /*#__PURE__*/React.createElement("div", { className: "grid grid-cols-2 gap-1 text-xs" }, /*#__PURE__*/React.createElement("button", { onClick: () => runPythonTool('normalize'), diff --git a/app/storage/sonicforge.db b/app/storage/sonicforge.db index 5a684f8b7f6615cb99035148a190fd3eda314b99..03dba3f13f8a1d59c49dba5287ac4fb8086e0eb3 100644 GIT binary patch delta 277 zcmZp8z|`=7X@WH4l!-FVj8iry2*@*XPA-&par1nj6lRXk?h2m^OKOjoIYNYJM)}M8{h*G(Nh`n!0&Xb)E?Tx-MM? delta 227 zcmZp8z|`=7X@WFk+e8^>#4APR#j4YE4(^8TQ4bzg6Q;ZD^ER0eu49ra|(>8yW7Yg9yh-Bbj z&pnAFa$}=62WOiZ3uk<&eK-5$edYUv5{pXm^Yav(6AMy{iWPkF^9v?-RoG2dsFt1l zq@rHK+}Idszk!mDQeI+is*;tGb7@JuGagNkDql^mtmfxp+Ut00hK5k8bJgZa)p;fW D_@YYw