From 9fc5498999f04eb3aaa9b77fdca450d3cd843caf Mon Sep 17 00:00:00 2001 From: locphamtran Date: Tue, 11 Aug 2026 15:57:12 +0700 Subject: [PATCH] T7: hardware MIDI keyboard route qua UnifiedMidiRouter (3 nguon 1 router) --- app/static/js/app.jsx | 29 +++++++++++-- app/static/js/app.precompiled.js | 19 +++++---- app/static/js/services/unifiedMidiRouter.js | 46 +++++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index 7c9fa11..1d86710 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -15738,6 +15738,9 @@ const App = () => { 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; }); + // T7: UnifiedMidiRouter gom targets, dispatch 1 lan sau 2 vong. + var hwRouter = _getMidiRouter(); + var hwTargets = []; // Piano Roll arming has priority: route to each armed sub-tab's parent track if (arSubs.length > 0) { arSubs.forEach(function(as) { @@ -15753,7 +15756,9 @@ const App = () => { try { heldMidiNotesRef.current[as.trackId] = (heldMidiNotesRef.current[as.trackId] || 0) + 1; } catch (err) { } - if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) { + if (hwRouter && asTrk && asTrk.id) { + hwTargets.push({ trackId: asTrk.id, ch: asCh, engine: new window.TrackInstrument(asTrk.id, _makeCtx(asCh, asProg, asSe, null)) }); + } else if (isStandaloneSf() && isSfTrackEngine(asSe) && !shouldRouteCarla(asSe)) { if (!_routeNoteOn(asTrk && asTrk.id, _makeCtx(asCh, asProg, asSe, null), pitch, scaledVel / 127, 60000, 'HARDWARE_KEYBOARD')) { window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, asProg, null, asCh, asSe); } @@ -15780,7 +15785,9 @@ const App = () => { try { heldMidiNotesRef.current[at.id] = (heldMidiNotesRef.current[at.id] || 0) + 1; } catch (err) { } - if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) { + if (hwRouter && at && at.id) { + hwTargets.push({ trackId: at.id, ch: atCh, engine: new window.TrackInstrument(at.id, _makeCtx(atCh, atProg, atSe, atDest)) }); + } else if (isStandaloneSf() && isSfTrackEngine(atSe) && !shouldRouteCarla(atSe)) { if (!_routeNoteOn(at && at.id, _makeCtx(atCh, atProg, atSe, atDest), pitch, scaledVel / 127, 60000, 'HARDWARE_KEYBOARD')) { window.SonicSF.playNote(pitch, scaledVel, 60000, undefined, atProg, atDest, atCh, atSe); } @@ -15795,7 +15802,12 @@ const App = () => { try { window.SonicCarlaMidi.noteOn(atCh, pitch, scaledVel); } catch (e) {} } }); + // T7: dispatch 1 lan qua router - keybed/scheduler/hardware + // chung 1 voice tracker (chong stuck note khi doi engine). + if (hwRouter && hwTargets.length) { + hwRouter.handleHardwareKeyboardMessage(msg.data, hwTargets, []); } + } } else if (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)) { activeMidiPitchesRef.current.delete(pitch); setActiveMidiPitches(new Set(activeMidiPitchesRef.current)); @@ -15809,13 +15821,19 @@ const App = () => { // when ARM is toggled off while a key is held if (window.SonicSF && window.SonicSF.stopNote) { var stopTracks = activeTracksRef.current || []; + var hwRouterOff = _getMidiRouter(); + var hwStopTargets = []; 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); - if (isStandaloneSf()) _routeNoteOff(st.id, { ch: stCh }, pitch, 'HARDWARE_KEYBOARD'); + if (hwRouterOff && st.id) { + hwStopTargets.push({ trackId: st.id, ch: stCh }); + } else if (isStandaloneSf()) { + _routeNoteOff(st.id, { ch: stCh }, pitch, 'HARDWARE_KEYBOARD'); + } window.SonicSF.stopNote(stCh, pitch); // MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh // kẹt âm VSTi (giống keybed ảo). @@ -15833,6 +15851,11 @@ const App = () => { } } catch (err) { } }); + // T7: note-off dispatch qua router - engine voice giu tu note-on; + // SonicSF.stopNote o tren van giu belt-and-suspenders. + if (hwRouterOff && hwStopTargets.length) { + hwRouterOff.handleHardwareKeyboardMessage(msg.data, [], hwStopTargets); + } } } diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index 316a7c6..756adfc 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -817,27 +817,32 @@ const scaledVel=Math.min(127,Math.max(1,Math.round(rawVel)));if(cmd===0x9&&rawVe // đây chỉ keybed ảo gọi __ensureMasteringRouting). try{if(window.__ensureMasteringRouting)window.__ensureMasteringRouting();}catch(er){}// Route MIDI input to ALL armed tracks on their dedicated channels // 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(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;});// T7: UnifiedMidiRouter gom targets, dispatch 1 lan sau 2 vong. +var hwRouter=_getMidiRouter();var hwTargets=[];// 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=asTrk?assignTrackMidiChannel(asTrk,allTracks):0;var asProg=as.instrumentProgram;var asSe=as.synth_engine;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(as.trackId,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ // (tick giữ peak cho tới khi note-off) -try{heldMidiNotesRef.current[as.trackId]=(heldMidiNotesRef.current[as.trackId]||0)+1;}catch(err){}if(isStandaloneSf()&&isSfTrackEngine(asSe)&&!shouldRouteCarla(asSe)){if(!_routeNoteOn(asTrk&&asTrk.id,_makeCtx(asCh,asProg,asSe,null),pitch,scaledVel/127,60000,'HARDWARE_KEYBOARD')){window.SonicSF.playNote(pitch,scaledVel,60000,undefined,asProg,null,asCh,asSe);}}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,asProg,null,asCh,asSe);}// MIDI Keyboard → Carla bridge (piano roll sub-tab ARM): +try{heldMidiNotesRef.current[as.trackId]=(heldMidiNotesRef.current[as.trackId]||0)+1;}catch(err){}if(hwRouter&&asTrk&&asTrk.id){hwTargets.push({trackId:asTrk.id,ch:asCh,engine:new window.TrackInstrument(asTrk.id,_makeCtx(asCh,asProg,asSe,null))});}else if(isStandaloneSf()&&isSfTrackEngine(asSe)&&!shouldRouteCarla(asSe)){if(!_routeNoteOn(asTrk&&asTrk.id,_makeCtx(asCh,asProg,asSe,null),pitch,scaledVel/127,60000,'HARDWARE_KEYBOARD')){window.SonicSF.playNote(pitch,scaledVel,60000,undefined,asProg,null,asCh,asSe);}}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,asProg,null,asCh,asSe);}// MIDI Keyboard → Carla bridge (piano roll sub-tab ARM): // track VSTi của sub-tab phát VSTi realtime giống keybed. if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(asSe,true)){try{window.SonicCarlaMidi.noteOn(asCh,pitch,scaledVel);}catch(e){}}});}// Route to ALL armed tracks (not just the first one) armedTracks.forEach(function(at){var atCh=assignTrackMidiChannel(at,allTracks);var atProg=at.instrumentProgram;var atSe=at.synth_engine;var atDest=activeTrackNodesRef.current[at.id]?.gainNode||null;if(window.triggerMidiVuActivity){window.triggerMidiVuActivity(at.id,scaledVel);}// Giữ VU theo trường độ: tăng counter note đang giữ -try{heldMidiNotesRef.current[at.id]=(heldMidiNotesRef.current[at.id]||0)+1;}catch(err){}if(isStandaloneSf()&&isSfTrackEngine(atSe)&&!shouldRouteCarla(atSe)){if(!_routeNoteOn(at&&at.id,_makeCtx(atCh,atProg,atSe,atDest),pitch,scaledVel/127,60000,'HARDWARE_KEYBOARD')){window.SonicSF.playNote(pitch,scaledVel,60000,undefined,atProg,atDest,atCh,atSe);}}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,atProg,atDest,atCh,atSe);}// MIDI Keyboard → Carla bridge (track VSTi + ARM + Carla +try{heldMidiNotesRef.current[at.id]=(heldMidiNotesRef.current[at.id]||0)+1;}catch(err){}if(hwRouter&&at&&at.id){hwTargets.push({trackId:at.id,ch:atCh,engine:new window.TrackInstrument(at.id,_makeCtx(atCh,atProg,atSe,atDest))});}else if(isStandaloneSf()&&isSfTrackEngine(atSe)&&!shouldRouteCarla(atSe)){if(!_routeNoteOn(at&&at.id,_makeCtx(atCh,atProg,atSe,atDest),pitch,scaledVel/127,60000,'HARDWARE_KEYBOARD')){window.SonicSF.playNote(pitch,scaledVel,60000,undefined,atProg,atDest,atCh,atSe);}}else{window.SonicSF.playNote(pitch,scaledVel,60000,undefined,atProg,atDest,atCh,atSe);}// MIDI Keyboard → Carla bridge (track VSTi + ARM + Carla // local): phát VSTi realtime — giống hệt keybed ảo. Trước // đây chỉ keybed gọi SonicCarlaMidi; keyboard hardware bị // bỏ sót → track VSTi không kêu khi bấm đàn thật. -if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(atSe,at.isArmed)){try{window.SonicCarlaMidi.noteOn(atCh,pitch,scaledVel);}catch(e){}}});}}else if(cmd===0x8||cmd===0x9&&rawVel===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 +if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(atSe,at.isArmed)){try{window.SonicCarlaMidi.noteOn(atCh,pitch,scaledVel);}catch(e){}}});// T7: dispatch 1 lan qua router - keybed/scheduler/hardware +// chung 1 voice tracker (chong stuck note khi doi engine). +if(hwRouter&&hwTargets.length){hwRouter.handleHardwareKeyboardMessage(msg.data,hwTargets,[]);}}}else if(cmd===0x8||cmd===0x9&&rawVel===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){// Only stop channels that actually carry this track's notes — +if(window.SonicSF&&window.SonicSF.stopNote){var stopTracks=activeTracksRef.current||[];var hwRouterOff=_getMidiRouter();var hwStopTargets=[];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);if(isStandaloneSf())_routeNoteOff(st.id,{ch:stCh},pitch,'HARDWARE_KEYBOARD');window.SonicSF.stopNote(stCh,pitch);// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh +if(!st.synth_engine&&st.midiChannel===undefined)return;var stCh=assignTrackMidiChannel(st,stopTracks);if(hwRouterOff&&st.id){hwStopTargets.push({trackId:st.id,ch:stCh});}else if(isStandaloneSf()){_routeNoteOff(st.id,{ch:stCh},pitch,'HARDWARE_KEYBOARD');}window.SonicSF.stopNote(stCh,pitch);// MIDI Keyboard → Carla bridge: note-off khi thả phím — tránh // kẹt âm VSTi (giống keybed ảo). if(window.SonicCarlaMidi&&window.SonicCarlaMidi.shouldRoute(st.synth_engine,st.isArmed)){try{window.SonicCarlaMidi.noteOff(stCh,pitch);}catch(e){}}// Giảm counter note đang giữ — hết note → VU được phép decay/tắt // (tick sẽ thấy counter = 0 và không còn giữ peak nữa) -try{if(heldMidiNotesRef.current[st.id]!==undefined){heldMidiNotesRef.current[st.id]=Math.max(0,heldMidiNotesRef.current[st.id]-1);if(heldMidiNotesRef.current[st.id]===0){delete heldMidiNotesRef.current[st.id];}}}catch(err){}});}}// ── Sustain (CC64), Modulation (CC1), Pitch Bend ── +try{if(heldMidiNotesRef.current[st.id]!==undefined){heldMidiNotesRef.current[st.id]=Math.max(0,heldMidiNotesRef.current[st.id]-1);if(heldMidiNotesRef.current[st.id]===0){delete heldMidiNotesRef.current[st.id];}}}catch(err){}});// T7: note-off dispatch qua router - engine voice giu tu note-on; +// SonicSF.stopNote o tren van giu belt-and-suspenders. +if(hwRouterOff&&hwStopTargets.length){hwRouterOff.handleHardwareKeyboardMessage(msg.data,[],hwStopTargets);}}}// ── 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=assignTrackMidiChannel(ct,ccTracks);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=assignTrackMidiChannel(pt,pbTracks);window.SonicSF.pitchBend(ptCh,bendVal);});}else{window.SonicSF.pitchBend(midiCh,bendVal);}}}// Forward to active MIDI recorders diff --git a/app/static/js/services/unifiedMidiRouter.js b/app/static/js/services/unifiedMidiRouter.js index ac63514..d2845bc 100644 --- a/app/static/js/services/unifiedMidiRouter.js +++ b/app/static/js/services/unifiedMidiRouter.js @@ -99,6 +99,52 @@ window.SonicUnifiedMidiRouter = window.SonicUnifiedMidiRouter || {}; return false; // CC / unknown }; + // Hardware MIDI keyboard message → dispatch tới targets đã gom sẵn (armed + // tracks). app.jsx thu thập noteOnTargets [{trackId, ch, engine}] ở note-on + // và noteOffTargets [{trackId, ch}] ở note-off, gọi 1 lần mỗi message. + // parse giống handler cũ (cmd 4-bit, pitch clamp, velocity 1-127) rồi đẩy + // qua dispatchMidiEvent → 3 nguồn (keybed/scheduler/hardware) chung router. + UnifiedMidiRouter.prototype.handleHardwareKeyboardMessage = function (msg, noteOnTargets, noteOffTargets) { + var cmd = Number(msg[0]) >> 4; + var pitch = clampPitch(msg[1]); + var rawVel = Number(msg[2]) || 0; + var velocity = Math.min(127, Math.max(1, Math.round(rawVel))); + var noteOn = (cmd === 0x9 && rawVel > 0); + var noteOff = (cmd === 0x8 || (cmd === 0x9 && rawVel === 0)); + + if (noteOn && noteOnTargets) { + noteOnTargets.forEach(function (t) { + if (!t || t.trackId === undefined || t.trackId === null) return; + if (t.engine) this.registerEngine(t.trackId, t.engine); + this.dispatchMidiEvent({ + trackId: t.trackId, + command: COMMAND_NOTE_ON, + channel: t.ch, + pitch: pitch, + velocity: velocity / 127, + durationMs: 60000, + source: 'HARDWARE_KEYBOARD' + }); + }, this); + } + + if (noteOff && noteOffTargets) { + noteOffTargets.forEach(function (t) { + if (!t || t.trackId === undefined || t.trackId === null) return; + this.dispatchMidiEvent({ + trackId: t.trackId, + command: COMMAND_NOTE_OFF, + channel: t.ch, + pitch: pitch, + source: 'HARDWARE_KEYBOARD' + }); + }, this); + } + + console.log('[MidiRouter] hardware ->', { noteOn: noteOn, noteOff: noteOff, pitch: pitch, velocity: velocity, targets: (noteOnTargets ? noteOnTargets.length : 0) + '/' + (noteOffTargets ? noteOffTargets.length : 0) }); + return { noteOn: noteOn, noteOff: noteOff, pitch: pitch, velocity: velocity }; + }; + // All Notes Off: quét voice tracker, gửi note-off cho engine sở hữu từng // voice, xóa tracker. Gọi khi dừng playback / đổi instrument giữa chừng. UnifiedMidiRouter.prototype.panicAllNotesOff = function () {