C.W.K.
Stream
Lesson 04 of 05 · published

Web MIDI: Another Sink, Not Another Scheduler

~12 min · web-midi, one-scheduler, sinks

Level 0Cold Ash
0 XP0/33 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"When you add an output, branch at the leaf — never fork the scheduler."

The Feature: Play a Real Synth, Live

The v1 engine can play the solo and chords out through Web MIDI — driving a real software/hardware instrument (a synth fed over the macOS IAC bus) so it sounds in time alongside the in-app audio. That's a satisfying feature on its own. But the interesting part is how it was added, because the wrong way is so tempting.

The Tempting Mistake: Fork the Scheduler

The naive way to add MIDI output is to give MIDI its own scheduler — its own loop, its own timing — running alongside the audio one. Now you have two schedulers, and the two outputs drift apart exactly like the independent voices in the last lesson. You'd have re-created the jitter problem one level up. The fix you just learned (one clock) would be quietly undone by forking the decider.

The Right Way: One Scheduler, Many Sinks

Instead, the single scheduler still decides when. Outputs differ only in how they emit: a Web Audio sink plays a sample, a Web MIDI sink sends note-on bytes. Adding MIDI added a sink, not a scheduler. This is the API-first / single-source idea from Track 4, now at the playback layer: one decider, many consumers, and you branch only at the leaf where the actual emission differs. The proof it works: the same scheduler that drives the in-app audio also drives a hardware synth, in time, with no second timing engine.

Code

Branch at the sink, not the scheduler·javascript
// ONE scheduler decides WHEN (from the anchored clock of the last lesson).
// Outputs differ only in HOW they emit -- they are sinks, not schedulers.
function emit(note, audioTime) {
  if (audioOut.enabled) {
    playSample(note, audioTime);                  // Web Audio sink
  }
  if (midiOut.enabled) {
    midiOut.send(noteOnBytes(note), toMidiTime(audioTime));  // Web MIDI sink
  }
}

// Adding MIDI did NOT fork the scheduler -- it added a sink.
// One scheduler, many sinks: the API-first rule at the playback layer.

External links

Exercise

Take a system that produces output (a renderer, a notifier, an exporter) and imagine adding a second output channel. Would you duplicate the logic that decides WHAT/WHEN, or branch only at the final emission? If your instinct is to copy the whole pipeline for the new output, you're forking the scheduler — find the single leaf where the outputs actually differ and branch only there.
Hint
The decision logic (what, when, which note) should live once. Only the emission (to a speaker, to MIDI, to a file, to a webhook) is per-output. Copy only the emission; share everything upstream of it.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue
💛 by Pippawarm

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.