Utilities, value delay, time chooser tapers and tempo-syncable examples - #214
Merged
Merged
Conversation
setup() for an optional value delegates to the concrete type's overload; the typed ones (int, float, string...) set is_event, the ossia::value one does not. So a halp::val_port<..., std::optional<ossia::value>> bound to an address read the address's value at every tick instead of receiving its messages: Flip Flop toggled on every tick, and Repetition Filter, Repeat and Spigot saw the value again at each tick. Set is_event from the optional port itself after the concrete setup, still honouring a Field::event override. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The script always went to ::system, i.e. /bin/sh. An "Interpreter" combobox now picks the system shell (the default, as before), bash, zsh, fish, sh, python3, PowerShell or cmd, each given the script with its -c / -Command / /C flag; "Custom" runs the "Custom command" line, where %s stands for the script (appended when absent), e.g. "/opt/foo/bash -c %s". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The output was a plain value, so the last message that passed was sent again at every tick, disabled or not. Make it optional and clear it each tick; also a lowercase c_name like the other utilities. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
- The Output bang sent the raw count: it is now clipped / wrapped / folded by the Mode like the counting itself. - A Reset bang starts the count over. - "Send" chooses when the count goes out: every tick (the behaviour so far and the default), on each message, or only on the Output bang. The Count outlet is optional, so nothing is sent on ticks where nothing asked. The new ports come after the existing ones: saved documents keep theirs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
"Send" chooses between every tick (the behaviour so far and the default), on each input, or only on the new Output bang; the outlets are optional so nothing is sent otherwise. Reset now also forgets the consecutive difference, and is sent once as zeros in the "on input" mode. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The delay was clocked by the ticks: one step per tick of a nominal 500 Hz line with fixed tap times, so how long it delayed depended on the tick rate, and Length did nothing. A Mode combobox keeps that as the default (Ticks) and adds: - Messages: tap i gives the value received (i+1) * Length changes of In ago; - Time: tap i gives the value In had (i+1) * Length milliseconds ago, measured with the tick's frames and the sample rate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
- widget_type::file and halp::file_path<"Name", "filters">: a path picked with a file dialog, given to the object as a string (file_port gives the contents instead). - halp::save_file_path<"Name">: a typed path (a dialog could only pick existing files) whose placeholders the host may expand (%t, %n). - Read File and Read File Line take a file_path; Write File a save_file_path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
Only lists were handled: a vector value fell through and gave an empty output. They are taken as the list of their components. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
Ghost notes: every note gets its own timing and length deviation, so notes of the same pitch could overlap or swap after scheduling -- two note-ons in a row, a note-off cutting the next note, stray note-offs. A new note of a pitch also replaced the previous one in the tracking table, so the first note's note-off ended the second. The output now pairs notes: each note-on and its note-off carry the note's id; per channel and pitch the output knows which note sounds, ends it before another note-on of that pitch, and drops a note-off that is not the sounding note's. Note-offs match the oldest instance of their pitch. Flushing ends exactly the notes sounding at the output. Pitch (appended after the existing controls): standard deviation, in semitones, of a random transposition of each note; its note-off follows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The preview stretched the texture to the square item, and the sample markers with it. The image is drawn as large as fits, centered (letterboxed), and the markers are placed in that rectangle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
New object. Trigger: a bang outputs the next value of the list (an internal counter bounded by Clip, Wrap or Fold), a number the value at that index. List: a list, or a vec2f/3f/4f. Mode: Manual (triggers only), Every tick, or Timed (the next value at each Interval, counted in frames). Value out, and Index out when the index changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
A maintained button (bool control, button/pushbutton widget) is true while held. An impulse arriving on it -- a cable, an OSC message -- went through ossia::convert<bool>, which makes an impulse false, so it did nothing: a Clear or a Hold could not be triggered by a message. Such an impulse now sets the button for the tick (with its update()), and finish_run releases it (update() again). A button already held stays held; true and false keep their meaning. Other bool controls and ossia::convert are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
- Enumerator and Value delay took operator()(halp::tick) without declaring
`using tick = halp::tick;`: the bindings never called it, and the objects
did nothing (Value delay since its Messages / Time modes). A static_assert
now refuses an operator() that cannot be called.
- Enumerator keeps the last list it received: the List port was a plain
ossia::value, which the ossia binding resets after each tick.
- Value delay: a Time control for the Time mode's tap spacing, instead of
reading Length as milliseconds.
- Counter: the count goes out from operator(). The binding clears optional
outputs after the controls' update() and before operator(), so the Output
bang wrote a value that was then dropped.
- ossia binding: a plain number on a time control (a cable, OSC) is a time in
seconds; it converted to {v, v} and was read as a musical ratio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
- avnd::init_controls called every control's update(), and an impulse button's update() is its press: every object's Output, Bang, Reset... was pressed once when it was created, e.g. a Counter, Accumulator or Buffer queue in Manually mode sent once at the start of the execution. A port whose optional value holds nothing is not updated; which ports can be empty is decided at compile time (avnd::optional_value_field). - ossia binding: a bool control takes numbers (non-zero is true), as ossia::convert<bool> does; a toggle driven by a cable or an OSC controller ignored 0 / 1. - Array best sends when an array arrives or Mode / Softmax scale change, instead of the same results at every tick. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
In takes any value (was a float slider); taps and the new Mix outlet carry it. One delay line for every mode, at positions counted in ticks, messages or milliseconds; Ticks mode taps are Length ticks apart like the others (was a Gamma multitap at fixed 0.1 + 0.11 i s with a hidden 0.1 feedback). - Feedback: each echo is mixed back into the line, (1 - fb) In + fb echo, so a movement repeats and fades while a steady value stays. - Mix outlet: (1 - mix) In + mix first echo. - Freeze: the line repeats its last Length / Time and ignores In. - Clear: empties the line. - Smooth (Time mode): glides between recorded values. Numbers, vec2/3/4 and lists of numbers mix; other values are delayed as they are. Time already follows the tempo: the binding converts a musical value to seconds at the current tempo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
A 6 px dot in the runtime-value colour, lit when a message arrives and fading out over 300 ms of wall-clock time (was a light 10 px square fading per repaint, so at the repaint rate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The cube of the knob's travel: short times (envelope attacks, a few milliseconds) get most of it, the whole range stays reachable, 0 included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
…imes ADSR's attack, decay and release, the flanger's sweep (a period instead of a frequency), the compressor's and limiter's attack and release are time choosers: seconds, or a note value -- a release synced to the beat is what makes a compressor pump in time. Stage times reach 0. The "Relase" typo is gone from the new ones. The old objects stay, as "(old)" and deprecated (hidden from score's library), so that documents using them are unchanged. The old and new versions share their implementation: the ADSR envelopes, and the dynamics setup. For both, a ratio of 0 no longer divides by zero, the lookahead is clamped to the delay line (at 0 it read the oldest sample instead of the newest, beyond 100 ms the read position wrapped around), and the delay lines are only reallocated for a new sample rate, not every time prepare() re-runs from the audio thread for a larger buffer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The binding hands a synced time chooser over in seconds at the current tempo, but the particles used 1 / seconds as the quantification rate, i.e. took the seconds for a fraction of a whole note: at 120 BPM a synced quarter (0.5 s) triggered on half notes. The grid's rate is 240 / (seconds * tempo). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
A new playhead was pushed into a vector that grew on the audio thread. The number of sounds playing at once is now bounded and reserved in prepare(). frame_in_interval takes 64-bit positions: the transport's frame count overflows an int after about 12 hours at 48 kHz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
The limiters ran the compressor with an infinite ratio followed by a static waveshaper. The one-pole gain envelope does not reach its target before the peak, so the waveshaper did the limiting, and it bent the signal below the threshold: at the default threshold of Limiter v1 its linear region was empty and everything was squashed to about +-0.02 around a DC offset; Limiter v2's knee attenuated signal under the threshold, and its makeup saturated instead of adding level. Both now share PeakLimiter: input gain (Makeup) first, a peak detector linked over all channels and the sidechain (which can only add gain reduction), a hard-knee gain computer min(1, ceiling / peak) with the threshold as the ceiling, a running minimum over the attack, a one-pole release keeping its t60 meaning, and a moving average over the attack, with the audio delayed by the lookahead. Below the threshold the signal comes out unchanged, only delayed; above it, the output never exceeds the threshold. Attack defaults to the default lookahead, so that the gain ramp spans the whole lookahead. The compressors are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
…ndaries tick_musical converted musical positions to frames in three places, each with a plain floor of the ratio. The positions come out of floating-point arithmetic exact only to a few ulps of their magnitude, so a point meant to fall on a sample boundary could land one sample early, and one in the last ulps of a tick was dropped by both that tick and the next. musical_to_frame treats a point within that tolerance below a boundary as on it, and "past the end of the tick" is decided on the musical position rather than on the rounded frame. It is kept identical to ossia::musical_to_frame so that the engine and the objects agree on where a grid point falls. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
Layouts are walked as aggregates, and Boost.PFR used without P1061 rejects any base class: a layout inheriting from enabled_when, as the example suggested, does not build there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
jcelerier
force-pushed
the
score/utilities-and-time-choosers
branch
from
September 29, 2026 02:51
0c88975 to
529e8a3
Compare
jcelerier
added a commit
to ossia/score
that referenced
this pull request
Sep 30, 2026
celtera/avendish#214 was rebase-merged; same tree as 529e8a3fbc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changes to the avendish utilities, examples and bindings that ossia score's integration branch (ossia/score#2312) needs.
Bindings and halp
ossia::valueport is an event port. It read the bound address's value at every tick instead of receiving its messages (Flip Flop toggled on every tick; Repetition Filter, Repeat and Spigot saw the value again each tick).static_assertrefuses anoperator()(halp::tick)that the bindings cannot call (Enumerator and Value delay silently did nothing).file_pathandsave_file_path; the file objects use them.time_chooser_mapper, the taper of time choosers (cube of the knob travel: short times get most of it, 0 stays reachable).musical_to_framemaps musical positions to frames with one function, exact at sample boundaries, identical toossia::musical_to_frameso the engine and the objects agree.enabled_when/visible_whenreturned from astatic constexpr auto condition()rather than inherited. Boost.PFR (used without P1061: clang < 21, gcc < 16, MSVC) rejects layouts with a base class.Utilities
%sstands for the script.Audio examples
Testing
The objects are tested through ossia score's suite in ossia/score#2312 (utilities, value delay, counter / accumulator send modes, limiter ceilings and transparency below the threshold for v1 and v2, envelopes, audio particles grid rate). The local non-GPU suite passes (450/450). Windows/MSVC, FreeBSD (the Boost.PFR path) and the Qt 6.4 / 6.8 builds are only confirmed by score's CI.
🤖 Generated with Claude Code
https://claude.ai/code/session_014tPT7bc2XWmFyQUtv9B4MX