Skip to content

feat: Loop de Cay additive overdub loop - #623

Draft
kosmar wants to merge 16 commits into
ATOVproject:mainfrom
kosmar:feat/loop-de-cay
Draft

feat: Loop de Cay additive overdub loop#623
kosmar wants to merge 16 commits into
ATOVproject:mainfrom
kosmar:feat/loop-de-cay

Conversation

@kosmar

@kosmar kosmar commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New 1ch app Loop de Cay: clocked additive overdub loop with per-wrap level decay (kill-floor to true 0)
  • Five I/O modes via Config: Pitch→MIDI, Gate→MIDI, MIDI→MIDI, MIDI→CV, Fader→Both
  • Gestures: Press = gate/play, Long = mute (no fader move), Shift+Short = arm, Shift+Long = erase
  • Alt = decay, Third = 1–8 bar window (crop/reveal + virgin tile); poly 4; RAM-only buffer
  • App ID 38 (avoids Hold Sam 36 / Harmonica 37 WIP IDs)

Test plan

  • Flash firmware and add Loop de Cay on a channel
  • Fader→Both: arm, play short notes with button+fader, confirm loop repeats on clock
  • Overdub adds notes; Alt decay fades velocity to silence; mute pauses decay
  • Shift+Long erases; Third shortens/extends bars (tile on virgin extend, reveal after crop)
  • Spot-check other modes (Pitch→MIDI, Gate→MIDI, MIDI→MIDI, MIDI→CV)

Prerequisites

The platform pieces this app needs are under review as their own PRs, so that
each app PR stays small enough to review on its own.

Required — these APIs do not exist on current main:

This branch is still based on an older main that still had the removed clock
helper, which is why it builds on its own but not after a rebase.

On top of the PRs above there is an app-side change: ClockEvent::Tick became
Tick(u64) in #579, so the match arm here has to be updated.

Building any of this on a current Rust nightly also needs the toolchain fix in
#632.

kosmar and others added 9 commits August 3, 2026 13:02
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Detect Shift+long with a timer, move mute/arm LEDs off the clock path,
re-arm on unmute, and re-anchor loop origin after Start/Reset so the
playhead does not stick after a clock restart.

Co-authored-by: Cursor <cursoragent@cursor.com>
Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
…ranch

Co-authored-by: Cursor <cursoragent@cursor.com>
Holding the button and moving the fader now slides through scale degrees
toward the fader position, matching the Chord Vamp gesture, so held notes
can be bent while the loop keeps running.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Carries the UX fragment (params, gestures including the Hold+Fader
glissando, prose) next to the app itself, matching how Bassment and Contura
ship their manuals on their own branches.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Mode 0 always opens a CV in jack, and resolve_pitch read that jack instead of
the fader whenever it existed — so the fader branch was unreachable and Span
had no effect there. An open input rests near 0 V, which pinned every note to
Base Note and made the fader look like it had a tiny range. Hold+Fader
glissando was dead for the same reason: its target never moved.

CV pitch and the fader offset now stack. Unpatched the fader keeps the span the
manual documents; with a cable it transposes, and the centre position is a
0-semitone no-op, which is also the saved default.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kosmar

kosmar commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Update: Pitch→MIDI fader + Span

e9822be — In Pitch→MIDI, Mode 0 always opens a CV in jack and resolve_pitch preferred that path, so the fader (and Span) never drove pitch when unpatched. Open CV rests near 0 V and pinned notes to Base Note; Hold+Fader glissando was dead for the same reason.

CV pitch and the bipolar fader offset now stack: unpatched the fader keeps the documented span; with a cable it transposes; centre = 0 semitones (also the saved default).

Test on hardware

  • Loop de Cay · Pitch→MIDI · Base Note C3 · Span 120 · no cable on the jack
  • Hold button + move fader → full ±60 semitone range (scale-quantized), not stuck on C3
  • Patch V/Oct: centre = unchanged CV pitch; move fader to transpose

kosmar and others added 2 commits August 13, 2026 20:29
The Hold+Fader glissando loop resolved a fresh target every millisecond
and retuned toward it unconditionally. In Pitch→MIDI the target includes
the CV input, so any movement there — a patched source, or just ADC
jitter across a quantizer step on an open jack — made retune_live close
and re-attack the note every 35 frames. A held button machine-gunned
instead of sustaining.

The gesture is Hold+Fader, so the chase now waits for the existing
fader_moved_while_held flag, which already has a movement threshold.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Playback started and ended notes on an exact `== pos` match. Since the
gatekeeper publishes ticks immediately, a subscriber that briefly falls
behind drops ticks instead of stalling the device clock, so `pos` can
jump over the position a voice was scheduled to end on. The note then
sounded until its slot was stolen by a later note — "sticks until one
after the next NoteOff".

Both the start and the end test now ask whether the loop advanced across
the target since the previous tick, wrap included.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kosmar

kosmar commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Pushed two fixes found while testing on the playground build:

  • fix(loop-de-cay): let a plain hold sustain again — the Hold+Fader glissando chased its target unconditionally, and since the target includes CV in, an unpatched jack jittering across a quantizer step retriggered the held note. A plain hold now sustains; the glide only engages once the fader has actually moved during the hold.
  • fix(loop-de-cay): end loop voices even when clock ticks are skipped — note starts and ends tested the loop position for exact equality. The clock gatekeeper publishes ticks immediately, so a subscriber that falls behind drops them rather than stalling the device clock, and a missed end position left the note sounding until its voice slot was stolen. Both now test whether the position was crossed since the last tick, wrap-around included.

Written and pushed by an AI coding agent on behalf of @kosmar. Verified to build; hardware test still pending.

kosmar and others added 5 commits August 13, 2026 21:09
Upstream PR ATOVproject#601 asserts at compile time that CONFIG strings are pure
ASCII, because postcard-bindgen JavaScript codec cannot round-trip anything
else. Doc and inline comments keep their typography.

Authored by an AI coding agent on behalf of kosmar.
Clock::get_ticker is gone and ClockEvent::Tick carries the tick number.
The app polls the counter from several places outside the clock arm, so it
uses App::clock_ticker (ATOVproject#646) and takes the tick from the payload where it
is already in scope.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Same mutate+save API as ATOVproject#640 so WIP apps can call params.update now.
Host AppState push stays out until the real writeback lands.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
ParamStore::update polyfill (mutate+FRAM, no host push).

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
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