Adds a cycle (loop) region: a range on the timeline ruler that the
transport wraps at during playback. This is the substrate for
GarageBand-style multi-take cycle recording (phases 2 and 3).
The region is authored in BEATS so it stays put musically across tempo
changes. It lives on the Document (saved in the .beam, serde-defaulted
so old files load) and is edited through SetCycleRegionAction, so it is
undoable and marks the document modified like any other edit.
Backend:
- Engine gains loop_region/loop_enabled plus a wrap at the single
playhead-advance point in process(). The wrap is phase-preserving
(modulo, so an overshoot larger than the loop can't strand the
playhead outside the region) and gated on playhead >= 0 so a count-in
pre-roll never wraps.
- Sounding voices are deliberately NOT reset at the wrap the way
Command::Seek does, since that would chop sustain and reverb tails at
every pass.
- MidiRecordingState::wrap_at_cycle writes note-offs for held notes at
the region end and re-opens them at the region start, so a key held
across the boundary can't end up with a negative duration or hang.
- loop_bounds_frozen freezes the region's sample bounds for the duration
of an *audio* recording. Phrased positively on audio so future
multi-track recording inherits it: MIDI is beat-segmented and
tempo-invariant, but audio is segmented geometrically and we have no
time-stretch, so cross-tempo audio takes wouldn't be compable anyway.
- Command::Play jumps to loop_start when starting from outside the
region; starting inside it plays from where you are.
Editor:
- Cycle lane along the bottom of the ruler (bottom, so it doesn't cover
the bar numbers), painted inside render_ruler under the ticks. Only
exists while looping is armed; with cycle off the ruler is entirely
the playhead scrubber, as before.
- Drag to create/move/resize with a three-zone hit test, previewed
locally and committed as ONE action on release. Driven off raw pointer
state rather than an egui Response: the lane sits inside the timeline's
content response, and a second widget on the same pixels just contests
hover every frame.
- snap_to_grid/quantize_grid_size take a min_grid_px "visual coarseness"
parameter instead of a hardcoded constant, with two named profiles:
SNAP_PX_FINE for the playhead and clip edges, SNAP_PX_CYCLE (coarser)
for the cycle region, so loops land on bars rather than odd
subdivisions.
- Cycle toggle button using the Lucide repeat glyph.
Cargo.lock picks up the 1.0.9-alpha version bump it missed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>