fix: MIDI keyboard preview silent after transport STOP — live flag bypass guard (V8)

This commit is contained in:
2026-08-16 15:43:23 +07:00
parent 2d63774474
commit b6950d6418
11 changed files with 1450 additions and 1448 deletions
+8 -8
View File
@@ -9457,7 +9457,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
} }
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {} try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) }); window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && !kbNative) { } else if (window.SonicSF && !kbNative) {
// ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s) // ⚠️ FIX: giữ note theo thời gian bấm phím — durationMs lớn (5s)
// chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng // chỉ là auto-off phòng hờ; mouseup/mouseleave gọi stopNote dừng
@@ -9485,7 +9485,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
} }
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {} try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(kbTrk ? kbTrk.id : 0, kbCtx.program, kbCtx.synthEngine); } catch (er) {}
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) }); window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 100 / 127, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) { } else if (window.SonicSF && !(isStandaloneSf() && isSfTrackEngine(kbCtx.synthEngine) && !shouldRouteCarla(kbCtx.synthEngine))) {
// giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave // giữ note khi kéo qua phím (mouse enter) — dừng bằng mouseup/leave
window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine); window.SonicSF.playNote(pitch, 100, 5000, undefined, kbCtx.program, null, kbCtx.ch, kbCtx.synthEngine);
@@ -9504,7 +9504,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch); if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) }); window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch); } else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {} } catch (e) {}
if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; } if (window.__carlaKeybedTimer) { clearTimeout(window.__carlaKeybedTimer); window.__carlaKeybedTimer = null; }
@@ -9516,7 +9516,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos
try { try {
if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch); if (isStandaloneSf()) stopNativeSfNote('kb_' + st.trackId + '_' + pitch);
if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) { if (window.SonicMidiRouter && window.SonicMidiRouter.isBridgeActive()) {
window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128) }); window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: kbTrk ? kbTrk.id : 0, pitch: pitch, velocity: 0, percussion: !!(kbCtx && kbCtx.synthEngine && kbCtx.synthEngine.soundfont_bank === 128), live: true });
} else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch); } else if (window.SonicSF && window.SonicSF.stopNote) window.SonicSF.stopNote(kbCtx.ch, pitch);
} catch (e) {} } catch (e) {}
if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} } if (window.SonicCarlaMidi) { try { window.SonicCarlaMidi.noteOff(kbCtx.ch, pitch); } catch (e) {} }
@@ -15828,7 +15828,7 @@ const App = () => {
try { try {
heldMidiNotesRef.current[bt.id] = (heldMidiNotesRef.current[bt.id] || 0) + 1; heldMidiNotesRef.current[bt.id] = (heldMidiNotesRef.current[bt.id] || 0) + 1;
} catch (err) {} } catch (err) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127, percussion: !!(bt.synth_engine && bt.synth_engine.soundfont_bank === 128) }); } catch (e) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: bt.id, pitch: pitch, velocity: scaledVel / 127, percussion: !!(bt.synth_engine && bt.synth_engine.soundfont_bank === 128), live: true }); } catch (e) {}
}); });
} else if (window.SonicSF) { } else if (window.SonicSF) {
var allTracks = activeTracksRef.current || []; var allTracks = activeTracksRef.current || [];
@@ -15911,7 +15911,7 @@ const App = () => {
} }
} }
} catch (err) {} } catch (err) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0, percussion: !!(bst.synth_engine && bst.synth_engine.soundfont_bank === 128) }); } catch (e) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: bst.id, pitch: pitch, velocity: 0, percussion: !!(bst.synth_engine && bst.synth_engine.soundfont_bank === 128), live: true }); } catch (e) {}
}); });
} else if (window.SonicSF && window.SonicSF.stopNote) { } else if (window.SonicSF && window.SonicSF.stopNote) {
var stopTracks = activeTracksRef.current || []; var stopTracks = activeTracksRef.current || [];
@@ -21853,9 +21853,9 @@ const App = () => {
const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128); const isPerc = !!(track && track.synth_engine && track.synth_engine.soundfont_bank === 128);
const tch = track ? track.id : 0; const tch = track ? track.id : 0;
try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(tch, program, track && track.synth_engine); } catch (e) {} try { if (window.__ensureBridgeProgram) window.__ensureBridgeProgram(tch, program, track && track.synth_engine); } catch (e) {}
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc }); } catch (e) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_ON', channel: tch, pitch: pitch || 60, velocity: velocity, percussion: isPerc, live: true }); } catch (e) {}
setTimeout(function () { setTimeout(function () {
try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc }); } catch (e) {} try { window.SonicMidiRouter.pushEvent({ cmd: 'NOTE_OFF', channel: tch, pitch: pitch || 60, velocity: 0, percussion: isPerc, live: true }); } catch (e) {}
}, (durationMs || 500) + 30); }, (durationMs || 500) + 30);
return; return;
} }
File diff suppressed because one or more lines are too long
@@ -110,7 +110,7 @@
* velocity 0..1; sampleOffset in samples within the current audio block. * velocity 0..1; sampleOffset in samples within the current audio block.
* data2/data3 (A12): CC value / program / PB LSB|MSB. * data2/data3 (A12): CC value / program / PB LSB|MSB.
*/ */
dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3) { dispatchMidiEvent: function (cmd, channel, pitch, velocity, sampleOffset, data2, data3, live) {
if (!this._tauri()) return false; if (!this._tauri()) return false;
var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127); var safeVelocity = Math.floor(Math.min(1.0, Math.max(0.0, velocity)) * 127);
var byteCmd; var byteCmd;
@@ -127,7 +127,8 @@
velocity: safeVelocity, velocity: safeVelocity,
data2: data2 === undefined ? 0 : data2, data2: data2 === undefined ? 0 : data2,
data3: data3 === undefined ? 0 : data3, data3: data3 === undefined ? 0 : data3,
sampleOffset: sampleOffset || 0 sampleOffset: sampleOffset || 0,
live: !!live
}).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); }); }).catch(function (e) { console.warn('[BridgeService] push_midi_event:', e); });
return true; return true;
}, },
+1 -1
View File
@@ -54,7 +54,7 @@
if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion); if (typeof ch === 'string') ch = this.allocateChannel(ch, opts.percussion);
if (ch === undefined || ch === null) ch = 0; if (ch === undefined || ch === null) ch = 0;
if (this.isBridgeActive()) { if (this.isBridgeActive()) {
window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3); window.NativeBridgeService.dispatchMidiEvent(cmd, ch, pitch, vel, sampleOffset, opts.data2, opts.data3, opts.live);
return; return;
} }
if (this.onFallback) this.onFallback(cmd, ch, pitch, vel); if (this.onFallback) this.onFallback(cmd, ch, pitch, vel);
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1 -1
View File
@@ -32,7 +32,7 @@ struct SharedAudioBufferIPC {
uint8_t velocity; // 0-127 (0x9 + vel=0 == note off) uint8_t velocity; // 0-127 (0x9 + vel=0 == note off)
uint8_t data2; // CC value / program number / PB LSB uint8_t data2; // CC value / program number / PB LSB
uint8_t data3; // PB MSB (0xE only) uint8_t data3; // PB MSB (0xE only)
uint8_t reserved[2]; // explicit padding — keeps C/Rust layout stable uint8_t reserved[2]; // reserved[0]: live flag (1 = live input, bypass transportStopped guard); reserved[1]: 0 — keeps C/Rust layout stable
uint32_t sampleOffset; uint32_t sampleOffset;
} midiQueue[64]; } midiQueue[64];
+3 -2
View File
@@ -722,7 +722,7 @@ int main(int argc, char* argv[]) {
// truoc segment chua no (A11 splitting), nen offset tuong doi la 0. // truoc segment chua no (A11 splitting), nen offset tuong doi la 0.
// Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre. // Truyen offset tuyet doi truoc day lam sfizz/VST3 trigger tre.
if (evt.velocity > 0) { if (evt.velocity > 0) {
if (transportStopped) return; // V8 bug 3: drop note-on sau STOP if (transportStopped && !evt.reserved[0]) return; // V8 bug 3: drop note-on sau STOP, tru live input (reserved[0])
instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f); instruments.noteOn(evt.channel, evt.pitch, evt.velocity / 127.0f);
} else } else
instruments.noteOff(evt.channel, evt.pitch); instruments.noteOff(evt.channel, evt.pitch);
@@ -735,7 +735,7 @@ int main(int argc, char* argv[]) {
// gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu // gui CC64 xuong sau STOP (note-on/CC timer tre); VSTi giu
// note khi pedal down -> am treo loop. CC64=0 (sustain-up) // note khi pedal down -> am treo loop. CC64=0 (sustain-up)
// van cho qua. // van cho qua.
if (transportStopped && evt.pitch == 64 && evt.data2 > 0) return; if (transportStopped && evt.pitch == 64 && evt.data2 > 0 && !evt.reserved[0]) return;
instruments.controlChange(evt.channel, evt.pitch, evt.data2); instruments.controlChange(evt.channel, evt.pitch, evt.data2);
break; break;
case 0xC: // program change: program in data2 case 0xC: // program change: program in data2
@@ -1007,6 +1007,7 @@ int main(int argc, char* argv[]) {
for (uint32_t y : samePathSilenced) for (uint32_t y : samePathSilenced)
unmute_if_not_closing(y, "openGUI"); unmute_if_not_closing(y, "openGUI");
if (ok) { if (ok) {
disable_ime_contexts((HWND)hwnd); disable_ime_contexts((HWND)hwnd);
std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd std::cout << "[NativeBridge] GUI attached hwnd=" << hwnd
+2 -1
View File
@@ -667,11 +667,12 @@ fn push_midi_event(
data2: u8, data2: u8,
data3: u8, data3: u8,
sample_offset: u32, sample_offset: u32,
live: bool,
) -> Result<(), String> { ) -> Result<(), String> {
let state = app.state::<ShmState>(); let state = app.state::<ShmState>();
let guard = state.0.lock().map_err(|e| e.to_string())?; let guard = state.0.lock().map_err(|e| e.to_string())?;
let shm = guard.as_ref().ok_or("bridge shm unavailable")?; let shm = guard.as_ref().ok_or("bridge shm unavailable")?;
shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset) shm.push_midi(command, channel, pitch, velocity, data2, data3, sample_offset, live as u8)
.then_some(()) .then_some(())
.ok_or_else(|| "midi queue full".to_string()) .ok_or_else(|| "midi queue full".to_string())
} }
+3 -3
View File
@@ -34,7 +34,7 @@ pub struct MidiEventIPC {
pub velocity: u8, pub velocity: u8,
pub data2: u8, // CC value / program / PB LSB pub data2: u8, // CC value / program / PB LSB
pub data3: u8, // PB MSB (0xE only) pub data3: u8, // PB MSB (0xE only)
pub reserved: [u8; 2], pub reserved: [u8; 2], // reserved[0]: live flag (1=live input, bypass transportStopped guard); reserved[1]: 0
pub sample_offset: u32, pub sample_offset: u32,
} }
@@ -112,7 +112,7 @@ impl Shm {
unsafe { &mut *self.view } unsafe { &mut *self.view }
} }
pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32) -> bool { pub fn push_midi(&self, cmd: u8, channel: u8, pitch: u8, velocity: u8, data2: u8, data3: u8, sample_offset: u32, live: u8) -> bool {
let ipc = self.ipc(); let ipc = self.ipc();
if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP { if ipc.midi_queue_count as usize >= MIDI_QUEUE_CAP {
return false; return false;
@@ -125,7 +125,7 @@ impl Shm {
velocity, velocity,
data2, data2,
data3, data3,
reserved: [0u8; 2], reserved: [live, 0u8],
sample_offset, sample_offset,
}; };
ipc.midi_queue_count += 1; ipc.midi_queue_count += 1;