Skip to content

MIDI objects, list / matrix / mask controls, MIDI batching and sysex in the ossia binding - #225

Merged
jcelerier merged 36 commits into
mainfrom
feature/midi-effects
Oct 28, 2026
Merged

jcelerier merged 36 commits into
mainfrom
feature/midi-effects

Conversation

@jcelerier

Copy link
Copy Markdown
Member

MIDI objects and binding work used by ossia score's MIDI effects branch.

New objects (examples/Advanced/MidiScaler): Midi transform, CC snapshot, value shaper, dispatcher, MPE, sustain, delay, random sequencer (Turing / Marbles), progression player (194 progressions, Roman numeral parser), Game of Life sequencer.

Extended: Midi filter (Types, channel mask, note / CC ranges, value range, remap tabs, Rejected outlet, note-offs follow their note-on), Midi scaler (MIDI output: per-note pitch bend or MTS sysex), Humanize (chance per chord / period, Euclid gate, random CC), MIDI Sync In (full-frame MTC, rates 29.97 / 30 fixed, tempo from timestamps, Clock out with divide / shift / delay), MidiGlide (note-offs on their note's channel, ends on stop). All MIDI objects get process_exec and release their notes on stop.

halp: value_list, note_matrix, channel_mask controls; drag_payload; timeline_clip (files placed together in one box).

ossia binding:

  • MIDI output batching per buffer is the default: MIDI sent before a loop point or a stop inside a buffer is no longer lost (local_midi_tick_batch false opts out).
  • MIDI 1 inputs reassemble SysEx7 packets into one message, across runs and buffers, without allocating.
  • Lists into fixed-capacity containers keep the elements that fit.

Depends on celtera/libremidi#268 (sysex reassembly helpers). Tested through score: 105 targeted test programs pass, including test_nodes_ossia_midi.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr

jcelerier and others added 30 commits October 25, 2026 00:51
Message kinds and range lists, a note tracker that pairs every note-on with
one note-off, a frame-dated event queue, a per-note channel allocator, euclid
and swing, and a chord-type / mode table (chord types from free-midi-chords,
MIT).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Midi scaler plays its Scala tuning as MIDI: one channel per note with a
pitch bend for the configured range, or MTS single-note tuning sysex.
Midi dispatcher spreads notes over a channel set (round robin, random,
ping-pong, chord orderings with a grace period, surplus channel).
Midi MPE converts MIDI to MPE and back, and shapes or randomizes per-note
expression.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Holds note-offs while a pedal (CC 64 or a toggle) is down, in All, Chord,
Semi-sostenuto and Sostenuto modes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Kinds list (including sysex and real-time), channel / note / CC range lists
with an invert, a value range, linear channel / note / CC maps, and a
Rejected output with everything that did not pass. Note-offs follow their
note-on's output and mapping when the settings change while notes are held.

The Channel control keeps the selected channel, as its label says; it used
to drop it. Each CC is sent once in CC mode; it used to be sent twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Midi transform turns one message kind into another, with byte sources,
insert / replace / drop modes and an input filter; produced notes always
get their note-off. Midi CC snapshot sends up to eight stored CC values,
immediately or with soft takeover.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A Clock out output carries the incoming clock and transport messages,
divided by N, shifted by N clocks at each Start, delayed by up to a second,
with or without SPP. The decoded outputs are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Note echoes, synced or in milliseconds, with repeats, velocity feedback, an
accumulating pitch step, dry / wet, fixed length and per-note expression
forwarded to the echoes. Every echo gets its note-off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Shapes velocity, release velocity, CC, pitch bend (in 14 bits) or pressure:
fixed value, rescale, curve, compression, gain with key tracking, offset,
seeded random and clip, in that order. Note-ons never become note-offs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Chance can be drawn per note, per chord or per period on the musical grid,
velocity can act as a probability, a euclidean gate drops notes on rests,
and one CC can get a seeded random offset. stop() releases what sounds.
Seeded takes are unchanged at the new controls' defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Sections, tabs and grids for Midi transform, CC snapshot, value shaper,
delay, MPE and Humanize, with the controls that do not apply to the current
mode greyed out. Humanize's columns are titled sections so nothing is
clipped. Labels: transform "Value 1/2", "Fixed channel", "Only channel",
byte ranges and modes; value shaper "CC or note", "Random add"; Humanize
"Random CC", "CC random range"; MPE "Release reserve (ms)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A note-off goes to the output its note-on went to, whatever the type
selection says: with Types "noteon", or the Note on filter type, the
note-offs of kept notes were sent to Rejected and the notes hung.
Layout in Basic / Select / Remap sections; labels "Types", "Invert ranges",
"Value range", "Value mode", "Outside map".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
With no surplus channel, a note that finds no free channel takes the one
released longest ago, even within its release reserve, or else ends the
oldest sounding note (in the chord modes, the one on the channel of the same
rank) and takes its channel. A legato chord change longer than the grace
time used to lose the whole new chord. Grouped layout; "Surplus channel",
"Grace (ms)", "Release reserve (ms)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
"Input bend range" (default 2) scales the incoming pitch bend; it was read
with the output Bend range, so a keyboard wheel bent 48 semitones with an
MPE synth setup. Grouped layout, MIDI output controls greyed in MTS mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- Rate code 2 is 29.97 fps and 3 is 30 fps, as the MTC spec says; they were
  swapped, also when picking the frame rate by hand.
- Full-frame MTC sysex is decoded (it was gated on meta events), for any
  device id.
- Sync Mode and Framerate start at MTC and clock / Auto in the host too, with
  readable labels.
- Grouped layout: Decode and Clock out sections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Progressions.hpp parses the chord-symbol notation of free-midi-chords
(diatonic numerals, mingus suffixes, accidentals, slash basses, rests) without
allocating, realizes symbols in a key and mode, and voices them with the
closest inversion. ProgressionTable.hpp holds the 194 progressions of
free-midi-chords (MIT) with their moods.

Midi progression player steps through a progression of the table or a custom
one every N beats (locked to bars), on a trigger or on each played note; it
plays voiced chords, snaps played notes to the chord or scale, or maps white
keys to chord tones, and outputs the current chord, degree and root.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Two engines, one object. Turing: a shift register of 1 to 16 steps whose
recirculating bit is redrawn with probability 1 - Lock, negative Lock giving
the doubled inverted loop; gates from register bits like the Pulses expander.
Marbles: per-step draws shaped by Spread and Bias, Deja vu replaying a loop of
Length steps, Density / Regularity gates and Jitter. Steps on the musical grid
(or free, or advanced by input notes or a Step impulse), quantized to a scale
weighted by Steps, swing, gate length, velocity range, accent, seed; Value
and Gate outputs to drive other processes.

Inspired by Music Thing Modular's Turing Machine (Tom Whitwell) and Mutable
Instruments Marbles (Emilie Gillet); no code taken.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A grid of up to 32 x 16 cells whose rows are scale notes (or a drum note
map); a play-head steps through the columns on the musical grid and plays the
live cells, or only the cells born this generation. The world evolves once
per sweep or every N steps, under Life-like rules (Life, HighLife, Seeds, Day
and Night, Maze, Move or custom B/S text), from a random fill, a classic
pattern or a drawn pattern, and reseeds itself when it dies or repeats. The
layer shows the grid live; a click toggles a cell. No allocation in the
processing path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
halp::value_list holds up to N numbers or aggregates of numbers in a
fixed-capacity vector, with optional column descriptions and count bounds.
halp::note_matrix maps the 12 degrees of an octave to output degrees, or
mutes them. halp::channel_mask holds the 16 MIDI channels as toggles.
List conversion from ossia keeps the first elements of a fixed-capacity
container instead of overflowing it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
…text

Dispatcher, Scaler and Filter channels are channel masks. The Filter's
channel, note and CC maps are rows of input and output ranges, up to four,
the first match applying. Transform's byte ranges are integer range
spinboxes, Life's note map a list of notes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
…ontrol, Sync

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Quarter frames received before a full frame could complete later and
overwrite the new position, and clocks counted before it were added on top.
A full frame resets the quarter-frame assembly and the clock interpolation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
…ixes

- Layouts: tabs and grids for Filter, Transform, MPE, Delay, Humanizer,
  CC snapshot, Value shaper, Life, Random sequencer, Progression player.
  Midi life's grid fills its view at every size.
- Proportions are 0-1 knobs (Delay feedback and dry/wet, Life gate, swing
  and density, gates, Value shaper gains 0-4 and 0-2).
- halp_flag(process_exec) on every object whose stop() releases notes:
  without it the host never calls stop().
- Transform: a produced note ends when What or Into changes. Dispatcher and
  MPE: note-offs passed through do not free a busy channel. Sustain: Chord
  mode ends the chord held at pedal down. Delay: expression echoes cannot
  crowd note echoes out of the queue (EventQueue reports drops, make_room).
  Value shaper: seed 0 keeps running across a stop. Life: no burst when
  switching sync off, swing on the grid's odd steps, no drift in free mode.
  Scaler: MTS sysex fits libremidi's inline storage on 32-bit targets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- MidiFilter: note-offs of notes rejected only for their kind go to the main
  output when note-offs are selected.
- MidiCommon: truncated channel messages classify as other; NoteTracker can
  end a note at a given time, used by the dispatcher when it steals a voice.
- Life, RandomSequencer, ProgressionPlayer: seeks come from transport() and
  from a rewind of the quarters; free steps follow the transport.
- MidiSyncInput: clock out follows Song Position through divide and shift,
  tempo from the clocks' timestamps, quarter-frame time as of the last piece.
- MidiMpe: RPN and NRPN sequences on member channels configure the input
  (RPN 0 sets the bend range) instead of reaching the single output channel.
- MidiValueShaper: "any controller" leaves bank select, RPN/NRPN and channel
  mode messages alone.
- MidiScaler: reads note bytes in place and reserves its frequency output.
- from_value: vecN into a fixed-capacity list keeps the elements that fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A node runs once per token, and a buffer holds several when a process
loops inside it or stops inside it. The outlet was cleared on each run, so
the MIDI sent before a loop point, or in the last partial tick before a
stop, was lost. Batching per buffer is now the default; the outlets and the
batch reserve local_midi_tick_batch_reserve packets, or the buffer size, and
local_midi_tick_batch false keeps the old behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
The sounding note's channel is kept with its pitch: a new note ends the
previous one on its own channel, a release ends it only when its channel
matches, all notes off clears it, and stopping the process ends it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Each MIDI input reassembles the SysEx7 packets it receives into one MIDI 1
message, across the runs of a buffer and across buffers, without
allocating; UMP inputs get the packets unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
jcelerier and others added 6 commits October 25, 2026 18:15
halp_meta(alignment, halp::alignments::left / center / right) on a
container aligns its children; on a grid each column is as wide as its
widest item, so their edges line up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
The free-running seed was reloaded unchanged on every reset, so each play
repeated the same take.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A grid value is {unit, value}: free seconds (0 is none), a note value
(a fraction of a whole note, 4 quarters), bars of the current signature,
or a multiple of the object's own period. The ossia binding reads and
writes it as vec2f, the time chooser's encoding, so a time chooser turned
into a grid chooser keeps its saved values.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
…s, no CC snapshot

- Midi filter: Types is a grid of toggles; Notes and CCs are rows of ranges.
- Midi transform and Midi value shaper output the value of each message
  they produce.
- Midi Glide: Priority, On release, Transition (retrigger or legato) and
  Latch, with a Hold button.
- Midi sustain: Pedal is a momentary button.
- Midi CC snapshot is removed: cues do this.
- halp::toggle_grid: a grid of named toggles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
…gain

The tuning goes with the RPN selecting tuning program 0 on all 16
channels, which receivers such as FluidSynth need to apply it; Send tuning
and playback start send it again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
@jcelerier
jcelerier merged commit 76d0d2b into main Oct 28, 2026
23 checks passed
@jcelerier
jcelerier deleted the feature/midi-effects branch October 28, 2026 13:26
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