feat: allow multiple simultaneous solo tracks
Replace single-track solo (radio) with multi-solo toggle. No longer clears solo on other tracks when toggling one track.
This commit is contained in:
+15
-12
@@ -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"
|
||||
|
||||
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user