fix: pass channel+synth_engine to playNote in all MIDI playback paths

6 call sites were missing channel parameter, causing FluidSynth
to use ch=0 for all tracks -> program_change on channel 0
overwrote instrument across tracks during playback.

Fixes: startTrackPlayback, section sub-track MIDI, playMidiPreviewNote,
piano roll Alt+scroll preview, note click preview, keybed preview.
This commit is contained in:
2026-07-27 20:42:45 +07:00
parent f429f6b1d3
commit 40502eda86
3 changed files with 75 additions and 18 deletions
+61 -10
View File
@@ -4842,8 +4842,14 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
const playing = notes.filter(n => const playing = notes.filter(n =>
currentBeat < n.start_beat && newBeat >= n.start_beat currentBeat < n.start_beat && newBeat >= n.start_beat
); );
var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var pvCh = pvTrk ? pvTrk.midiChannel : undefined;
if (pvCh === undefined && pvTrk) {
for (var pvi = 0; pvi < activeTracks.length; pvi++) { if (activeTracks[pvi].id === st.trackId) { pvCh = pvi >= 9 ? pvi + 1 : pvi; break; } }
if (pvCh === undefined || pvCh >= 16) pvCh = 0;
}
playing.forEach(n => { playing.forEach(n => {
window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, st.instrumentProgram, null); window.SonicSF.playNote(n.pitch, (n.velocity || 0.8) * 127, 200, ctx.currentTime, st.instrumentProgram, null, pvCh, pvTrk ? pvTrk.synth_engine : undefined);
}); });
} }
} }
@@ -5118,7 +5124,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (clickedNoteIdx !== -1 && !e.ctrlKey && !e.shiftKey && !e.altKey) { if (clickedNoteIdx !== -1 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
if (window.SonicSF) { if (window.SonicSF) {
const ctx = getAudioContext(); const ctx = getAudioContext();
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, st.instrumentProgram, null); var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var clCh = clTrk ? clTrk.midiChannel : undefined;
if (clCh === undefined && clTrk) {
for (var cli = 0; cli < activeTracks.length; cli++) { if (activeTracks[cli].id === st.trackId) { clCh = cli >= 9 ? cli + 1 : cli; break; } }
if (clCh === undefined || clCh >= 16) clCh = 0;
}
window.SonicSF.playNote(notes[clickedNoteIdx].pitch, 100, 300, ctx.currentTime, st.instrumentProgram, null, clCh, clTrk ? clTrk.synth_engine : undefined);
} }
} }
@@ -5640,6 +5652,13 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
}; };
const renderKeybed = () => { const renderKeybed = () => {
var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; });
var kbCh = kbTrk ? kbTrk.midiChannel : undefined;
if (kbCh === undefined && kbTrk) {
for (var kbi = 0; kbi < activeTracks.length; kbi++) { if (activeTracks[kbi].id === st.trackId) { kbCh = kbi >= 9 ? kbi + 1 : kbi; break; } }
if (kbCh === undefined || kbCh >= 16) kbCh = 0;
}
var kbSynth = kbTrk ? kbTrk.synth_engine : undefined;
const keys = []; const keys = [];
for (let pitch = 127; pitch >= PITCH_START; pitch--) { for (let pitch = 127; pitch >= PITCH_START; pitch--) {
const isBlack = [1, 3, 6, 8, 10].includes(pitch % 12); const isBlack = [1, 3, 6, 8, 10].includes(pitch % 12);
@@ -5658,7 +5677,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
keybedMouseDownRef.current = true; keybedMouseDownRef.current = true;
try { try {
if (window.SonicSF) { if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null); window.SonicSF.playNote(pitch, 100, 500, undefined, st.instrumentProgram, null, kbCh, kbSynth);
} }
} catch (err) { } catch (err) {
console.error('playNote error:', err); console.error('playNote error:', err);
@@ -5668,7 +5687,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
if (keybedMouseDownRef.current) { if (keybedMouseDownRef.current) {
try { try {
if (window.SonicSF) { if (window.SonicSF) {
window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null); window.SonicSF.playNote(pitch, 100, 200, undefined, st.instrumentProgram, null, kbCh, kbSynth);
} }
} catch (err) { console.error('playNote error:', err); } } catch (err) { console.error('playNote error:', err); }
} }
@@ -10114,13 +10133,22 @@ const App = () => {
const track = activeTracks.find(t => t.id === trackId); const track = activeTracks.find(t => t.id === trackId);
const destNode = getOrCreateTrackNode(track, context); const destNode = getOrCreateTrackNode(track, context);
const program = track ? track.instrumentProgram : undefined; const program = track ? track.instrumentProgram : undefined;
var prevCh = track ? track.midiChannel : undefined;
if (prevCh === undefined && track) {
for (var pi = 0; pi < activeTracks.length; pi++) {
if (activeTracks[pi].id === track.id) { prevCh = pi >= 9 ? pi + 1 : pi; break; }
}
if (prevCh === undefined || prevCh >= 16) prevCh = 0;
}
window.SonicSF.playNote( window.SonicSF.playNote(
pitch, pitch,
velocity, velocity,
durationMs, durationMs,
context.currentTime, context.currentTime,
program, program,
destNode destNode,
prevCh,
track ? track.synth_engine : undefined
); );
}; };
@@ -10163,6 +10191,16 @@ const App = () => {
// MIDI items playback // MIDI items playback
const midiItems = track.midiItems || []; const midiItems = track.midiItems || [];
if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) { if ((track.type === 'MIDI' || midiItems.length > 0) && window.SonicSF) {
var trkCh = track.midiChannel;
if (trkCh === undefined) {
for (var tci = 0; tci < activeTracks.length; tci++) {
if (activeTracks[tci].id === track.id) {
trkCh = tci >= 9 ? tci + 1 : tci;
break;
}
}
if (trkCh === undefined || trkCh >= 16) trkCh = 0;
}
const bpmVal = parseInt(bpm) || 120; const bpmVal = parseInt(bpm) || 120;
const secondsPerBeat = 60.0 / bpmVal; const secondsPerBeat = 60.0 / bpmVal;
midiItems.forEach(item => { midiItems.forEach(item => {
@@ -10186,7 +10224,9 @@ const App = () => {
durationMs, durationMs,
startTime, startTime,
program, program,
gainNode gainNode,
trkCh,
track.synth_engine
); );
} else { } else {
const playOffset = offsetTime - noteStartSec; const playOffset = offsetTime - noteStartSec;
@@ -10197,7 +10237,9 @@ const App = () => {
remainingDurMs, remainingDurMs,
context.currentTime, context.currentTime,
program, program,
gainNode gainNode,
trkCh,
track.synth_engine
); );
} }
} }
@@ -10224,7 +10266,7 @@ const App = () => {
const secTracks = sec.tracks || []; const secTracks = sec.tracks || [];
const hasSubSolo = secTracks.some(st => st.solo); const hasSubSolo = secTracks.some(st => st.solo);
secTracks.forEach(subTrack => { secTracks.forEach((subTrack, subIdx) => {
const isSubPlayable = hasSubSolo ? subTrack.solo : !subTrack.muted; const isSubPlayable = hasSubSolo ? subTrack.solo : !subTrack.muted;
if (!isSubPlayable) return; if (!isSubPlayable) return;
@@ -10283,6 +10325,11 @@ const App = () => {
if (noteStartMain >= secStart && noteStartMain < secEnd && offsetTime < noteEndMain) { if (noteStartMain >= secStart && noteStartMain < secEnd && offsetTime < noteEndMain) {
const notePlayEndMain = Math.min(noteEndMain, secEnd); const notePlayEndMain = Math.min(noteEndMain, secEnd);
const program = subTrack.instrumentProgram !== undefined ? subTrack.instrumentProgram : 0; const program = subTrack.instrumentProgram !== undefined ? subTrack.instrumentProgram : 0;
var subCh = subTrack.midiChannel;
if (subCh === undefined) {
subCh = subIdx >= 9 ? subIdx + 1 : subIdx;
}
if (subCh === undefined || subCh >= 16) subCh = 0;
if (offsetTime < noteStartMain) { if (offsetTime < noteStartMain) {
const delay = noteStartMain - offsetTime; const delay = noteStartMain - offsetTime;
@@ -10294,7 +10341,9 @@ const App = () => {
playDurMs, playDurMs,
startTime, startTime,
program, program,
subNode subNode,
subCh,
subTrack.synth_engine
); );
} else { } else {
const remainingDurMs = (notePlayEndMain - offsetTime) * 1000; const remainingDurMs = (notePlayEndMain - offsetTime) * 1000;
@@ -10304,7 +10353,9 @@ const App = () => {
remainingDurMs, remainingDurMs,
context.currentTime, context.currentTime,
program, program,
subNode subNode,
subCh,
subTrack.synth_engine
); );
} }
} }
File diff suppressed because one or more lines are too long
+6
View File
@@ -473,3 +473,9 @@
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (verify lines 4642-4667, 5905, 6763-6807, 8422-8424), `app/core/render_engine.py` (verify lines 95-115) - **Các file ảnh hưởng:** `app/static/js/app.jsx` (verify lines 4642-4667, 5905, 6763-6807, 8422-8424), `app/core/render_engine.py` (verify lines 95-115)
- **Ghi chú/Test (nếu có):** No code changes needed. All 4 rules confirmed working. - **Ghi chú/Test (nếu có):** No code changes needed. All 4 rules confirmed working.
--- ---
### [2026-07-27 20:37] Task: Fix cross-track instrument interference (missed channel param)
- **Tóm tắt thay đổi:** Sửa 6 vị trí gọi `SonicSF.playNote()` thiếu tham số `channel` + `synthEngine`, khiến FluidSynth mặc định `ch=0` cho mọi track → `program_change` trên channel 0 đè lên nhau giữa các track. Fix: thêm channel computation (từ `track.midiChannel` hoặc track index) + truyền `synth_engine` vào `playNote` tại `startTrackPlayback`, section sub-track MIDI, `playMidiPreviewNote`, piano roll Alt+scroll preview, note click preview, keybed onMouseDown/onMouseEnter. `setTrackInstrumentWithProgram` đã filter đúng `trackId` — lỗi không nằm ở logic set instrument mà ở playback.
- **Các file ảnh hưởng:** `app/static/js/app.jsx` (lines 4846, 5121, 5661-5671, 10117-10124, 10179-10201, 10285-10307)
- **Ghi chú/Test (nếu có):** `npm run build` pass. Cần test: main session play 2 tracks MIDI khác instrument → mỗi track giữ instrument riêng. Piano Roll preview note → không ảnh hưởng track khác.
---