Skip to content

Realtime Block Events#71

Merged
nick-thompson merged 18 commits intodevelopfrom
nick/events
Sep 17, 2025
Merged

Realtime Block Events#71
nick-thompson merged 18 commits intodevelopfrom
nick/events

Conversation

@nick-thompson
Copy link
Copy Markdown
Contributor

This is a big change, but broadly it covers only a few small topics:

  • Unifying the BlockContext type, and adding a new BlockEvents input and output pair to that struct
  • Updating the graph rendering algorithm to allocate and distribute BlockEvents instances to the nodes for realtime processing with a pool-based algorithm that can reuse instances during traversal
  • Updating the graph rendering algorithm to use this same pool-based algorithm for assigning float buffers. On a handful of example graphs, this appeared to reduce memory usage for graph rendering as much as 85%

The canonical example for this BlockEvents struct is propagating MIDI events in realtime, so that individual graph nodes can consume/react to incoming MIDI as you might expect if you're used to working with something like JUCE. As such, this PR includes a couple of new midi utility graph nodes:

  • el.midinotein – a simple MIDI event pass-through node
  • el.midinoteallocate – a polyphonic note allocation node with LRU voice allocation and stealing
  • el.midinoteunpack – converts MIDI events to audio signals to propagate frequency and velocity information into the audio domain
  • el.midinoteshift – A simple MIDI utility which shifts the note value of any incoming note event

As an example, the following graph demonstrates writing an 8-voice polyphonic sine tone synth:

  let allocator = el.midinoteallocate(
    { voices: 8 },
    el.midinotein(),
  );

  let voices = Array.from({ length: 8 }, (_, i) => {
    let [freq, vel] = el.midinoteunpack({ channel: i }, allocator);
    return el.mul(0.1, el.sm(vel), el.cycle(freq));
  });

  let monoSynth = el.add(...voices);

And we can nudge this example to demonstrate modifying the MIDI event stream, in this case to generate simple root + 5th chords for any incoming note event:

  let allocator = el.midinoteallocate(
    { voices: 8 },
    el.midinotein(), // Propagate original note events
    el.midinoteshift({ steps: 7 }, el.midinotein()), // Propgate 5ths 
  );

  let voices = Array.from({ length: 8 }, (_, i) => {
    let [freq, vel] = el.midinoteunpack({ channel: i }, allocator);
    return el.mul(0.1, el.sm(vel), el.cycle(freq));
  });

  let monoSynth = el.add(...voices);

Passing events into the Elementary graph now requires that you fill a BlockEvents struct with the events you care about and pass that struct into the Runtime::process() call. In the web and offline renderers, there's a new method pushMidiNote for pushing an event into the queue, to be delivered to the realtime thread as soon as possible.

@nick-thompson nick-thompson merged commit 9849815 into develop Sep 17, 2025
8 checks passed
@nick-thompson nick-thompson deleted the nick/events branch September 17, 2025 15:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant