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:
+64
-34
@@ -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
@@ -24,7 +24,7 @@
|
||||
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
|
||||
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
|
||||
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608031040" defer></script>
|
||||
<script src="/static/js/app.precompiled.js?v=202608031100" defer></script>
|
||||
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
|
||||
<style>
|
||||
:root {
|
||||
|
||||
Reference in New Issue
Block a user