(feat): add Contura melodic contour app - #634
Conversation
Selectable 12-TET scale sets on equal footing; no culture-family framing. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
…contura Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # faderpunk/src/app.rs
… grammar 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>
Mirror Grooves/Bassment Jack Out/In + Dest + Att so Contura can take patched LFO CV without losing MIDI; Out remains pitch CV. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
The Interval fader now opens up to two or three octave leaps across the pool and Density densifies audibly from the middle instead of only near the top, so both faders read musically over their whole travel. try_send_note_on and try_send_note_off report whether the app MIDI queue accepted the message so the voice can retry instead of silently dropping a note or leaving one hanging. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
…e param Jack and CV Dest spent two params on a choice that was really one: a CV In destination is dead while the jack drives CV Out, and vice versa. They become a single enum — CV Out, CV In Density, CV In Interval, CV In Reset — matching Grooves. Stored setups fold forward instead of resetting, so CV Out stays CV Out and a CV In keeps its destination. 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.
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>
Melodies sat on one or three note lengths with no texture. min_duration_for_div had its grids inverted, forbidding short notes where they belong and forcing holds where they do not; rests were always exactly one slot, so every gap sounded alike; and velocity was a constant, flattening phrase starts, held notes and passing notes into one dynamic. Rests now span one, two or four slots and velocity follows position and length, both reusing the roll already drawn for the slot so the RNG budget is unchanged. Fixes the pool not being rebuilt when the tonic changes: build_pool bakes the tonic into absolute notes, so following a transpose without a rebuild made the cadence resolve toward a note the pool could not play. Also moves the follow resolution behind the shared follow_key helper. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
Same reasoning as the genre helpers next to it: Bassment, Contura, Chord Vamp, Venn and Arp de Lévy all resolve the device Key/Tonic, and landing it once ahead of the app PRs keeps the copies from diverging. 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 gives one 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. 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>
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>
Fourteen apps carry their own copy of the 24 PPQN division table and five repeat the free-running LFO speed curve, so the magic numbers and the bounds checks drift independently. Centralise them as CLOCK_DIVISIONS, division_at, lfo_step, lfo_step_modulated, quant_step and signal_brightness with unit tests. The tests uncovered a wrap in the brightness curve that every caller shared: a bipolar 4095 divides to 256 and cast to u8 as 0, dimming the button at the very peak of the wave. It clamps now. 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>
…/contura Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # faderpunk/src/apps/mod.rs
…ontura Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # faderpunk/src/tasks/clock.rs
Adds the ornament planner and wires Contura's clock path to it, so a note can resolve into a short grace, turn or repeat driven by the scale feel and the phrase position. The planner is shared with Grooves and Bassment, which land separately, so the genre entry points are marked dead for now. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
The app ran nearly blind: the button duck was 25 ms at Low and easy to miss, and the fader latches gave no reading of where a layer sat. Notes now dim the button to Off for 80 ms and drive a pitch meter on top and bottom from the pool index, so contour is visible. Each latch layer meters its fader on top/bottom and puts the value on the button as a brightness gradient: interval on Main, phrase in white on Alt, density in the scale colour on Third. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Branch updated: the shared ornament engine and the signal / latch-layer LED feedback are now on Depends on two open PRs, merged into this branch so it builds on its own:
Their commits show up in this diff until they land on
LED behaviour that came with this: notes dim the button to Off for 80 ms and drive a pitch meter on top/bottom from the pool index, and each latch layer meters its fader on top/bottom with the value as a brightness gradient on the button — interval on Main, phrase in white on Alt, density in the scale colour on Third. Gates on this branch: |
Review feedback on ATOVproject#639: a dropped NoteOff hangs the note with no later message to correct it, and APP_MIDI_CHANNEL is shared by all 16 channels, so a busy neighbour can cause the drop. The voice task hand- rolled a retry around the non-blocking sender to work around exactly that; awaiting the blocking sender does the same thing without the extra state. `note_on` is cleared right after the NoteOff so a dropped NoteOn retry cannot silence the old note twice. Drops the now-unused try_send_note_off from the branch copy of app.rs, matching ATOVproject#639. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
Phrase was the only main fader without a CV destination. It shapes the contour arch and the cadence window, so modulating it morphs the sentence rather than just the note density. The value is read before the ornament block so ornaments and the step path share one phrase length. Authored by an AI coding agent on behalf of kosmar. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Pushed Phrase was the only main fader without a CV destination. It shapes the contour arch and the cadence window, so modulating it morphs the length of the musical sentence rather than adding another density control. The value is read before the ornament block so ornaments and the step path always share one phrase length. Written and pushed by an AI coding agent on behalf of @kosmar. Verified to build; hardware test still pending. |
A modulator that never reaches its rails could only nudge the target a little, since CV Att could at most pass the input through unchanged. Allowing gain up to 4x lets a tame LFO drive the destination across the full range, with the existing clamp in attenuate_bipolar turning the excess into a hold at the end stops. Stored presets keep their behaviour: the scale is linear and 100 still means unity. Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Test plan
Made with Cursor
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_tickertry_send_*)This branch currently carries its own inline copy of the code in #638, #639.
Once those land it will be rebased to drop the duplicates, which shrinks this
diff to the app file plus its registry line.
Building any of this on a current Rust nightly also needs the toolchain fix in
#632.