Skip to content

MIDI effects, audio and MIDI loopers, MIDI sync - #2361

Open
jcelerier wants to merge 83 commits into
masterfrom
feature/midi-effects
Open

jcelerier wants to merge 83 commits into
masterfrom
feature/midi-effects

Conversation

@jcelerier

@jcelerier jcelerier commented Oct 25, 2026 •

Copy link
Copy Markdown
Member

MIDI effects, loopers and MIDI sync.

Depends on

Merge these first, then bump the submodules here to their merged commits:

What it adds

  • Loopers: the audio Looper rebuilt on a shared transport (layers with undo / redo / erase / mute, feedback, extend, capture, latency compensation, follow tempo, reverse, x1/4-x4 speed, per-cycle comping, restart in phase), and a new MIDI looper (merge / replace overdubs, controllers per key, held notes, grace, quantize on playback, transpose, MIDI layers view). Record on a loop overdubs it, as on hardware loopers. Takes sent or dragged to the timeline play with the looper's Follow tempo (Off: unstretched). Takes are written as loop WAV / SMF files carrying their tempo, and sent or dragged to the timeline as one box with a looping slot per file.
  • MIDI effects: Arpeggiator (7 new patterns, gate, swing, accents, velocity decay, retrigger, latch, pressure, seed), Chord (43 chords, harmonizer, inversion, voice chance / velocity, strum, learned memory, MPE), Midi scale (direction, remap matrix, fold, range), Midi quantify (gate, trigger on release, decay, release velocity), LFO one shot. Ten new avendish MIDI processes registered, MIDI subcategories in the library.
  • MIDI Sync Out: free clock with tap tempo, nudge, clock rate, clock offset; one sender; Start, seeks and loops send Stop / Song Position / Continue with the Song Position on the receiver's next 16th; first clock with Start; MTC fixes.
  • Widgets: value list, note matrix, channel mask; long enums as drop-downs, range slider values, text field sizes.
  • Binding: object-to-UI messages reach every view without allocating; UI-to-object values and messages are made and freed on the main thread via the GC queue; one-shot UI updates retried when the queue is full; stop() deferred one tick so the last notes end.
  • Engine: no crash closing a document whose processes read a MIDI device; one process per dropped .mid (master made two); MIDI device input keeps sysex; driver latency in the execution state.

Behaviour changes from master

  • Midi filter's Channel keeps the selected channel (master dropped it): scores that set it filter the other way.
  • Midi scale custom masks count from Base; "custom" passes every note (master dropped them).
  • Chord, Arpeggiator, Midi quantify and the MIDI objects end their notes when the process ends.
  • Audio Looper: overdubbing an empty loop records it; overdubs are layers; at most Channels (2) and Max length (60 s); a take file is written per recording; late stop within 120 ms of a grid point. Quantif is now Quantize: its Whole becomes 1 bar; other note values are now real note lengths in every signature (a 4th in 3/4 is a quarter, not a third of the bar).
  • MIDI Sync Out / In: see above; 29.97 and 30 fps timing fixed.

New ports are appended; uuids, port order and saved enum values of existing objects are unchanged.

Release notes (for ossia-score.appdata.xml)

