Skip to content

Pattern sequencer: steps with parameters, polymeter, an editor and a real-time safe engine - #2348

Merged
jcelerier merged 28 commits into
masterfrom
feature/pattern-sequencer-ui
Oct 18, 2026
Merged

jcelerier merged 28 commits into
masterfrom
feature/pattern-sequencer-ui

Conversation

@jcelerier

Copy link
Copy Markdown
Member

A rework of the Pattern sequencer (Patternist): what a step can hold, how lanes relate in time, a full editor with a Rings view, and an engine that is real-time safe and never leaves a note sounding.

Depends on ossia/libossia#962 (token_request::for_each_quantification_date). The submodule points at that commit.

Steps and lanes

  • Per step: velocity, velocity random, chance, timing, timing random, gate, ratchet (1 to 8), pace (bends the spacing of a ratchet's hits), pitch, pitch random, and a condition (fill, not fill, first, not first, pass A of B).
  • Ties hold the note struck before them. A tie strikes again when it ratchets or changes pitch, and its gate can end the note.
  • Per lane: its own length and rate (polymeter), velocity, mute and solo. For velocity, pitch and timing, the lane picks whether the whole note shares one random value or each hit of a ratchet draws its own.
  • Inputs: Fill, Density, Velocity, Swing, Ratchet (a continuous multiplier), Pace, and Repeat time (synced or in seconds).

Layer and node

  • The grid fills its layer. On the timeline, steps fall where they play, on the parent's bars and signatures.
  • Drag to paint, Shift to tie, right-click to clear. Mute and solo are in the lane menu.
  • Shorter lanes repeat across the longest one, drawn as editable cells. Notes show velocity, gate, timing, chance, ratchets and conditions.
  • Colours come from the skin and follow skin changes.

Editor

  • Opens as the process's UI (window, side panel or central view).
  • Pattern bank: duplicate, remove, and the playing and next patterns marked.
  • Toolbar:
    • select, copy, paste, clear;
    • rotate, random;
    • transpose by -12, -1, +1, +12;
    • steps and rate, add lane.
  • Lane cards: name, mute and solo, steps and rate. Steps and rate can be dragged, and double-clicked to follow the pattern.
  • Parameter bars:
    • one button per parameter, with Random and Reset;
    • Shift-drag moves all bars;
    • Alt-drag blends them towards a random state.
  • Step selection across lanes by time. Copy and paste land on the lane and step under the mouse.
  • Generators: random, Euclidean (replace, add, mask).
  • Rings: the lanes as concentric rings, each its own loop. The selected lane is emphasised, and the centre shows the notes struck while playing.

Engine

  • No allocation on the audio thread. Controls are clamped and NaN is ignored, both from the inlets and from the GUI.
  • Notes in flight are counted per note. Releases are ordered so no note is stranded. Lanes that are removed release their notes.
  • The event queue is sized for the worst case.
  • A transport jump repositions the pattern. Lanes added during playback, or given another rate or length, join in time.
  • The GUI reads what the engine publishes through atomics.

Loading and undo

  • Documents and pastes are brought into range: NaN, huge divisions, lane count, current pattern, channel.
  • An ongoing edit follows the pattern it is made on. Undoing a selection past the end gives the list back.

Tests

  • test_unit_patternist_node: engine, timing, ratchets, ties, spreads.
  • test_unit_patternist_edit: edits, layout, generators, transpose.
  • test_unit_patternist_port: ports, serialization, clipboard.
  • test_unit_patternist_robustness:
    • real-time allocation counting;
    • stuck notes;
    • hostile documents;
    • the GUI/engine race;
    • skin changes;
    • paint cost.
      Measurements and renders run on demand.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB

@coveralls

coveralls commented Oct 18, 2026 •

Copy link
Copy Markdown

Coverage Status

Coverage is 49.151% — feature/pattern-sequencer-ui into master. No base build found for master.

jcelerier and others added 28 commits October 18, 2026 10:53
Lanes share the layer's height and steps the width the note choosers
leave; drawing, hit testing and the choosers use the same layout, which
follows every resize.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The chooser can now follow the room its owner gives it and show the
note however the owner names it, on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
Layer
- In an interval the steps fall where they play: on the bar grid of the
  closest interval with a signature or a tempo, the grid starting over at
  every bar as in the engine. The first pass is lit, the repetitions are
  drawn darker to the end of the layer, as in the step sequencer. In a
  node the steps share the whole box.
- Click toggles a step, shift-click ties it, right-click on a note clears
  it; dragging paints over several steps as one undo step. Hover outline,
  bar and beat lines, a column on the step playing, and a note when another
  pattern plays or is about to.
- The context menu has copy, paste, clear, rotate, randomize, add lane,
  and mute, solo or remove on a lane header.

Editor
- The process UI, in a window or docked: the pattern bank (shown,
  playing, next), duplicate and remove, the pattern tools, steps and rate,
  add lane, mute and solo buttons per lane and step numbers.

Model and engine
- Lanes have mute and solo, applied by the engine, saved in documents.
- Lane labels (note and number, name, number, General MIDI drum) are a
  property of the process, chosen in the inspector.
- Every lane is at least as long as its pattern; the inspector lengthens
  them all, allows 1 to 128 steps, takes an integer rate, and no longer
  submits commands that change nothing.
- The executor reports the pattern playing and the one waiting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- The node and editor grids group steps in the beats and bars of the
  signature the process plays on, as the timeline does. Every interval up
  the chain is watched, so a signature added anywhere is followed.
- Beats are told apart by a darker shade on every other one, bar lines are
  faint: they no longer stand out over the steps.
- No M/S text in the compact view; mute and solo stay in the editor.
- The editor says what copy, paste, clear, rotate and randomize did, and
  disables Paste when the clipboard holds no pattern. Bank buttons keep
  their width whichever pattern is shown or playing.
- Looking for a pattern in the clipboard no longer goes through the
  logging JSON parser: right-click logged an error for any text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
Model
- Every step has a velocity, a chance, an offset (early or late, up to half
  a step), a gate, a ratchet count, a pitch offset and a condition: fill,
  not fill, first pass, not first, or pass A of every B.
- A lane can have its own length and division, a velocity, and a Euclidean
  rhythm (pulses, rotation) that replaces, adds to or masks its written
  steps; printing writes the rhythm into the steps.
- Fill, Density, Velocity and Swing inputs, for automation.
- Saved in documents; only what differs from the defaults is written.

Engine
- Each lane counts its own steps and passes, so lanes of different lengths
  or divisions run against each other.
- What a step plays is scheduled at its musical position and sent in the
  tick it falls in: gates and ratchets inside the step, late steps in a
  later tick, early steps from the step before.
- Without musical time, or rewinding, steps still play at once.

Editing
- Randomize works on a lane or on the whole pattern.
- Right-click on a lane name opens the lane menu instead of starting a
  drag of its note, which hid the cursor under the menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
Layer
- Each lane is laid out on its own length and division: in a node, the
  longest loop fills the box and the others repeat at their own speed; in
  the timeline every lane follows the bar grid at its own rate.
- A note's height is its velocity, its width its gate, its position its
  timing; ratchets split it, a note that may not play is faint, a corner
  mark shows a condition. Euclidean rhythms are drawn as they play.
- Each lane highlights its own playing step.
- Editing a step of a rhythm that replaces the steps writes the rhythm out
  first; over a rhythm that adds or masks, clicks edit the written steps.

Editor
- A selected lane (click its name or a step) with a bar lane under the grid
  for velocity, chance, timing, gate, ratchet or pitch, drawn by dragging.
- A lane panel: length, division, velocity, Euclidean hits, rotation and
  mode, write into steps, randomize lane.
- Right-click on a step: when it plays (always, fill, not fill, first, not
  first, pass A of every B), and reset its parameters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
The grid takes every click, so the layer was never focused, and score only
opens the layer menu of the focused layer: right-click on a lane name did
nothing until something else had focused it. The layer asks for focus on
press and its menu request waits for that focus, which arrives through a
queued connection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
When a lane's loop is longer than the others, the shorter lanes repeat
across the node: those repeats are now whole, editable cells, dimmed, and
the step under the mouse is outlined wherever it repeats. When lining the
lanes up would make steps too thin to aim at, each lane spreads its own
loop over the width.

The grid takes its colours from the skin: beats alternate between two
close tones (bars when a step lasts a beat or more), bars are marked by a
wider gap rather than a line drawn over the cells, and what plays is
tinted towards the notes' hue. Notes stay inside their cell.

The engine reports how many steps each lane played, so that the view
highlights the pass the time is on. Lane names are hidden when the lanes
get too thin for them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
Editor:
- Lane names in a larger font, with M and S under them when the lane is
  tall enough.
- The step parameters are chosen with buttons beside their bars, with
  Random and Reset (Shift: every lane) under them.
- Toolbar buttons light up under the mouse.
- Select mode, or Ctrl-drag: select steps over several lanes. Ctrl+C,
  Ctrl+X, Delete, Ctrl+A, Escape; Ctrl+V pastes the copied steps with
  their first lane and step under the mouse.
- Euclidean is a generator next to Random: hits, rotation, and whether
  it replaces, adds to or masks the steps. The live Euclidean lane mode
  is gone.

The grid keeps two cell tones in repeats; only their notes dim.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
A selection is the stretch of time dragged over: each lane gets the
steps under it, so lanes of different rates or lengths select what
plays at the same time, not the same step numbers. Copied steps paste
in each lane from the step at the time under the mouse.

Steps get a velocity and a pitch spread, drawn anew each time they play,
with their own bars, Random and Reset in the editor.

The editor's lane names use the toolbar's font, and Select comes first
in the toolbar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
A tied step played as one held note whatever its ratchet. Its first hit
now carries the tie, the others strike again, each released after its
gate, and the last is held into the next step. The grid marks those
hits on tied steps too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
1 to 8, for ratcheting the whole pattern from a controller while it
plays; a step strikes at most 16 times.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- Pace, per step and as an input: bends the spacing of a ratchet's hits,
  wide then tight or the other way round. The grid draws the hits where
  they fall.
- The Ratchet input takes fractions: the hits divide the step by the
  step's ratchet times the input, the last gap cut short by the next step.
- Repeat time input, a time chooser (synced or in seconds): when set, the
  steps that ratchet repeat at that interval instead.
- In the parameter bars, Shift-drag moves every bar by the same amount and
  Alt-drag blends them towards a random state, further for a longer drag;
  on the selected steps when there are some. One undo step each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- Controls go through setters that keep their value on anything that is
  not a finite number and hold it to its range, from the inlets and from
  the GUI alike: a NaN on Swing or Pace no longer fills the queue with
  events that never come due.
- No allocation on the audio thread: the queue and the step's releases
  sort with std::sort on an explicit push order instead of the stable
  algorithms; notes in flight are a fixed count per note; the grid points
  come from token_request::for_each_quantification_date.
- Notes in flight are counted per note: two lanes on one note share a
  voice, restarted when struck again and released by the last lane.
- A lane releases its held note after that note's note-on, so an early
  step landing on a late one no longer loses the release.
- What is due goes out before every grid point, the lanes' own divisions
  walked in time order with the pattern's: the queue holds a few steps at
  most, sized for the worst case, and keeps room for releases.
- Notes held by lanes that were removed are released.
- A transport moves the step counters to where the playhead lands.
- Solo is worked out once per tick instead of once per lane and step.
- The GUI reads what it draws from atomics the engine publishes at the
  end of every tick, never the pattern list being swapped, and repaints
  only when a step moved.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- normalize() gives the default to step and lane values that are not
  finite numbers (score's JSON reader accepts NaN), holds divisions to
  1/64 and lanes to 128, and clamps conditions: a document can no longer
  stall the audio thread with a huge division or freeze it with NaN.
- Every load path ends in sanitize(): at least one pattern, all
  normalized, the channel and the current pattern within range.
- Adding a lane stops at 128, the most the engine plays.
- Patterns made by selecting past the end, still untouched, go away when
  the selection comes back below them, so undoing the selection gives the
  list back.
- An ongoing UpdatePattern follows the edit when the shown pattern changes
  in the middle of a drag, instead of writing it into the first one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- The header width, which measures every lane's name, is worked out once
  and kept until the lanes, their names or the size change, instead of
  for every lane on every paint: 128 lanes paint in 2.2 ms instead of
  87.5 ms (Debug).
- The editor opens at 960 x 600; past what fits, its lanes scroll rather
  than get lower than 20 px, and the parameter buttons shrink before the
  bars do.
- Mute and solo buttons are left out of lanes too low to aim at them;
  they stay in the lane's menu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
test_unit_patternist_robustness keeps what the adversarial review found
as regression tests:
- no allocation on the audio thread;
- no note left sounding (early on late steps, two lanes on one note,
  removed lanes, a full step of ratchets, transport jumps);
- NaN and out-of-range controls, steps, divisions, selections and channels;
- undo of selections and of edits that change pattern mid-drag;
- the GUI's reads of what the engine publishes, against a writer thread;
- painting in linear time in the lanes.
Measurements and renders run on demand ([.][measure], [.][shots]).

The layer, its time grid, the editor, the command group and the step
serialization are exported so that the tests use the plug-in's own.

The engine emits what is due before a grid point only once the queue is
half full, which still keeps the order (points come in time order), and
ignores a division past 1/64 handed to it directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
In the editor, each lane's header is a card on its row, as tall as its
cells and separated by the same gaps, lit with the row when selected; the
parameter buttons sit on the same kind of card. With room for a third
row, the card holds the lane's steps and rate: dragged up or down,
double-clicked to follow the pattern again, dimmed while they do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
The palette and the fonts the layer derives from the skin are worked out
once and again only when another skin loads (Skin::LoadIndex), instead
of on every paint and for every button. A skin change remeasures and
repaints the layer, and rebuilds the editor's styles and background.

The parameter bars of steps without a note are drawn barely off the
background.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
A Grid | Rings toggle in the editor's toolbar shows the lanes as
concentric rings, lane 1 outermost, each its own loop, so lanes of
different lengths show as cells of different sizes. Cells, notes and
their parameters keep the grid's colours and encoding: velocity fills
outward, gate and timing along the ring, chance as opacity, ratchets as
slits spaced by pace, conditions as a dot. Each ring tints its playing
step.

The selected lane's ring is twice as thick and outlined, the others step
back, its step numbers go around the rings and its name, hits, steps and
rate sit in the middle; the lane list on the left lights its row. Clicks,
painting, ties and the step menu work as on the grid, and the parameter
bars stay underneath. There is no step selection on rings. Nodes and the
timeline keep the grid.

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

A tied step only holds the note struck before it, so its own velocity
does nothing unless it strikes again (after a rest, with a ratchet or
another pitch). The grid and the rings draw a tie as loud as the note it
holds, and the velocity bars of ties that do not strike are dimmed like
those of empty steps (tieStrikes, struckStep).

An Alt-drag in the parameter bars heads for values anywhere in the
parameter's range instead of the mild ones of the Random button: pitch
random reaches 24 semitones, not 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
A tie held its note until the next step whatever its gate, in the engine
and on the grid. A gate under a full step now releases the tied note
there, and the grid and the rings draw the tie that long.

Early notes were marked with a line the height of the cell at its start,
which read as a stray pixel above short or quiet notes: gone, the note
already shows its timing by where it starts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
With rings, the lanes are listed as the same cards as on the grid, with
their name, mute and solo, steps and rate, stacked from the top at the
height those need.

The tooltips, the status and the ring's caption use plain punctuation,
one item per line in the tooltips, and the button removing a pattern is
a plain hyphen.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- A lane added while the pattern plays, or given another rate, joins on
  the step it would be on had it always played, and a pattern given
  another length goes on from where it is, instead of starting over. The
  engine flags those when the new list comes in and moves only them on
  the next tick, keeping a count of the pattern's own steps since it
  started.
- Transpose buttons in the editor: every lane's note by -12, -1, +1 or
  +12, the accent and slide lanes left as they are.
- In a node, a right-click on the pattern opened the layer menu twice, one
  over the other, the hidden one taking the hover: the layer only takes
  focus on a click on the timeline, where that is what opens its menu.
  The mouse grab of the press is released when the menu opens.
- With rings, the middle shows the notes struck on the steps playing now,
  and the selected lane's name and length when stopped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
- Timing random, per step: each time the step plays, its timing moves by
  up to half a step either way, early included: the spread of a note is
  drawn a step ahead, so that one moved early is scheduled in time. It
  has its bars, Random and Reset like the other spreads.
- Each lane chooses, for velocity, pitch and timing, whether the whole
  note shares one random value or each hit of a ratchet draws its own;
  a hit's release is for its own pitch, and with pitch drawn per hit a tie
  holds whatever was struck. The choice is in the editor's lane panel.
- Grid and Rings sit on the pattern row, so the tools fit the editor; the
  random spreads are named alike.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
The deployment builds of Qt are configured with QT_NO_STYLE_STYLESHEET,
where setStyleSheet is not declared at all, which broke the AppImage and
Windows arm64 builds. The toolbar and pattern bank buttons draw their
hover, pressed and checked states themselves, in the skin's colours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
An accent step takes the velocity of every note on it part of the way to
the top, by its amount times the accent lane's depth; its outlet carries
the amount and drops back to 0 when its gate ends. A slide step holds the
notes on it into the next step, which strikes before the held note is
released, or carries it on when it is the same note; its outlet holds the
glide time until the note it glides into is struck. Both are decided once
per step, so the outlet and the notes agree, and a note scheduled early
finds the control step it belongs to.

The editor adds them from the Add lane menu, keeps them under the note
lanes, does not let their chooser turn them into notes, and shows only the
parameters they use. Gate random joins the step parameters, with its own
per-hit option, and the parameter buttons pair each value with its spread.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
Ctrl-click on a lane header picks it, the All lanes button over the
headers picks them all. With no step selected, Copy, Paste and Clear act
on the lanes picked: copied lanes go over the lanes picked, or from the
lane under the mouse down, each keeping its note, mute, solo and velocity.
With nothing selected, Copy and Clear do nothing. The lane menu copies,
pastes and clears its lane.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FCfP6rUUXFnGw6VoTiRCWB
@jcelerier
jcelerier force-pushed the feature/pattern-sequencer-ui branch from 184540e to 5f62864 Compare October 18, 2026 15:01
@jcelerier
jcelerier merged commit 9822fa3 into master Oct 18, 2026
41 of 42 checks passed
@jcelerier
jcelerier deleted the feature/pattern-sequencer-ui branch October 18, 2026 15:40
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