Route mc.zerr.combinator~ through the core EnvelopeCombinator - #19
Open
zeyuyang42 wants to merge 1 commit into
Open
zeyuyang42 wants to merge 1 commit into
zeyuyang42 wants to merge 1 commit into
Conversation
The mode argument was parsed and thrown away. `zerr_combinator_new` read the symbol into a local, left a `// TODO: Initialize the ZerrCombinator based on the mode`, and never constructed a ZerrCombinator at all -- `x->zc` stayed NULL for the object's whole life. `perform64` hardcoded a max-combination loop, so `mc.zerr.combinator~ 2 root` and `2 max` computed exactly the same thing while the help patch shipped both side by side as if they differed. `bang` reported the literal string "everything" as the mode. Make the object real. ZerrCombinator now owns a zerr::EnvelopeCombinator and is built in two stages, because Max hands over the object's shape in stages: the number of envelope sets is known at creation, but the channel count, sample rate and block size only arrive in dsp64. So the constructor validates, and prepare() -- called from dsp64, where allocating is safe -- builds the core module. It is idempotent, so toggling DSP does not reallocate, and it rebuilds when the channel count or block size changes. perform() is now noexcept: it runs inside Max's C call stack, where the previous `throw std::invalid_argument` would have terminated the host. Anything unexpected zeroes the output instead. Copies are clamped to the prepared block size. Also on the argument path: the mode is now optional and defaults to "max", which is what the object has always computed, so no saved patch changes behaviour. Positional arguments stop at the first @Attribute. The mode value is validated rather than just its atom type -- `2 banana` used to instantiate happily. A bad mode reports the error and falls back rather than returning NULL, since a typo should not leave a dead object box with no inlets. Two things fixed in passing: - dsp64 returned without dsp_add64 when the inlets disagreed on channel count, which made the muting branch in perform64 unreachable and left the outlet holding whatever the previous DSP chain wrote. The perform routine is now always added, so muting actually silences. - `chans` was a writable attribute shadowing a value derived from the signal; setting it only desynced the object. It is read-only now. The sample rate and block size come from the dsp64 arguments rather than sys_getsr()/sys_getblksize(), which differ inside poly~. Help patch: the description was copied verbatim from the Pd three-argument object and described arguments Max does not take. Rewritten for the real two-argument form, "add" documented, and "root" corrected -- it is the Nth root of N sets multiplied, not a square root, which only coincides for two sets. Runtime mode switching needs a setter on the core module and is left for a follow-up; mode is still fixed at construction on both platforms. Note for anyone with existing patches: `2 root` used to compute max and now computes the geometric mean, so it will be quieter. Refs Zerr* board ID 136. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zeyuyang42
commented
Aug 26, 2026
| * @throws std::invalid_argument if inputCount < 1 or mode is not a known mode | ||
| */ | ||
| ZerrCombinator(const zerr::SystemConfigs& sys_config, int inputCount, std::string mode) | ||
| : systemConfigs { sys_config } |
Collaborator
Author
There was a problem hiding this comment.
where does this external get the system config if you have delete it? just explain, don't rush to change anything
zeyuyang42
commented
Aug 26, 2026
| if (!combinator->initialize()) { | ||
| return false; | ||
| }; | ||
| return mode == "add" || mode == "root" || mode == "max"; |
Collaborator
Author
There was a problem hiding this comment.
I think this wraper should be thiner, so for now if I added a new mode in core I still need to update here. also explain first.
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.
The problem
mc.zerr.combinator~was faking mode selection.zerr_combinator_newparsed the mode symbol into a local, left a// TODO: Initialize the ZerrCombinator based on the mode, and dropped it. Only the atom type was checked, somc.zerr.combinator~ 2 bananainstantiated happily.x->zcwas set toNULLand never assigned —zerr::EnvelopeCombinatorwas never constructed anywhere in the Max tree.zerr_combinator.hpphad itsunique_ptrinitializer commented out with a stale signature.perform64hardcoded a max-combination loop, so2 rootand2 maxproduced identical output — while the help patch shipped both side by side as if they differed.bangposted the literal string"everything"as the mode.Pd was already doing this correctly (
puredata/src/zerr_combinator~.cpp→zerr_combinator.cpp→ core), so this brings Max in line.What changed
ZerrCombinatornow owns a realzerr::EnvelopeCombinator, built in two stages because Max hands over the object's shape in stages — the number of envelope sets is known at creation, but the channel count, sample rate and block size only arrive indsp64. The constructor validates;prepare()(called fromdsp64, where allocating is safe) builds the core module, is idempotent so toggling DSP does not reallocate, and rebuilds when the shape changes.perform()isnoexceptnow — it runs inside Max's C call stack, where the previousthrow std::invalid_argumentwould have terminated the host. Copies are clamped to the prepared block size.Arguments: mode is optional and defaults to
max(what the object has always computed, so no saved patch changes). Positional args stop at the first@attribute. The mode value is validated; a bad one reports the error and falls back rather than returningNULL, since a typo shouldn't leave a dead object box with no inlets.Fixed in passing:
dsp64used toreturnwithoutdsp_add64when the inlets disagreed on channel count, making the muting branch inperform64unreachable and leaving the outlet holding whatever the previous DSP chain wrote — so muting never actually silenced. Andchanswas a writable attribute shadowing a signal-derived value; it is read-only now. Sample rate and block size come from thedsp64arguments rather thansys_getsr()/sys_getblksize(), which differ insidepoly~.Help patch: the description was copied verbatim from the Pd three-argument object and described arguments Max does not take. Rewritten for the real two-argument form,
adddocumented, androotcorrected — it is the Nth root of N sets multiplied, not a square root (which only coincides for two sets).mc.zerr.combinator~ 2 rootused to compute max and now computes the geometric mean, so existing patches usingrootwill get quieter output. That is the fix, but it changes mixes.Verification
Driven through the real
ZerrCombinatorAPI the same way the external does, built against this branch'slibzerr_core.aunder ASan + UBSan (clean):Two sets of constants
0.25and0.81, three envelopes per set:maxaddrootBefore this PR all three read 0.81.
Lifecycle, all passing:
perform()beforeprepare()is silent;prepare()is idempotent on repeat; re-prepare across 3→8 channels and 64→128 block size keeps correct output;sampleframessmaller than the prepared block size is handled;prepare(0 channels)is rejected; too-few input pointers silences instead of reading out of bounds. A bogus mode is rejected at construction, not at DSP start.Not yet exercised in Max itself — the listening pass is still to do.
Not in this PR
Runtime mode switching (the "add message" half of the task) needs a setter on the core
EnvelopeCombinator, whose mode is currently bound once ininitialize(). That is a separate change, along with Pd parity for the message. Mode remains construction-time on both platforms.Routing through the core also reintroduces a per-block
Blockscopy, sinceEnvelopeCombinator::performtakes and returns by value — a core signature change, so it belongs with the same follow-up (and overlaps board ID 140).Refs Zerr* board ID 136.
🤖 Generated with Claude Code