New features

  • loopers: the Looper is rebuilt and joined by a MIDI looper: layers you can undo, redo, erase, mute and solo; takes kept for comping; overdubs that extend the loop; reverse and tape speeds; capture of what was just played; loops that follow the tempo; a Switches tab for footswitches; and a Quantize that waits for the loop's restart, N bars, triplets or a time in seconds. Every recording is written as a loop WAV or MIDI file, which can be sent or dragged to the timeline as a box with a looping clip per layer.
  • midi: new objects: Midi transform, Midi CC snapshot, Midi value shaper, Midi dispatcher, Midi MPE, Midi sustain, Midi delay, Midi random sequencer (Turing Machine and Marbles), Midi progression player (194 progressions in Roman numerals) and Midi life (a Game of Life sequencer). Every MIDI object comes with presets.
  • midi: Chord gains a harmonizer, 43 chords, inversions, per-voice chance and velocity, strumming, learned chords and MPE output; Arpeggiator gains seven patterns, gate, swing, accents, velocity decay, retrigger, latch, pressure and a seed; Midi scale remaps degrees; Midi quantify gains gate, trigger on release and decay; LFO gains a one-shot mode.
  • midi: Midi filter selects by lists of types, channels, notes, controllers and values, remaps them, and sends what it rejects to its own outlet; Midi scaler outputs retuned MIDI, per-note pitch bend or MTS.
  • midi sync: MIDI Sync Out has a free-running clock with tap tempo, nudge, clock rate and clock offset; MIDI Sync In decodes full-frame timecode and sends a divided, shifted and delayed clock.
  • avnd API: list, note-matrix and channel-mask controls, drag payloads, sending files to the timeline, and alignment for layouts.

Improvements

  • midi: MIDI objects end their notes when their process ends, and MIDI sent before a loop point or a stop that falls inside a buffer is no longer lost.
  • midi: system-exclusive messages from MIDI devices reach processes whole, so full-frame timecode now drives MIDI Sync In.
  • midi sync: after a seek or a loop, MIDI Sync Out sends a Song Position on the receiving device's next 16th note and starts the clock there, at any clock rate; the first clock goes out with Start; 29.97 and 30 fps timecode is timed correctly, sent and received.
  • midi: system-exclusive messages from a graph port reach VST2, VST3, CLAP and LV2 plug-ins, MIDI devices and scripts whole; the Scala tuning object's MTS output selects the tuning on every channel and can be sent again.
  • midi: a MIDI file is placed by its beats, so a two-bar file is two bars whatever tempo it was saved at; dropping a MIDI file creates one process instead of two, and MIDI 1 pitch bend going through MIDI 2 keeps its centre.
  • midi filter: the Channel control keeps the selected channel; it used to drop it, so scores that set it now filter the other way.
  • midi scale: custom 0/1 masks count from the Base note instead of C, and choosing "custom" lets every note through instead of none.
  • looper: scores made with the previous Looper keep their settings; overdubbing an empty loop now records it, overdubs are kept as layers, a loop holds at most its Channels (2) and Max length (60 s), recordings are written as files next to the score, the old Restart setting is now Start from, Takes is now Export, Loop is Mode, Quantif is Quantize (its Whole becomes 1 bar while shorter values keep their note value in every signature), Bars is Record bars; the Sync setting is gone; Record on a loop that holds something now overdubs it (Clear first to record anew).
  • crashes: closing a score, or removing a MIDI device, while processes read a MIDI device no longer crashes.
  • widgets: long lists of choices are drop-downs, range sliders show their values, and grids keep their size when laid out again.

Tests

Full build; 105 targeted test programs pass (avnd, fx, looper, preset loader with 263 preset fixtures in tests/fixtures/presets, midi, execution, graphics, protocol, nodes). UI checked in the running app (screenshots of the loopers, take boxes, MIDI layers).

🤖 Generated with Claude Code

https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr

@coveralls

coveralls commented Oct 25, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 50.888% (+1.7%) from 49.183% — feature/midi-effects into master

@jcelerier
jcelerier force-pushed the feature/midi-effects branch from b261d1d to c368dd6 Compare October 28, 2026 03:52
jcelerier and others added 28 commits October 28, 2026 16:33
The executor requests a tick after stop() for objects with a MIDI output
and a stop() member, so they can send the note-offs of what they hold.
score_plugin_fx sees avendish's examples for the shared MIDI helpers, which
get their own unit test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Bumps avendish for the MIDI objects: Midi transform, CC snapshot, value
shaper, sustain, delay, dispatcher and MPE are registered; Midi filter,
Humanize, scaler and MIDI Sync In gain the features of the batch.

