fix: stuck MIDI notes when ARM toggled off mid-note

Remove !st.isArmed guard in note-off handler. Stop note on ALL
tracks regardless of ARM state — FluidSynth stopNote on inactive
channel/pitch is a no-op, so this is safe.
This commit is contained in:
2026-07-29 10:02:30 +07:00
parent fa7815bb53
commit 5ed943b3d6
3 changed files with 18 additions and 11 deletions
+9 -9
View File
@@ -7122,15 +7122,15 @@ const App = () => {
lastMidiNoteRef.current = { ...current, length: lenSec };
setLastMidiNote(prev => prev && prev.pitch === pitch ? { ...prev, length: lenSec, time: Date.now() } : prev);
}
// Stop the note on ALL armed tracks' dedicated channels
if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || [];
stopTracks.forEach(function(st) {
if (!st.isArmed) return;
var stCh = window.SonicPianoRoll ? window.SonicPianoRoll.getTrackMidiChannel(st, stopTracks) : (st.midiChannel !== undefined ? st.midiChannel : 0);
window.SonicSF.stopNote(stCh, pitch);
});
}
// 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);
});
}
}
// Sustain (CC64), Modulation (CC1), Pitch Bend
+3 -2
View File
@@ -198,8 +198,9 @@ const handleSectionItemDragStartRef=useRef(null);const[localSelectionTrackId,set
// Do NOT use raw MIDI hardware channel (msg.data[0] & 0x0F)
if(window.SonicSF){var allTracks=activeTracksRef.current||[];var arSubs=subTabsRef&&subTabsRef.current?subTabsRef.current.filter(function(s){return s.type==='PIANO_ROLL'&&s.isArmed;}):[];var armedTracks=allTracks.filter(function(t){return t.isArmed;});// Piano Roll arming has priority: route to each armed sub-tab's parent track
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 asProg=as.instrumentProgram;var asSe=as.synth_engine;window.SonicSF.playNote(pitch,velocity,60000,undefined,asProg,null,asCh,asSe);});}// 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 atProg=at.instrumentProgram;var atSe=at.synth_engine;var atDest=activeTrackNodesRef.current[at.id]?.gainNode||null;window.SonicSF.playNote(pitch,velocity,60000,undefined,atProg,atDest,atCh,atSe);});}}else if(cmd===0x8||cmd===0x9&&velocity===0){activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const current=lastMidiNoteRef.current;if(current&&current.pitch===pitch){const lenSec=(performance.now()-current.startTime)/1000;lastMidiNoteRef.current={...current,length:lenSec};setLastMidiNote(prev=>prev&&prev.pitch===pitch?{...prev,length:lenSec,time:Date.now()}:prev);}// Stop the note on ALL armed tracks' dedicated channels
if(window.SonicSF&&window.SonicSF.stopNote){var stopTracks=activeTracksRef.current||[];stopTracks.forEach(function(st){if(!st.isArmed)return;var stCh=window.SonicPianoRoll?window.SonicPianoRoll.getTrackMidiChannel(st,stopTracks):st.midiChannel!==undefined?st.midiChannel:0;window.SonicSF.stopNote(stCh,pitch);});}}// ── Sustain (CC64), Modulation (CC1), Pitch Bend ──
armedTracks.forEach(function(at){var atCh=window.SonicPianoRoll?window.SonicPianoRoll.getTrackMidiChannel(at,allTracks):at.midiChannel!==undefined?at.midiChannel:0;var atProg=at.instrumentProgram;var atSe=at.synth_engine;var atDest=activeTrackNodesRef.current[at.id]?.gainNode||null;window.SonicSF.playNote(pitch,velocity,60000,undefined,atProg,atDest,atCh,atSe);});}}else if(cmd===0x8||cmd===0x9&&velocity===0){activeMidiPitchesRef.current.delete(pitch);setActiveMidiPitches(new Set(activeMidiPitchesRef.current));const current=lastMidiNoteRef.current;if(current&&current.pitch===pitch){const lenSec=(performance.now()-current.startTime)/1000;lastMidiNoteRef.current={...current,length:lenSec};setLastMidiNote(prev=>prev&&prev.pitch===pitch?{...prev,length:lenSec,time:Date.now()}:prev);}// 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);});}}// ── Sustain (CC64), Modulation (CC1), Pitch Bend ──
const midiCh=msg.data[0]&0x0F;if(cmd===0xB){// Controller Change: forward to ALL armed tracks' dedicated channels
const cc=msg.data[1];const val=msg.data[2];if(window.SonicSF&&window.SonicSF.controllerChange){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);});}else{window.SonicSF.controllerChange(midiCh,cc,val);}}}else if(cmd===0xE){// Pitch Bend: forward to ALL armed tracks' dedicated channels
const lsb=msg.data[1];const msb=msg.data[2];const bendVal=msb<<7|lsb;if(window.SonicSF&&window.SonicSF.pitchBend){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);});}else{window.SonicSF.pitchBend(midiCh,bendVal);}}}// Forward to active MIDI recorders