diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index a2056ed..614d63d 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -9198,8 +9198,7 @@ const App = () => { const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false); const [saveAsModalOpen, setSaveAsModalOpen] = useState(false); const [openProjectModalOpen, setOpenProjectModalOpen] = useState(false); - const soloedTrack = tracks.find(t => t.solo); - const soloedTrackId = soloedTrack ? soloedTrack.id : null; + const hasAnySolo = tracks.some(t => t.solo); const [toastMessage, setToastMessage] = useState(null); const [audioDevices, setAudioDevices] = useState([]); const [midiDevices, setMidiDevices] = useState([]); @@ -12985,12 +12984,16 @@ const App = () => { // If selection cleared by user, play linearly (don't loop) if (!selectionCleared && isLoopingSelection && selLeft !== null && selRight !== null) { if (selRight > selLeft && updatedTime >= selRight) { - if (soloedTrackId !== null || selectionMode === 'local') { + if (hasAnySolo || selectionMode === 'local') { stopAllPlayback(); startOffsetTimeRef.current = selLeft; startAudioTimeRef.current = context.currentTime; - const soloTid = soloedTrackId !== null ? soloedTrackId : localSelectionTrackId; - startLocalTrackPlayback(soloTid, selLeft); + if (hasAnySolo) { + const soloed = tracks.filter(t => t.solo); + soloed.forEach(t => startLocalTrackPlayback(t.id, selLeft)); + } else { + startLocalTrackPlayback(localSelectionTrackId, selLeft); + } setCurrentTime(selLeft); setIsPlaying(true); } else { @@ -13164,9 +13167,9 @@ const App = () => { const startTrackPlayback = offsetTime => { const context = getAudioContext(); - const hasSolo = activeTracks.some(t => t.solo) || soloedTrackId !== null; + const hasSolo = activeTracks.some(t => t.solo); activeTracks.forEach(track => { - const isPlayable = hasSolo ? track.id === soloedTrackId || track.solo : !track.muted; + const isPlayable = hasSolo ? track.solo : !track.muted; if (!isPlayable) return; const gainNode = getOrCreateTrackNode(track, context); @@ -20022,8 +20025,8 @@ const App = () => { toggleTrackSoloEvaluate(track.id); }, title: "Solo", - className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${soloedTrackId === track.id || track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}` - }, /*#__PURE__*/React.createElement("i", { "data-lucide": soloedTrackId === track.id || track.solo ? "headphones" : "headphone-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", { + className: `px-1.5 py-0.5 text-[10px] rounded font-mono font-bold border transition flex items-center gap-0.5 ${track.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}` + }, /*#__PURE__*/React.createElement("i", { "data-lucide": track.solo ? "headphones" : "headphone-off", className: "w-3 h-3" })), /*#__PURE__*/React.createElement("button", { onClick: e => { e.stopPropagation(); toggleTrackDrum(track.id); @@ -20548,7 +20551,7 @@ const App = () => { e.stopPropagation(); toggleTrackSoloEvaluate(vTrack.id); }, - className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${soloedTrackId === vTrack.id || vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}` + className: `px-1.5 py-0.5 text-xs rounded font-mono font-bold border ${vTrack.solo ? 'bg-amber-950 text-amber-400 border-amber-600' : 'bg-zinc-800 text-zinc-400 border-transparent hover:text-zinc-200'}` }, "S"))), /*#__PURE__*/React.createElement("div", { className: "flex flex-col gap-2.5 text-[14px] mb-3" }, /*#__PURE__*/React.createElement("div", { @@ -20992,9 +20995,9 @@ const App = () => { className: "text-amber-400 font-semibold uppercase text-xs" }, "Local Sel"), selectionMode === 'global' && /*#__PURE__*/React.createElement("span", { className: "text-purple-400 font-semibold uppercase text-xs" - }, "Global Sel"), soloedTrackId && /*#__PURE__*/React.createElement("span", { + }, "Global Sel"), hasAnySolo && /*#__PURE__*/React.createElement("span", { className: "text-amber-500 font-semibold" - }, "Solo: ID ", soloedTrackId), isLoopingSelection && selectionMode === 'local' && /*#__PURE__*/React.createElement("span", { + }, "Solo: ", tracks.filter(t => t.solo).length, " track(s)"), isLoopingSelection && selectionMode === 'local' && /*#__PURE__*/React.createElement("span", { className: "text-emerald-400 font-semibold uppercase text-xs" }, "Solo Loop"), isLoopingSelection && selectionMode !== 'local' && /*#__PURE__*/React.createElement("span", { className: "text-cyan-400 font-semibold uppercase text-xs" diff --git a/wiki.md b/wiki.md index 21484dd..a1a07c3 100644 --- a/wiki.md +++ b/wiki.md @@ -4,6 +4,12 @@ - **Ghi chú/Test (nếu có):** Không cần build. Fix áp dụng cho tất cả đường dẫn tạo track: addNewTrack, insertTrackBelow, AI cut/composition/rearrange, MIDI import, section clone. --- +### [2026-07-31 07:09] Task: Allow multiple simultaneous solo buttons (Track TCP) +- **Tóm tắt thay đổi:** `toggleTrackSoloEvaluate` trước đây set `solo: false` cho tất cả track trừ track được click → chỉ cho phép 1 solo. Fix: toggle solo chỉ trên track được click, giữ nguyên trạng thái solo của track khác. `soloedTrackId` (single track ID) được thay bằng `hasAnySolo` (boolean). `startTrackPlayback` dùng `track.solo` trực tiếp thay vì so sánh với `soloedTrackId`. Loop restart với solo: iterate tất cả soloed tracks thay vì chỉ play 1 track. UI highlight các nút solo dùng `track.solo`. +- **Các file ảnh hưởng:** `app/static/js/app.jsx` +- **Ghi chú/Test (nếu có):** Bấm solo track A → active, bấm solo track B → cả A và B đều solo. Audio playback chỉ play track có solo = true. +--- + ### [2026-07-28 07:56] Task: Fix PIANO_ROLL activeTracks + instrument auto-assign trong SECTION-TAB - **Tóm tắt thay đổi:** Fix `activeTracks` useMemo: khi `activeTab` là PIANO_ROLL sub-tab từ SECTION-TAB (`parent_tab_id` bắt đầu `session_`), trả về tracks của section tab thay vì main tracks. Trước đây `activeTracks` luôn trả về main tracks khi activeTab !== session_xxx → dropdown MIDI item rỗng, track name không hiển thị, instrument lookup sai. Fix: check `subTabs` cho PIANO_ROLL type để resolve đúng parent session tracks. - **Các file ảnh hưởng:** `app/static/js/app.jsx`, `app/static/js/app.precompiled.js`