MIDI Sync Out gets a free-running clock with its own BPM and transport, tap
tempo, nudge, a clock rate and a clock offset; following score's timeline is
unchanged at the defaults. The time-dependent logic lives in
MIDIClockEngine.hpp so it can be tested with an injected clock. It ignores
the frameless tick that follows stop(), which would overwrite the song
position stop() resets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Chord: the 37 chord types of free-midi-chords after the six existing ones
(unchanged), harmonizer intervals, per-voice chance and velocity, strum,
chord memory and an MPE mode.
Midi scale: a per-degree remap mode and a direction for out-of-scale notes.
Arpeggiator: new patterns, gate, swing, accents, velocity decay, retrigger,
latch, pressure to velocity (note repeat), seed; non-note messages pass
through.
Midi quantify: gate, trigger on note-off, decay, release velocity.
LFO: one shot.
Every object releases what it holds on CC 120 / 123 and on stop, and the
existing controls keep their behaviour at the new controls' defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
One test per new or extended MIDI object: the features, every note-on
paired with one note-off under random input and control changes, CC 120 /
123, pass-through order, timestamps inside the tick, stop, and seeded
reproducibility; pinned outputs check that existing objects keep their
behaviour at the defaults.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- An enum with more than 8 values and no icons is drawn as a drop-down in
  process views instead of a grid of buttons.
- Text fields in process views draw their background, so an empty one can be
  seen and clicked.
- In the inspector, a control drawn through a graphics view (time choosers)
  is as tall as its item instead of taking the remaining height, and text
  fields are no longer capped at 70 pixels.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Range sliders in process views draw their two values, each in a color that
reads on the range or on the background. Combo boxes leave 8 pixels on each
side of their longest entry so it does not touch the marker or the stepper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- Grouped layouts for Chord, Midi scale, Arpeggiator and Midi quantify, with
  controls greyed when they do not apply; clearer labels ("Voice chance",
  "Voice velocity", "Strum direction", "Latch hold"), descriptions for the
  Midi scale degrees.
- Midi scale: a custom mask starts on Base like the built-in scales, and a
  scale text that is not a mask (picking "custom" in the list) keeps every
  note instead of dropping them all.
- Arpeggiator: the Up/Down, Down/Up, Converge, Diverge, Low Up and High Up
  patterns run across all the octaves instead of repeating inside each one;
  the most recent kind of pressure sets the velocity.
- Midi quantify: a note on a grid point plays on it instead of waiting a
  step (the frame to musical time conversion is not exact).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
MTC rate code 2 is 29.97 fps and 3 is 30 fps: the codes sent were right but
the timing used the other rate. Layout in Send / Clock / Free clock sections,
the free clock greyed unless the source is Free.
avendish: MIDI object layouts, Midi filter note-off routing, dispatcher
voice taking, Midi scaler input bend range, MIDI Sync In fixes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
MidiPresetHarness loads a package's .scp presets and applies them to an
object's inputs the way the avnd process maps them (entry N is input N;
synced times converted to seconds as the ossia binding does). One test per
object checks that every preset applies cleanly and does what its name says,
with notes paired and timestamps inside the tick. Dispatcher and filter
tests follow the new voice-taking and note-off routing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Registers Midi random sequencer (Turing Machine / Marbles style), Midi
progression player (with the free-midi-chords progressions) and Midi life (a
Game of Life sequencer), with their unit tests and the tests of their presets
in the default package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A list control edits rows of the existing spinboxes and combo boxes: +
appends a copy of the last row, x or - removes one, the context menu
inserts, duplicates, moves and clears, and the whole list can be edited as
text. The note matrix maps 12 input degrees to output degrees, with muted
degrees; the channel mask toggles the 16 MIDI channels. They bind to
halp::value_list, halp::note_matrix and halp::channel_mask.

