feat: add Venn dual-Euclidean boolean-logic app - #629
Conversation
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>
|
Pushed 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>
|
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 ( Spawn also no longer forces OR. Hardware test checklist:
|
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>
Summary
Test plan
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:App::clock_ticker, which replaces theClock::get_tickerthis branch was written againstgenre_palette/groove/led_fxmodulesThis branch is still based on an older
mainthat still had the removed clockhelper, 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 notexist on
mainat all, and on the integration branch it is used by seven stockapps — 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.