feat: sustain (CC64), modulation (CC1), pitch bend for MIDI keyboard

- Add controllerChange forwarder for all CC messages (cmd 0xB)
- Add pitchBend method to SonicSF + MIDI handler (cmd 0xE)
- CCs routed to _synthInstance.controllerChange(ch, cc, val)
- Pitch bend routed to _synthInstance.pitchBend(ch, 14bit-val)
This commit is contained in:
2026-07-27 11:02:45 +07:00
parent 7eb26a004f
commit 5a49d2012b
2 changed files with 47 additions and 19 deletions
+40 -19
View File
@@ -6753,25 +6753,26 @@ const App = () => {
const pitch = msg.data[1]; const pitch = msg.data[1];
const velocity = msg.data[2]; const velocity = msg.data[2];
if (cmd === 0x9 && velocity > 0) { if (cmd === 0x9 && velocity > 0) {
lastMidiNoteRef.current = { pitch, velocity, startTime: performance.now(), length: 0 }; lastMidiNoteRef.current = { pitch, velocity, startTime: performance.now(), length: 0 };
setLastMidiNote({ pitch, velocity, length: 0, time: Date.now() }); setLastMidiNote({ pitch, velocity, length: 0, time: Date.now() });
activeMidiPitchesRef.current.add(pitch); activeMidiPitchesRef.current.add(pitch);
setActiveMidiPitches(new Set(activeMidiPitchesRef.current)); setActiveMidiPitches(new Set(activeMidiPitchesRef.current));
{ if (window.SonicSF) { { if (window.SonicSF) {
const arSub = subTabsRef && subTabsRef.current && activeTabRef && subTabsRef.current.find(s => s.id === activeTabRef.current && s.type === 'PIANO_ROLL' && s.isArmed); const ch = msg.data[0] & 0x0F;
if (arSub) { const arSub = subTabsRef && subTabsRef.current && activeTabRef && subTabsRef.current.find(s => s.id === activeTabRef.current && s.type === 'PIANO_ROLL' && s.isArmed);
window.SonicSF.playNote(pitch, velocity, 500, undefined, arSub.instrumentProgram, null, undefined, arSub.synth_engine); if (arSub) {
} else { window.SonicSF.playNote(pitch, velocity, 500, undefined, arSub.instrumentProgram, null, undefined, arSub.synth_engine);
const armedTrack = activeTracksRef.current ? activeTracksRef.current.find(t => t.isArmed) : null; } else {
if (armedTrack) { const armedTrack = activeTracksRef.current ? activeTracksRef.current.find(t => t.isArmed) : null;
const prog = armedTrack.instrumentProgram; if (armedTrack) {
const se = armedTrack.synth_engine; const prog = armedTrack.instrumentProgram;
const dest = activeTrackNodesRef.current[armedTrack.id]?.gainNode || null; const se = armedTrack.synth_engine;
window.SonicSF.playNote(pitch, velocity, 500, undefined, prog, dest, undefined, se); const dest = activeTrackNodesRef.current[armedTrack.id]?.gainNode || null;
} window.SonicSF.playNote(pitch, velocity, 500, undefined, prog, dest, undefined, se);
} }
} } }
} else if (cmd === 0x8 || (cmd === 0x9 && velocity === 0)) { } }
} else if (cmd === 0x8 || (cmd === 0x9 && velocity === 0)) {
activeMidiPitchesRef.current.delete(pitch); activeMidiPitchesRef.current.delete(pitch);
setActiveMidiPitches(new Set(activeMidiPitchesRef.current)); setActiveMidiPitches(new Set(activeMidiPitchesRef.current));
const current = lastMidiNoteRef.current; const current = lastMidiNoteRef.current;
@@ -6780,6 +6781,26 @@ const App = () => {
lastMidiNoteRef.current = { ...current, length: lenSec }; lastMidiNoteRef.current = { ...current, length: lenSec };
setLastMidiNote(prev => prev && prev.pitch === pitch ? { ...prev, length: lenSec, time: Date.now() } : prev); setLastMidiNote(prev => prev && prev.pitch === pitch ? { ...prev, length: lenSec, time: Date.now() } : prev);
} }
}
}
// Sustain (CC64), Modulation (CC1), Pitch Bend
const midiCh = msg.data[0] & 0x0F;
if (cmd === 0xB) {
// Controller Change: forward all CCs to SpessaSynth
const cc = msg.data[1];
const val = msg.data[2];
if (window.SonicSF && window.SonicSF.controllerChange) {
window.SonicSF.controllerChange(midiCh, cc, val);
}
} else if (cmd === 0xE) {
// Pitch Bend: 14-bit value (LSB + MSB)
const lsb = msg.data[1];
const msb = msg.data[2];
const bendVal = (msb << 7) | lsb;
if (window.SonicSF && window.SonicSF.pitchBend) {
window.SonicSF.pitchBend(midiCh, bendVal);
}
} }
// Forward to active MIDI recorders // Forward to active MIDI recorders
@@ -154,6 +154,13 @@
return { ..._channels[channel] }; return { ..._channels[channel] };
}, },
pitchBend: function (channel, value) {
if (channel < 0 || channel > 15) return;
if (_initialized && _synthInstance) {
try { _synthInstance.pitchBend(channel, value); } catch (e) {}
}
},
playNote: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) { playNote: function (note, velocity, durationMs, startTime, program, destinationNode, channel, synthEngine) {
if (_initialized && _synthInstance) { if (_initialized && _synthInstance) {
this._playNoteSpessa(note, velocity, durationMs, startTime, program, channel, synthEngine); this._playNoteSpessa(note, velocity, durationMs, startTime, program, channel, synthEngine);