Skip to content

(feat): add Contura melodic contour app - #634

Draft
kosmar wants to merge 34 commits into
ATOVproject:mainfrom
kosmar:feat/contura
Draft

(feat): add Contura melodic contour app#634
kosmar wants to merge 34 commits into
ATOVproject:mainfrom
kosmar:feat/contura

Conversation

@kosmar

@kosmar kosmar commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add Contura (WIP ID 42): clocked melodic contour over selectable 12-TET pitch-class sets
  • Flat named scale-set list (Western modes and other conventional labels on equal footing);
  • Anchors to device quantizer tonic (optional follow of device scale); pitch CV + MIDI

Test plan

  • On hardware: load Contura, run clock, confirm mixed short/medium/long notes
  • Cycle scale sets (long / Shift+short); verify mute and octave span
  • Confirm Follow device tonic / Follow device scale behave as expected

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:

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.

kosmar and others added 9 commits August 8, 2026 23:24
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>
kosmar and others added 20 commits August 10, 2026 14:04
…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>
@kosmar

kosmar commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Branch updated: the shared ornament engine and the signal / latch-layer LED feedback are now on feat/contura.

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 main; once they do, the diff reduces to Contura's own files.

ornament.rs is new here. It was landed on the playground alongside Grooves and Bassment, but only Contura's share was taken across — no Bassment or Grooves changes are in this branch, and the genre entry points carry #[allow(dead_code)] until those apps land. The WIP cheatsheet is deliberately not included; that belongs to #613.

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: cargo fmt --check, firmware clippy, libfp clippy, and cargo test --lib -p libfp (113 passed) all clean.

kosmar and others added 2 commits August 13, 2026 18:42
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>
@kosmar

kosmar commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Pushed feat(contura): add CV In Phrase as a jack destination.

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.

kosmar and others added 2 commits August 14, 2026 17:47
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>
@kosmar kosmar mentioned this pull request Aug 15, 2026
7 tasks
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