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:
+61
-10
@@ -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
@@ -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.
|
||||||
|
---
|
||||||
|
|||||||
Reference in New Issue
Block a user