[WIP] Loop Jefe - #192
Draft
sastraxi wants to merge 2 commits into
Draft
Conversation
sastraxi
force-pushed
the
feat/loopjefe
branch
from
August 29, 2026 19:22
03a3aca to
df178ed
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Live looping on pi-Stomp. The device now shows a multitrack looper UI for loopjefe instances per footswitch binding. Each has a progress border that visualizes loop progression and a beat pulse that comes from the transport clock.
No looper-specific code is in the handler, the LCD, or the input path.
pprops:triggerport sends one rising edge, and does not toggle.LedSpec. The generic driver reads the plugin output ports and makes the color, the style, and the loop position.output_setframes that a loaded plugin asked for, and drops the rest.Before you start
Two things are external to this repository:
beat_syncmessage comes from our mod-ui fork. Stock mod-ui does not send it. Without it, the device falls back to the tap tempo (a free grid).A route through the code
Let's start at
BeatSyncMessage, which allows us to synchronize our visuals with the audio "beat".OutputSetMessageis required for us to observe changes from the loopjefe plugins (and other plugins, if we care to listen).BeatGridholds one anchor and extrapolates from it:Each new sample fully replaces the anchor. A late sample or a lost sample means more extrapolation, so in practice de-syncs should be rare. The downbeat is computed locally.
tick()produces aTickState. When there is no anchor, or when the anchor is more than 5 seconds old,tick()falls through to_free_tick()and uses the tap tempo. A free grid has beats but no bar, so it has no downbeat and no loop position.Per-plugin footswitch LED customization
LedSpecis the the plugin-facing contract for how it wants to drive its LED if it's bound to a footswitch:state_symbol— the output port whose integer value selects a color.downbeat_symbol— an output port that is 0 on the loop's own downbeat.bars_symbol— the loop length in bars. It is the denominator for the position. 0 means "not known yet".chase_states— states with a position but no denominator, such as a first take that is still recording.labels— the port is anlv2:OutputPort, so its scalePoints never reach us as aParameter. The names must be declared here.The renderer (
modalapi/led_render.py)Pure function of
(LedSpec, output_values) -> LoopProgress. The fourLoopFillmodes are the model of the feature:FILLCHASESTATICFREEHandler changes
:1072—BeatSyncMessageanchors the grid.OutputSetMessageat:1075writes toplugin.output_values.:666_drive_footswitch_leds()— called each 10 ms tick frompoll_controls. It ticks the grid one time and applies the result to every footswitch, so all LEDs pulse together.:718_render_footswitch()and:761_write_led()— where the color reaches the hardware.pistomp/footswitch.py:set_ledis now pure state, and it makes no hardware write. This side-steps a conflict between the LED driver and the pixel driver.:1415_update_interesting_outputs()— makes the subscription set fromplugin.monitored_output_symbolsand gives it to the bridge.:538and:566— the momentary path. If the boundParameteris a trigger port, the press callsParameter.pulse()(common/parameter.py) instead of a toggle.Websocket filter
Before this branch,
output_setwas dropped whole, because the audio meters flood it. Now the worker keeps the frames whoseinstance/symbolkey is in_interesting.Note that
_latest_outputsholds the last unsubscribed value per port. mod-ui dumps every monitored port at connect time, before the board binds. Without the replay, the first paint is stale until the plugin next moves.Painting
uilib/glyphs/perimeter_progress.py— draws an arc on the perimeter of a rounded rectangle.segmentsputs bar ticks around it.uilib/glyphs/loop_icon.py— the racetrack icon that replaces the plugin name whenloop_icon=True.Unrelated fixes
common/parameter.py). MOD-UI gives short property names in some paths and full LV2 URIs in others._has_property()accepts both. Before this, a plugin that reported full URIs got no type at all.ContextLayer.remove()(common/contexts.py) — the mutation counterpart toadd(), so callers do not touchrowsdirectly.TODO