Skip to content

feat: add Venn dual-Euclidean boolean-logic app - #629

Draft
kosmar wants to merge 19 commits into
ATOVproject:mainfrom
kosmar:feat/venn
Draft

feat: add Venn dual-Euclidean boolean-logic app#629
kosmar wants to merge 19 commits into
ATOVproject:mainfrom
kosmar:feat/venn

Conversation

@kosmar

@kosmar kosmar commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Summary

  • New 2ch app Venn (ID 40): dual independent Euclidean layers combined with boolean logic (OR / AND / XOR / Accnt)
  • Gate-only (no pitch) — distinct from Euclid (single layer) and GenSeq (Turing + pitch CV); inspired by OXI ONE MKII GEN dual-layer Logic
  • Latch layers: Main = pulses A/B, Alt = rotation A/B, Third = length A/B (2–32, lengths independent for polyrhythm)
  • Shift+Btn0 cycles Logic; Shift+Btn1 toggles post-logic invert (shadow); Btn1 mutes both outs
  • Jack0 = primary logic result; Jack1 coupled per mode (overlap / symmetric diff / accent); MIDI Note A/B; Config: Division, GATE %, Prob %, Vel %, Color (Layer B auto-complement)

Test plan

  • Flash firmware and place Venn on two free channels
  • With clock running: scrub Main pulses A/B; hear/see Jack0/Jack1 and MIDI notes
  • Shift: rotate each layer; Btn0-hold: change lengths independently (XOR/AND get interesting)
  • Shift+Btn0 cycles OR→AND→XOR→Accnt; button LED color follows logic; Shift+Btn1 invert shadow
  • Mute (Btn1); Prob % < 100 adds flakiness; Prob 100 stays deterministic
  • Scene save/load restores pulses/rot/len/logic/einv/mute

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.

One more dependency is deliberately not proposed yet:
App::wait_while_perf_muted, which this branch calls. That mechanism does not
exist on main at all, and on the integration branch it is used by seven stock
apps — so it needs its own PR and its own review rather than riding along here.
The call will be dropped from this branch until that PR exists.

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

kosmar and others added 17 commits August 6, 2026 19:07
Two-channel gate app: independent Euclidean layers combined with
OR/AND/XOR/Accnt; live pulses/rotation/length via latch layers.

Authored by an AI coding agent on behalf of kosmar.

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

Co-authored-by: Cursor <cursoragent@cursor.com>
App::wait_while_perf_muted does not exist on main. The mechanism behind it
gates the shared MIDI path and is used by several stock apps on the
integration branch, so it needs its own PR and its own review rather than
arriving as a side effect of an app. Without a host perf mute on main there is
nothing for this call to wait on, so removing it changes no behaviour here.

Authored by an AI coding agent on behalf of kosmar.
Note-generating apps each carried their own idea of Root and Scale, so the
device-wide Key/Tonic — live on the Scene button plus Fader 4/5 — could not act
as a transpose. This adds one shared place to resolve it: the normalized device
Key, the tonic pitch class, a root retuned onto it keeping its octave, and a
combined call that costs a single GlobalConfig copy instead of two.

Resolving copies the whole GlobalConfig, so the module documents that callers
must cache per bar or phrase rather than per note.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Reading the device Key sat in the step path and copied the whole GlobalConfig on
every step. It is now resolved at the start of each A-cycle, which is also where
a scale change belongs musically, and scale and tonic come from a single call
instead of two copies.

Venn already followed the device scale unconditionally while its root stayed a
local param. Follow device tonic (default on) closes that gap, so both lines
transpose with Bassment, Contura, Chord Vamp and Arp de Levy.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Grooves, Chord Vamp, and Bassment each carried their own copy of these
helpers on their feature branches, and the copies had already started to
diverge: genre_palette gained GENRE_PROG_8 on some branches while others
still shipped the unused GENRE_COLORS table. Landing them once, ahead of
the apps, gives the three a single source of truth and lets each app PR
shrink to its own app file.

The files are taken from the flashed playground state, which is ahead of
every feature branch for groove.rs.

