Add 10 utility abstractions: debounce, throttle, notepriority, cache, listdiff, ringbuf, dict.defaults, state.latch, notecluster, schmitt.edge~#27
Merged
user1303836 merged 25 commits intomainfrom Feb 25, 2026
Conversation
Implements a message debouncer that delays output until input is quiet for N ms. Supports leading/trailing edge modes, flush, cancel, bypass, and reset. Uses #0-scoped internals for instance isolation. Argument sets default debounce time (default 50ms).
Documents inlets, outlets, arguments, all control messages, mode combinations, default values, usage examples, and related objects.
Max trigger objects fire right-to-left. Several triggers had incorrect outlet ordering causing: - obj-29, obj-37: gate data arriving before gate control was set (t b l -> t l b, connections swapped) - obj-34: timer reset setting pending=1 before leading edge check could see pending=0 (t l l b -> t l b l, connections swapped) - obj-43: new delay starting before old one stopped (outlets 1<->2 swapped) - obj-58: pending cleared before checked, breaking flush (outlets 0<->2 swapped) - obj-61: status sent before message output on flush (outlets 0<->2 swapped) - obj-31: numinlets corrected to 2 for + object
The trigger object obj-34 was t l b l which caused the timer reset (setting pending=1) to fire before the leading edge check could read pending state. Changed to t b l l so R-to-L execution is: 1) outlet 2 (list): store msg in buffers 2) outlet 1 (list): check leading edge (while pending still has old value) 3) outlet 0 (bang): reset timer and set pending=1 (fires last) This fixes the bug where leading edge output never fired because pending was always 1 by the time the leading check ran.
When bypass is enabled, any pending trailing-edge timer is now canceled via s #0_cancel, preventing stale buffered messages from leaking through during bypass mode. Added t i i -> sel 1 chain on the bypass control path. Updated README to document this behavior.
Implements a MIDI note-priority abstraction that filters polyphonic input into monophonic output based on low/high/last priority modes. Supports switchover (re-triggers next priority note when active note is released), panic/reset, and bypass. Includes help patch and docs.
Key-value cache abstraction using dict objects for storage and cpuclock timestamps for expiry. Commands: set/get/has/invalidate/clear. Lazy expiry on get/has, eager expiry via sweep. All internal state #0-scoped for instance isolation. Includes help patch and README.
midi.notepriority: - Default mode corrected from "last" to "low" per spec (README) util.cache.ttl: - Fix 3 reversed trigger orderings in SetCmd subpatcher where cold inlets fired after hot inlets (obj-3, obj-10, obj-15 connections) - Default TTL corrected from 5000 to 1000 per spec (patch + README)
P1: midi.notepriority last mode ignores first note after init/reset. FindLast initialized bestseq to 0, so first note (seq=0) failed the strict > comparison. Changed bestseq init to -1. P2: midi.notepriority bypass leaves stale held-note state. Added t i i -> sel 1 -> s #0_panic chain on bypass control path so enabling bypass clears all internal state via panic. P2: util.cache.ttl leaks "invalidated" status on lazy expiry. Lazy expiry in get/has/sweep now uses s #0_lazy_invalidate routed to a silent p SilentInvalidate subpatcher (dict remove only, no status output). Explicit invalidate command still outputs status. Removed orphaned r #0_invalidate. P3: midi.notepriority README still claimed "last" as default in one example line. Fixed to match spec (low is default).
Implements a message throttle abstraction that passes at most one message per configurable interval. Three policies handle messages arriving during cooldown: drop (discard), latest (keep newest), and queue (FIFO buffer with configurable capacity). Includes help patch demonstrating all policies, flush, bypass, and dynamic interval adjustment.
…/common Implements a list differ abstraction using v8.codebox for correct duplicate-aware bag comparison. Outputs added, removed, and common items with support for sorted (order-independent) mode, bypass, and reset. Each occurrence is tracked independently.
Circular message history buffer using v8.codebox for ring buffer logic. Supports get (by recency index), dump (oldest to newest), clear, capacity, size, reset, and bypass. Configurable capacity via creation argument (default 16). Includes help patch and README.
When bypass was enabled, notes bypassed the state-tracking path (RecordNoteOn/ClearNoteOff). If bypass was then disabled while those bypass-started notes were still held, their subsequent note-offs entered the filtered path but were dropped because the held-note table had no record of them. Added an untracked note-off check in the note-off path: before entering ClearNoteOff + findwinner priority logic, read table #0_held for the incoming pitch. If held[pitch] == 0 (untracked), emit pitch 0 directly to the output, bypassing the priority logic. If held[pitch] > 0 (tracked), proceed with the normal priority evaluation path.
New abstraction that merges default key/value pairs into incoming dictionaries and validates required keys. Supports force mode (defaults overwrite vs gap-fill), required key management, and per-merge diagnostics (applied/preserved counts). Includes help patch and documentation.
Control-rate hysteresis latch that suppresses jitter/chatter in noisy control streams. State transitions require input to remain in threshold region for configurable dwell period before committing. Features: - Binary state (0/1) with high/low thresholds and neutral band - Configurable dwell timer for transition confirmation - Pending transitions cancel if input returns to neutral band - Bypass mode for simple thresholding without dwell - Duplicate state suppression - Creation args for low/high thresholds (defaults 0.4/0.6) - #0-scoped internals for instance isolation - Deterministic ordering via trigger objects throughout
Per-sample hysteresis implemented in a gen~ codebox: state=1 when input > high threshold, state=0 when input < low threshold, else hold previous state. edge~ converts state transitions to rising and falling bangs. Features: - 4 inlets: signal in, low threshold, high threshold, control messages - 4 outlets: state signal (0./1.), rising bang, falling bang, status - Creation args set initial thresholds (defaults 0.4 / 0.6) - Control messages: low, high, init, reset, bypass - Bypass mode substitutes simple >0.5 comparator via selector~ - Threshold clamping in gen~ ensures correct behavior if low > high - #0-scoped internals for instance isolation - Includes help patch and Cycling '74-style README
Remove spurious connection from t s s (obj-28) to ValidateRequired that caused validation to run before merge, producing an incorrect status message from stale dict data. Validation now only fires once, after MergeDefaults completes via t l l l (obj-29). Also corrects README output order to match actual trigger firing sequence.
…ection MIDI note clustering abstraction that accumulates note-on events within a configurable window (default 5ms) and emits cluster list with metadata. Features: - Cluster window via delay timer (restarts on each note-on) - Optional sort and unique filtering (both default on) - Metadata output: count, lowest, highest, single, done - Note-off passthrough on separate outlet - Bypass, flush, and reset control messages - #0-scoped internals for instance isolation Subpatchers: ClusterAccumulate (zl group), ClusterTimer (delay), ClusterShape (zl sort/unique), ClusterEmit (metadata computation). Includes help patch and README.
obj-82 used t i b i which fired the request value to != hot inlet before fetching current state into cold inlet, causing stale state comparisons. Changed to t i i b so bang fetches state first, then value triggers comparison -- matching the correct pattern in obj-106.
doStore() was emitting 'full' on every append once count === cap because count stays clamped at cap after the buffer first fills. Add a wasFull guard to only emit on the not-full to full transition.
Bug 1: ClusterEmit's zl len was only sending the length (outlet 0) to the trigger, while the pass-through list (outlet 1) was disconnected. Downstream list operators received an integer length instead of the actual cluster list. Fixed by connecting zl len outlet 1 directly to the gate data inlet and changing the trigger from t i l i to t i i. Bug 2: Bypass control path sent !- 1 output (0/1 integers) directly to outlet 3, polluting the note-off passthrough with stray control values. Disconnected the bypass inversion from the outlet since it is only needed for gate control.
…elector Bug 1: Reset now reads initial thresholds from v #0_init_low / v #0_init_high (populated at loadbang from patcherargs) instead of hardcoded 0.4/0.6 messages. Bug 2: Reset now sends selector control value 1 to s #0_sel, ensuring the selector~ switches back to the gen~ output (non-bypass) when bypass is cleared.
Bug 1: Reorder trigger outlets in enqueue path so pack's cold inlet (payload) is set before hot inlet (qidx) fires output. Changed obj-72 from 't l b b' to 't b l b' and rewired. Bug 2: Add capacity check gate before enqueue. Compare qidx >= cap and route to overflow status when queue is full, preventing unbounded growth and off-by-one. Bug 3: Swap flush trigger outlets so coll dump fires before clear, preventing data loss on flush.
…ring Fix three bugs found during review: 1. Bypass gate (root): gate 2 control received raw bypass (0/1) instead of bypass+1 (1/2). With bypass=0 the gate was closed, blocking all normal processing. Added +1 before gate control, removed dead !-1 path. 2. ClusterEmit list loss: zl len outlet 1 (list passthrough) arrived at the gate data inlet before the gate was opened by the count path, causing the list to be discarded. Inserted t l l splitter so the count path (outlet 1, fires first) opens the gate before the list (outlet 0, fires second) arrives. 3. Metadata ordering: done message fired first (outlet 4, R-to-L) instead of last. Swapped cluster and done trigger outlets so done fires last among metadata. Updated README to match corrected sequence.
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.
Summary
Implements all 10 native Max abstractions from the utility objects roadmap:
Message/Data Utilities
MIDI Utilities
Signal Utilities
Each utility includes abstraction
.maxpat, help/demo.maxpat, and Cycling '74-styleREADME.md.Design
#0-scoped names for instance isolationtriggerobjects throughoutbypasson all utilities clears pending stateReview Fixes Applied
Round 1
Round 2
Test plan