Spaces:
Running
Running
Commit ·
e805c10
1
Parent(s): b48cc75
feat(player): forward MIDI control-change events to FluidSynth
Browse filesscore-player now dispatches 'controller' events to the audio backend, and
LilyFluidAudio.controlChange forwards them to FluidSynth's sequencer so
sustain pedal (CC64), volume (CC7), pan, expression, etc. play back instead
of being dropped.
stopAllNotes also lifts the hold pedals (CC64/66/67) on every channel before
silencing, so a pedal-down whose pedal-off was dropped from the look-ahead
window (removeAllEvents on stop/seek) can't leave notes hanging forever.
- web/fluid-audio.js +26 -2
- web/score-player.js +1 -0
web/fluid-audio.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
| 7 |
*
|
| 8 |
* Shaped to match the subset of music-widgets' MidiAudio API that score-player.js
|
| 9 |
* drives: empty / loadPlugin / resume / noteOn / noteOff / programChange /
|
| 10 |
-
* stopAllNotes. score-player.js sends noteOn/noteOff with an absolute
|
| 11 |
* performance.now()-based timestamp; we forward to FluidSynth's Sequencer via
|
| 12 |
* sendEventAt(event, dtMs, isAbsolute=false) ("play in dtMs ms"), preserving the
|
| 13 |
* look-ahead timing without reconciling against the sequencer's own tick origin.
|
|
@@ -195,9 +195,32 @@
|
|
| 195 |
if (legacyReady && legacy) legacy.programChange(channel, program);
|
| 196 |
}
|
| 197 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
function stopAllNotes () {
|
| 199 |
if (seq) seq.removeAllEvents(); // drop the in-flight look-ahead window
|
| 200 |
-
if (synth) {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 201 |
if (legacyReady && legacy && legacy.stopAllNotes) legacy.stopAllNotes();
|
| 202 |
}
|
| 203 |
|
|
@@ -215,6 +238,7 @@
|
|
| 215 |
noteOn: noteOn,
|
| 216 |
noteOff: noteOff,
|
| 217 |
programChange: programChange,
|
|
|
|
| 218 |
stopAllNotes: stopAllNotes,
|
| 219 |
// Lightweight diagnostics: backend/AudioContext state. (A scheduled note with
|
| 220 |
// no thrown error does NOT prove audio output — for that, tap node with an
|
|
|
|
| 7 |
*
|
| 8 |
* Shaped to match the subset of music-widgets' MidiAudio API that score-player.js
|
| 9 |
* drives: empty / loadPlugin / resume / noteOn / noteOff / programChange /
|
| 10 |
+
* controlChange / stopAllNotes. score-player.js sends noteOn/noteOff with an absolute
|
| 11 |
* performance.now()-based timestamp; we forward to FluidSynth's Sequencer via
|
| 12 |
* sendEventAt(event, dtMs, isAbsolute=false) ("play in dtMs ms"), preserving the
|
| 13 |
* look-ahead timing without reconciling against the sequencer's own tick origin.
|
|
|
|
| 195 |
if (legacyReady && legacy) legacy.programChange(channel, program);
|
| 196 |
}
|
| 197 |
|
| 198 |
+
// Forward a MIDI control-change event (sustain pedal CC64, volume CC7, pan CC10,
|
| 199 |
+
// expression CC11, etc.) to FluidSynth, which honours them per-channel. Verovio
|
| 200 |
+
// emits these from MEI <pedal>/dynamics, so e.g. sustain-pedal spans play back
|
| 201 |
+
// correctly. The legacy MIDI.js fallback has no CC support, so CC is dropped
|
| 202 |
+
// while it is the active backend (only during the brief soundfont load).
|
| 203 |
+
function controlChange (channel, control, value, timestamp) {
|
| 204 |
+
if (seq) { seq.sendEventAt({ type: 'controlchange', channel: channel, control: control, value: value }, delayFromNow(timestamp), false); return; }
|
| 205 |
+
if (legacyReady && legacy && legacy.controlChange) legacy.controlChange(channel, control, value, timestamp);
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
function stopAllNotes () {
|
| 209 |
if (seq) seq.removeAllEvents(); // drop the in-flight look-ahead window
|
| 210 |
+
if (synth) {
|
| 211 |
+
for (var ch = 0; ch < 16; ++ch) {
|
| 212 |
+
// Reset the hold pedals BEFORE silencing: removeAllEvents() above may
|
| 213 |
+
// have discarded a not-yet-played pedal-off (CC64=0) that was waiting
|
| 214 |
+
// in the look-ahead window, leaving the channel stuck "sustain on".
|
| 215 |
+
// Any note played afterwards would then hang forever under the phantom
|
| 216 |
+
// pedal. Explicitly lift sustain (CC64), sostenuto (CC66) and soft
|
| 217 |
+
// (CC67) so the channel starts clean.
|
| 218 |
+
synth.midiControl(ch, 64, 0);
|
| 219 |
+
synth.midiControl(ch, 66, 0);
|
| 220 |
+
synth.midiControl(ch, 67, 0);
|
| 221 |
+
synth.midiAllSoundsOff(ch);
|
| 222 |
+
}
|
| 223 |
+
}
|
| 224 |
if (legacyReady && legacy && legacy.stopAllNotes) legacy.stopAllNotes();
|
| 225 |
}
|
| 226 |
|
|
|
|
| 238 |
noteOn: noteOn,
|
| 239 |
noteOff: noteOff,
|
| 240 |
programChange: programChange,
|
| 241 |
+
controlChange: controlChange,
|
| 242 |
stopAllNotes: stopAllNotes,
|
| 243 |
// Lightweight diagnostics: backend/AudioContext state. (A scheduled note with
|
| 244 |
// no thrown error does NOT prove audio output — for that, tap node with an
|
web/score-player.js
CHANGED
|
@@ -371,6 +371,7 @@
|
|
| 371 |
if (e.data.subtype === 'noteOn') A.noteOn(e.data.channel, e.data.noteNumber, e.data.velocity, ts);
|
| 372 |
else if (e.data.subtype === 'noteOff') A.noteOff(e.data.channel, e.data.noteNumber, ts);
|
| 373 |
else if (e.data.subtype === 'programChange') A.programChange(e.data.channel, e.data.programNumber);
|
|
|
|
| 374 |
}
|
| 375 |
}
|
| 376 |
updateHighlightsThrottled(state.currentTime);
|
|
|
|
| 371 |
if (e.data.subtype === 'noteOn') A.noteOn(e.data.channel, e.data.noteNumber, e.data.velocity, ts);
|
| 372 |
else if (e.data.subtype === 'noteOff') A.noteOff(e.data.channel, e.data.noteNumber, ts);
|
| 373 |
else if (e.data.subtype === 'programChange') A.programChange(e.data.channel, e.data.programNumber);
|
| 374 |
+
else if (e.data.subtype === 'controller' && A.controlChange) A.controlChange(e.data.channel, e.data.controllerType, e.data.value, ts);
|
| 375 |
}
|
| 376 |
}
|
| 377 |
updateHighlightsThrottled(state.currentTime);
|