feat: ARM multitrack mỗi track dùng MIDI channel riêng (0-15, trừ 9) - không làm đổi instrument track khác, lưu midi_channel khi save project

This commit is contained in:
2026-08-03 10:49:41 +07:00
parent 25471e6ea5
commit 84ab4ae823
4 changed files with 89 additions and 47 deletions
+64 -34
View File
@@ -4637,7 +4637,7 @@ const ProfileModal = ({
if (!sfId) continue;
const bank = se ? (se.soundfont_bank !== undefined ? se.soundfont_bank : 0) : (t.soundfont_bank !== undefined ? t.soundfont_bank : 0);
const prog = se ? (se.soundfont_program !== undefined ? se.soundfont_program : 0) : (t.soundfont_program !== undefined ? t.soundfont_program : 0);
var ch = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(t, tracks) : (t.midiChannel !== undefined ? t.midiChannel : 0);
var ch = t && assignTrackMidiChannel(t, tracks);
try {
await window.SonicSF.selectInstrument(ch, bank, prog, sfId);
} catch (e) {}
@@ -4702,6 +4702,7 @@ const ProfileModal = ({
}
setTracks(restoredTracks);
loadAudioBuffersForTracks(restoredTracks).catch(function(err) { console.warn('loadAudioBuffersForTracks error:', err); });
trackMidiChannelsRef.current = {};
preloadTrackInstruments(restoredTracks).catch(function(err) { console.warn('preloadTrackInstruments error:', err); });
setBpm(restoredBpm.toString());
setSelectedTrackId(restoredTracks[0]?.id || '1');
@@ -7888,6 +7889,7 @@ const serializeTracksList = (tracksList, secondsPerBar) => {
soundfont_bank: t.soundfont_bank !== undefined ? t.soundfont_bank : (t.synth_engine ? t.synth_engine.soundfont_bank : null),
soundfont_program: t.soundfont_program !== undefined ? t.soundfont_program : (t.synth_engine ? t.synth_engine.soundfont_program : null),
synth_engine: t.synth_engine || undefined,
midi_channel: t.midiChannel !== undefined ? t.midiChannel : null,
server_file_id: t.serverFileId || null,
items: items
};
@@ -11088,10 +11090,41 @@ const App = () => {
"Tinkle Bell","Agogo","Steel Drums","Woodblock","Taiko Drum","Melodic Tom","Synth Drum","Reverse Cymbal",
"Guitar Fret Noise","Breath Noise","Seashore","Bird Tweet","Telephone Ring","Helicopter","Applause","Gunshot"
];
// Dedicated per-track MIDI channel allocation
// FluidSynth has 16 channels; if two tracks share a channel, arming one track
// re-selects the other track's program and its instrument changes. Every track
// gets its own stable, unique channel (0-15, skipping 9 which is the classic
// percussion slot) so multi-track ARM never cross-contaminates instruments.
const trackMidiChannelsRef = useRef({});
const ensureTrackMidiChannel = (track, tracks) => {
if (!track) return 0;
const trackList = tracks || [];
const inUse = new Set();
trackList.forEach(tr => { if (tr && tr.id !== track.id && tr.midiChannel !== undefined) inUse.add(tr.midiChannel); });
const cached = trackMidiChannelsRef.current[track.id];
if (cached !== undefined && !inUse.has(cached)) return cached;
const preferred = track.midiChannel !== undefined && !inUse.has(track.midiChannel) ? track.midiChannel : null;
if (preferred !== null) { trackMidiChannelsRef.current[track.id] = preferred; return preferred; }
for (let c = 0; c < 16; c++) {
if (c === 9) continue;
if (!inUse.has(c)) { trackMidiChannelsRef.current[track.id] = c; return c; }
}
trackMidiChannelsRef.current[track.id] = 0;
return 0;
};
const assignTrackMidiChannel = (track, tracks) => {
const ch = ensureTrackMidiChannel(track, tracks);
if (track && track.midiChannel !== ch) track.midiChannel = ch;
return ch;
};
const setTrackInstrumentWithProgram = (trackId, instrumentId, programNumber, displayName, bankNumber) => {
const isSfInstrument = instrumentId && typeof instrumentId === 'string' && instrumentId.startsWith('sf_');
const sfBank = bankNumber !== undefined ? bankNumber : (isSfInstrument ? 0 : undefined);
const sfProg = programNumber !== undefined ? programNumber : undefined;
var mt = activeTracksRef.current || tracks;
var curTrk = null;
for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { curTrk = mt[ci]; break; } }
var mch = curTrk ? assignTrackMidiChannel(curTrk, mt) : (sfBank === 128 ? 9 : 0);
updateActiveTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
const hasInstrument = !!instrumentId;
@@ -11103,10 +11136,6 @@ const App = () => {
soundfont_program: sfProg !== undefined ? sfProg : 0,
soundfont_id: isSfInstrument ? instrumentId.replace('sf_', '') : ''
} : undefined;
var mt = activeTracksRef.current || tracks;
var midx = 0;
for (var mi = 0; mi < mt.length; mi++) { if (mt[mi].id === trackId) { midx = mi; break; } }
var mch = sfBank === 128 ? 9 : (midx % 16);
return { ...t, midiChannel: mch, instrumentId, instrumentProgram: sfProg, instrumentName: displayName, soundfont_bank: sfBank, soundfont_program: sfProg, synth_engine: synthEngine, type: hasInstrument ? 'MIDI' : (t.type === 'MIDI' ? 'audio' : t.type) };
}));
setInstrumentDropdownTrackId(null);
@@ -11115,15 +11144,7 @@ const App = () => {
// Trigger SpessaSynth load + program change when soundfont instrument selected
if (window.SonicSF && window.SonicSF.selectInstrument && instrumentId && isSfInstrument) {
const sfId = instrumentId.replace('sf_', '');
var allTracks = activeTracksRef.current || tracks;
var tidx = 0;
for (var i = 0; i < allTracks.length; i++) { if (allTracks[i].id === trackId) { tidx = i; break; } }
var ch = sfBank === 128 ? 9 : (tidx % 16);
// Store channel for consistent per-track instrument playback
if (!allTracks[tidx] || allTracks[tidx].midiChannel === undefined) {
updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, midiChannel: ch } : t));
}
window.SonicSF.selectInstrument(ch, sfBank || 0, sfProg || 0, sfId);
window.SonicSF.selectInstrument(mch, sfBank || 0, sfProg || 0, sfId);
}
setSubTabs(prev => prev.map(s => {
if (s.trackId !== trackId) return s;
@@ -11168,11 +11189,15 @@ const App = () => {
setInstrumentDropdownBtnRect(null);
if (instrumentId && instrumentId.startsWith('sf_')) {
// Set instrument on track immediately so Synth button shows the name
var mt2 = activeTracksRef.current || tracks;
var qTrk = null;
for (var qi = 0; qi < mt2.length; qi++) { if (mt2[qi].id === trackId) { qTrk = mt2[qi]; break; } }
var qch = qTrk ? assignTrackMidiChannel(qTrk, mt2) : 0;
updateActiveTracks(prev => prev.map(t => {
if (t.id !== trackId) return t;
const sfClean = instrumentId.replace('sf_', '');
const synthEngine = { type: 'soundfont', plugin_id: instrumentId, soundfont_bank: 0, soundfont_program: 0, soundfont_id: sfClean };
return { ...t, instrumentId, instrumentProgram: undefined, instrumentName: displayName, synth_engine: synthEngine };
return { ...t, midiChannel: qch, instrumentId, instrumentProgram: undefined, instrumentName: displayName, synth_engine: synthEngine };
}));
setSelectedSoundFontId(instrumentId);
setSynthCategory('soundfont');
@@ -11297,7 +11322,7 @@ const App = () => {
if (arSubs.length > 0) {
arSubs.forEach(function(as) {
var asTrk = allTracks.find(function(t) { return t.id === as.trackId; });
var asCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(asTrk, allTracks) : (asTrk ? asTrk.midiChannel : 0);
var asCh = asTrk ? assignTrackMidiChannel(asTrk, allTracks) : 0;
var asProg = as.instrumentProgram;
var asSe = as.synth_engine;
if (window.triggerMidiVuActivity) {
@@ -11308,7 +11333,7 @@ const App = () => {
}
// Route to ALL armed tracks (not just the first one)
armedTracks.forEach(function(at) {
var atCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(at, allTracks) : (at.midiChannel !== undefined ? at.midiChannel : 0);
var atCh = assignTrackMidiChannel(at, allTracks);
var atProg = at.instrumentProgram;
var atSe = at.synth_engine;
var atDest = activeTrackNodesRef.current[at.id]?.gainNode || null;
@@ -11329,13 +11354,17 @@ const App = () => {
}
// Stop the note on ALL tracks (not just armed) to prevent stuck notes
// when ARM is toggled off while a key is held
if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || [];
stopTracks.forEach(function(st) {
var stCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(st, stopTracks) : (st.midiChannel !== undefined ? st.midiChannel : 0);
window.SonicSF.stopNote(stCh, pitch);
});
}
if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || [];
stopTracks.forEach(function(st) {
// Only stop channels that actually carry this track's notes
// an index-based fallback could hit another track's dedicated
// channel and kill its sound.
if (!st.synth_engine && st.midiChannel === undefined) return;
var stCh = assignTrackMidiChannel(st, stopTracks);
window.SonicSF.stopNote(stCh, pitch);
});
}
}
// Sustain (CC64), Modulation (CC1), Pitch Bend
@@ -11348,11 +11377,11 @@ const App = () => {
var ccTracks = activeTracksRef.current || [];
var hasArmed = ccTracks.some(function(t) { return t.isArmed; });
if (hasArmed) {
ccTracks.forEach(function(ct) {
if (!ct.isArmed) return;
var ctCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(ct, ccTracks) : (ct.midiChannel !== undefined ? ct.midiChannel : 0);
window.SonicSF.controllerChange(ctCh, cc, val);
});
ccTracks.forEach(function(ct) {
if (!ct.isArmed) return;
var ctCh = assignTrackMidiChannel(ct, ccTracks);
window.SonicSF.controllerChange(ctCh, cc, val);
});
} else {
window.SonicSF.controllerChange(midiCh, cc, val);
}
@@ -11366,11 +11395,11 @@ const App = () => {
var pbTracks = activeTracksRef.current || [];
var hasArmedPB = pbTracks.some(function(t) { return t.isArmed; });
if (hasArmedPB) {
pbTracks.forEach(function(pt) {
if (!pt.isArmed) return;
var ptCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(pt, pbTracks) : (pt.midiChannel !== undefined ? pt.midiChannel : 0);
window.SonicSF.pitchBend(ptCh, bendVal);
});
pbTracks.forEach(function(pt) {
if (!pt.isArmed) return;
var ptCh = assignTrackMidiChannel(pt, pbTracks);
window.SonicSF.pitchBend(ptCh, bendVal);
});
} else {
window.SonicSF.pitchBend(midiCh, bendVal);
}
@@ -12128,6 +12157,7 @@ const App = () => {
}
setTracks(restoredTracks);
loadAudioBuffersForTracks(restoredTracks).catch(function(err) { console.warn('loadAudioBuffersForTracks error:', err); });
trackMidiChannelsRef.current = {};
preloadTrackInstruments(restoredTracks).catch(function(err) { console.warn('preloadTrackInstruments error:', err); });
setBpm(restoredBpm.toString());
setSelectedTrackId(restoredTracks[0]?.id || '1');
File diff suppressed because one or more lines are too long