A control that changes size lays out again the sections around it, and the
inspector view follows a list's size. Integer range spinboxes show integers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Chord's intervals, voice chances and voice velocities are number lists and
its MPE channels a channel mask. Midi scale's twelve remap offsets become a
note matrix in the way of Ableton's Scale, with muted degrees, Fold, and a
Lowest / Range band outside which notes pass unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Sequencers (piano roll, pattern sequencer, Patternal, Melodial, file
reader and scroller), Generators, Pitch, Timing, Routing, Control (CC tools
and MIDI to and from values) and Sync (MIDI clock and MTC, from Timing/Midi).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A preset naming an entry ("Toggle") of a combo box whose entries have
values reached the engine, which maps labels, but the widget matched values
only and showed the first entry.

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

The edition and GC queues' try_enqueue called enqueue, which can allocate:
they use the real try_ versions and fail when full. Messages too large for a
command go through a fixed ring per node; controller updates from the audio
thread go through a lock-free latest-value buffer.

An interval usually ends inside a buffer, and its last partial tick runs
after the process is stopped. Objects that release their notes on stop get
their stop() at the start of the extra empty tick, so the notes that last
tick starts also end.

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

With a bound device, the clock and MTC threads and the graph both wrote to
it. The audio thread now computes every message once and the clock thread
sends them at absolute times, without drift. A loop or seek is a jump: Stop,
Start or Song Position and Continue, clock phase realigned, a full MTC frame.
A positive clock offset delays the transport messages with the clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A value arriving between a row removal and its deferred rebuild read rows
past the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- Chord: voice chance and velocity lists in 0-1 and 0-4; chord names that
  were digits renamed (an int selected the wrong entry); Learn no longer
  allocates on the audio thread.
- Midi scale: the page follows Mode; the scale is kept as notes, no string
  copy on the audio thread.
- Arpeggiator: the engine builds arpeggios without allocating; gate, swing
  and velocity decay are 0-1 knobs; compact layout.
- Quantify: gate and decay knobs.
- LFO: One shot plays again when its process starts again.
- halp_flag(process_exec) on Chord, Arpeggiator v2, Quantify v2 and LFO v3,
  whose start() / stop() the host only calls with it.

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

avendish: compact layouts, 0-1 proportions, process_exec, review fixes,
MIDI Sync In full frames. libossia: libremidi keeps a pitch bend's center
through MIDI 2 (it was scaled as 16 bits). Tests for each fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
The audio Looper keeps its uuid and controls; its transport moves to a core
shared with a new MIDI looper. Passes are layers of their own with undo, redo,
clear and feedback; buffers are reserved before playback. A take is written as
a looping WAV or a MIDI file when a recording or overdub ends, and goes to the
timeline by Send to timeline or by dragging it from the looper's view.

Custom items can start drags and place clips in the timeline, and every view
of a process gets the processor's messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Loopers of one sync group share a loop: the first to record leads, the
others round their length to a multiple or a fraction of it and go round in
phase. Capture turns the last bars heard into the loop without having
recorded them.

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
The audio looper loads a dropped sound file as its loop, and goes round with
nothing patched in. Undo gives the passes under an overdub the gains feedback
took from them, and later laps of an overdub fade its own earlier ones. Sync
groups start their loop where it restarted, also after a late stop. MIDI notes
held past the loop and notes replaced while sounding end; take paths, dates
and names are kept per take.

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

A take can be written as the loop's mix, each pass in a file of its own, or
both; sending or dragging it places every file on a lane of its own.
Overdubbing an empty loop records it. A loop keeps the tempo it was recorded
at for its quantize grid and its takes. Quantizing note ends no longer
shortens a note to nothing. Rate limiters are reset when their process stops.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Past the end: an overdub goes round the loop (Wrap) or makes it longer by the
loop's length each time it reaches the end, the passes under it repeating
(Extend); undo gives the loop its length back. The MIDI looper plays its
loop at the score's tempo when Follow tempo is on, the default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
jcelerier and others added 27 commits October 28, 2026 16:34
QGraphicsGridChooser keeps the time chooser's knob and readout; a click on
the readout picks the unit (free time, note value, bars, the object's
period) and the feel. Process::GridChooser saves its configuration with
the port. The note table moves to NoteValues.hpp, shared by both widgets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
near is a macro in the Windows headers. A readout wider than the chip
("1/2 loop", "16 bars") is drawn smaller and narrowed instead of cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
Quantif takes free time, a note value, bars or the loop (1/4, 1/2, 1, 2x...).
On an empty loop, Loop waits for Bars bars, or one bar when Bars is 0.
Note values are whole-note fractions in every signature. Old scores and
presets convert: None stays none, Whole becomes 1 bar, any other value
the same note value. Presets saved when a control was of another kind now
go through its upgrade_value, as documents do.

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

