From 5ed943b3d68dc8c1defd04f4c9a4ba6ffa434016 Mon Sep 17 00:00:00 2001 From: 3dtours Date: Wed, 29 Jul 2026 10:02:30 +0700 Subject: [PATCH] fix: stuck MIDI notes when ARM toggled off mid-note MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- app/static/js/app.jsx | 18 +++++++++--------- app/static/js/app.precompiled.js | 5 +++-- wiki.md | 6 ++++++ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index c25547c..985227c 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -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 ── diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index b31efac..cc107fc 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -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&¤t.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&¤t.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 diff --git a/wiki.md b/wiki.md index 2460a70..ddebf80 100644 --- a/wiki.md +++ b/wiki.md @@ -688,3 +688,9 @@ - **Các file ảnh hưởng:** `app/static/js/app.jsx` - **Ghi chú/Test (nếu có):** `npm run build` — build passes. --- + +### [2026-07-29 10:02] Task: Fix stuck MIDI notes when ARM toggled off mid-note +- **Tóm tắt thay đổi:** Xóa `if (!st.isArmed) return;` trong note-off handler. Note-off giờ stopNote trên ALL tracks thay vì chỉ armed tracks — FluidSynth stopNote trên channel không có note đang play là no-op. +- **Các file ảnh hưởng:** `app/static/js/app.jsx` +- **Ghi chú/Test (nếu có):** `npm run build` — build passes. +---