Purely additive: three new modules plus their declarations. register_apps!
is untouched and no existing app or platform file changes, so stock app
behaviour cannot be affected. The modules are dead code until the first
consumer app lands, hence the allow(dead_code) on the declarations.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The integration branch exposes the same counter twice, as Clock::get_ticker and
as App::clock_ticker; only the latter is on its way upstream. Both hand out the
same fn() -> u64 reading TICK_COUNTER, so this is a rename at the call site.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
clippy::chunks_exact_to_as_chunks fires on the constant chunk size in the USB RX
loop, and CI runs clippy with -D warnings, so main does not pass its own gate on
a current toolchain. Feature branches have been carrying this same one-line fix
inside unrelated app commits to stay green; landing it here means they no longer
have to.

as_chunks::<4>() yields &[[u8; 4]], so the packet indexing below is unchanged.
The discarded remainder matches the previous behaviour: chunks_exact dropped a
trailing partial packet too, and a USB MIDI bulk transfer is always a multiple
of four bytes.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
A stored AND/XOR/Accnt from an earlier session made a freshly spawned
Venn look dead, because those modes only fire where both Euclidean
layers coincide. Scenes still restore their own logic mode.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
The local RESOLUTION table duplicates libfp::utils::CLOCK_DIVISIONS.
Leave a pointer so whoever touches this next can drop the copy.

Authored by an AI coding agent on behalf of kosmar.

Co-authored-by: Cursor <cursoragent@cursor.com>
The step ran on `clkn.is_multiple_of(div)` and the gate closed on
`clkn % div == gate_off` — both exact-tick tests. Since the gatekeeper
publishes ticks immediately, a subscriber that briefly falls behind
drops them instead of stalling the device clock, and a missed gate-off
tick left the note sounding and the CV gate high until the next one
happened to land.

The step now fires when the division index changes, and the gate closes
on the first tick at or past its offset within the step.

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 fix(venn): close the gate even when clock ticks are skipped.

Step advance and gate-off both tested the tick number for exact equality. The clock gatekeeper publishes ticks immediately, so a subscriber that falls behind drops them instead of stalling the device clock — and a missed gate-off tick left the MIDI note and the CV gate stuck high until the next one happened to land. The step now fires when the division index changes, and the gate closes on the first tick at or past its offset within the step.

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

Ch2 was derived from the logic result as well, which made OR, AND and XOR
emit the identical onset set (a || b) with the two voices merely swapped
between the line and the Interval — indistinguishable over MIDI. Ch2 now
carries Layer B, so only Ch1 changes with the mode.

Spawn no longer forces OR either: run() is restarted on every param
change, so hardcoding OR there reset the mode (and overwrote the stored
value) whenever anything was edited in the configurator.

Authored by an AI coding agent on behalf of kosmar.

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

kosmar commented Aug 14, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: the logic modes were not audibly distinguishable over MIDI.

Ch2 was derived from the logic result too, so OR, AND and XOR all emitted the same onset set (a || b) — the two modes only swapped which voice (line vs. line + Interval) played at a given step. With the default Interval of m2 that is one scale degree apart, which is effectively inaudible. Ch2 now carries Layer B unchanged, so the mode only affects Ch1. Accnt keeps its accent voice (a && b).

Spawn also no longer forces OR. run() restarts on every param change, so hardcoding OR there reset the mode and overwrote the stored value whenever anything was edited in the configurator.

Hardware test checklist:

  • Spawn Venn, start the clock, defaults (length 16, pulses ~7 / ~3): both channels fire.
  • Shift + Btn1 cycles OR → AND → XOR → Accnt; each step is clearly audible. AND is noticeably thinner, Accnt follows Layer A only.
  • Raise Interval (Alt + F1) to a fifth: the two voices are clearly separated and the mode differences stand out further.
  • Pick a non-OR mode, then change any param in the configurator: the mode stays put and does not snap back to OR.
  • Save a scene in AND, load another scene and come back: AND is restored.
  • Gate jacks 1 and 2 follow the same pattern as the MIDI notes in every mode.
  • Shift + Btn2 (eInv) still inverts both outputs; Btn2 tap still mutes.

An m2 default put the two voices one scale degree apart, which made them
hard to tell apart and masked the logic mode along with them. The storage
default matches the new param so a fresh spawn and a fresh scene agree.

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