Every change waits for the loop to come round; the first recording, with
no loop yet, ends on a bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A MIDI looper take writes the tempo and time signature it was recorded at.
score's timeline counts a quarter as half a second whatever the tempo, so
a dropped MIDI file is placed by its quarters, not by its file tempo: two
bars stay two bars.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- Sync groups are removed, with their presets.
- Record on a loop that holds something overdubs it; Clear starts anew.
- Stopping the score while recording or overdubbing leaves Mode on Play,
  so playing again no longer records over the loop.
- Loop is Mode, Quantif is Quantize, Bars is Record bars. On the MIDI
  looper, Grid is Note grid (note values, no None), its Quantize is
  Strength and Quantize ends is Note ends. Follow tempo has readable labels.
- Old scores and presets load with the new names; a score from master gets
  the grid its Quantif waited for, not seconds.
- avnd presets name their controls, and a rebuilt process keeps its port
  ids, so presets and cables survive a removed port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
PassStack.hpp holds what a layer is (period, loop before and after, mute),
undo, redo, erase, merges, set-aside and unmerge, for any payload;
LooperNode.hpp holds the tick, the layer actions, closing and trimming
takes, capture and export. Each looper keeps only its medium.

Where the two loopers differed, they now agree:
- solo is forgotten when a merge completes;
- material cut from a recording by its bars is carried into the overdub;
- merging with a muted layer keeps it for undo, and two muted layers merge
  into a muted one that keeps both;
- a take written when a pass closes leaves out the pass begun with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A take sent or dragged to the timeline carries its stretch mode, from the
Follow tempo the loop played with; Off plays it unstretched at its recorded
length. The metadata travels in an application/x-ossia-take mime next to
the files, applied by the Sound drop handler for both Send and drag. A
plain file drop keeps the mode guessed from its name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
One value inlet and outlet. Impulses fire where they were played and
merge; every other value is held until the next, and an overdub takes over
from its first new value to its end (latch). Whatever changes the held
value -- mute, erase, undo, the end of a pass, a jump -- sends the newly
held one. Each value carries a key, 0 for now, for several sources later.
Stored values go through one value_slot type. Takes are .values files; a
take of numbers or vectors lands as Automation curves.

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
- A paused or stopped looper no longer leaves its view waiting for a take
  forever, which stopped every drag.
- Dragging a lane drags that layer's own file, written on demand when the
  take has none; the Send button sends on click and drags the whole take.
- Lanes share the view's height, up to 32 lanes.
- MIDI and value loopers: an overdub becomes a layer on its first event; a
  pass that records nothing adds no layer, take or merge.
- Value looper: takes of numeric lists land as one automation per index.
- Tabs: Speed (Speed, Reverse, Follow tempo and their switches); Capture
  and Capture bars in Length; Keep last in Memory; Switches holds One
  button and Transport.

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
…inlet MIDI registration

Parameter, node, pattern, empty and non-MIDI addresses, rebinding across
devices, two inlets on one device, double registration, a removed device,
reset, and devices added and removed within one batch.

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

- Chord: Memory is a list of rows (C4: 0 4 7), learned without allocating;
  old text converts.
- Midi filter: Types as a toggle grid (QGraphicsToggleGrid, which the
  channel mask now builds on), Notes and CCs as range rows; old text
  converts in scores and presets.
- Midi transform and Midi value shaper: Value outputs. Midi Glide modes.
  Midi sustain's Pedal is a momentary button.
- Midi CC snapshot removed; a score holding one still loads.
- Control values larger than the execution queue's inline size travel
  through the heap path built on the UI thread.

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
VST2 gets VstMidiSysexEvent, VST3 a kMidiSysEx data event, CLAP
CLAP_EVENT_MIDI_SYSEX, LV2 one MIDI 1 event per message; the bytes live in
storage made beforehand for each node. MIDI 1 devices join the packets
(libremidi), JS scripts send and receive sysex as one message.

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
On an empty loop Capture still makes the loop of the last Capture bars.
Over a loop it now adds what was played during the last round as a new
layer, kept with the other layers' mutes, and Undo takes it out like any
pass. MIDI tests also cover Replace across three layers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A take dragged from a looper went through the plain MIDI file drop: dropped
on an existing box, e.g. the looper's own, it took the box's length and did
not loop, which made a four bar loop an eight bar piano roll. Takes now
carry loop=1 in their clip metadata, from the view's drags and from Send,
and the MIDI, value and sound drops set the clip to loop at the take's
length, in the same undo step. A .mid from a file browser is unchanged.

libossia: a tick split at a date keeps its musical positions, so a looper
in a box that does not start at 0 records the right number of quarters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
- Replace as you play: Passes Per cycle, so each round played over
  replaces the one before; it overdubbed one pass across rounds, and a
  pass never replaces itself.
- One footswitch, song form stops at the loop's end, as Song form.
- Comping, a layer per press exports the mix and each layer, as per round.
- MIDI Four bar phrase and Free phrase are Four bars, then play and Free
  loop, as for audio.
- Capture what I played (MIDI) and Capture what moved (values) are gone:
  they set nothing Capture needs. The audio one stays, for Keep last.
- Channels is no longer set by audio presets: it is the input's.
- Quantize and Note grid are stored in the grid chooser's format.

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

A node lays its layer out over its width, at the zoom of what it shows: a
loop when the process loops. updateSize, which a rename runs, set the width
alone, which makes the layer draw the whole process over the node. A take
dropped in the nodal view, named after the take once its loop was set,
showed its four bars squeezed into the first half of a box of eight.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
After a stop no node is left to write the files a drag or Send asks for, and
Send placed the last take as written, whatever Export said since. Now:
- the score's stop leaves a take with the mix and every layer, unless the
  last one is already whole: the audio thread hands a preallocated snapshot
  to the worker, which writes the files and tells the views through a host
  sender that reaches them once the node is gone;
- Export only chooses what Send and the grip place, read from the control
  on the UI side (a custom item's watched() controls), so it holds while
  stopped and when changed after the take was written;
- a deferred stop that never got its tick runs as the node is destroyed,
  and a worker job marked must_run runs once its object is gone.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
@jcelerier
jcelerier force-pushed the feature/midi-effects branch from a8171ce to 5061ebb Compare October 28, 2026 20:41
jcelerier and others added 2 commits October 28, 2026 21:36
The painter gives the width of a text, so that a custom item can right-align
one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qr9WkcUjEGCueg7eG1KxSr
A pass per cycle is cut on the loop's start while what is played goes on:
both ends were faded as a punch is, a 128-frame dip to silence on every
seam, heard at each new pass and on every round after. Each pass now keeps
where its ends meet the passes before and after it, and an end is faded only
while the pass on the other side is not heard (muted, soloed out, undone,
erased, cleared, merged away); the recording's end into its overdub is the
first of these seams.
Also at a cut, or on every round:
- Feedback glides the gains of the passes under the overdub instead of
  stepping them, which made each seam jump by 1/feedback once played.
- An overdub pass is written already moved by the latency, instead of moved
  when it closes, which left the last positions of each round silent.
- Below x1, the overdub no longer hears back what it just wrote.
- A pass begun after a merge into the newest layer gets a buffer of its own.
- A pass that went round over its start fades what it last wrote at its end.

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

This branch has not been deployed

No deployments
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.

2 participants