Skip to content

Golden differential testing for Pd/Max: broaden coverage and fix bindings#169

Merged
jcelerier merged 6 commits into
mainfrom
feat/pd-max-golden-coverage
Jul 12, 2026
Merged

Golden differential testing for Pd/Max: broaden coverage and fix bindings#169
jcelerier merged 6 commits into
mainfrom
feat/pd-max-golden-coverage

Conversation

@jcelerier

Copy link
Copy Markdown
Member

Extends the golden output-differential harnesses to verify that Pd and Max externals produce the same output as the C++ object given the same inputs — across controls, messages, callbacks, impulses, strings, containers and textures, not just audio. Running them surfaced and fixed several real binding bugs.

Binding bugs fixed (found by output differential, invisible to load tests)

  • Analyzer dispatch — objects with audio in and control out (e.g. Peak) were routed to the message processor and never ran their DSP. Both backends now dispatch on avnd::audio_processor.
  • Output-control commit — the Pd and Max audio processors never emitted non-audio outputs. They now create a message outlet per non-audio output and commit on bang.
  • Aggregate input distribution — the from_atoms handler assigned the first atom to every field, so xy/xyz/rgba/xyzw/range controls silently received only their first value in both backends.
  • Tick-only objects — objects taking halp::tick could not run in the message processors and prepare() got no sample rate.
  • variable_audio_bus crashrequest_channels was an empty std::function, crashing on the first process pass.
  • Help-patch object name — generated patches used the class c_name() instead of the registered CMake C_NAME, so a renamed external's help patch failed to instantiate.

Coverage

  • Golden generator records impulse/message/callback/string/container/tick cases in addition to audio and scalar controls.
  • Pd harness drives control/message/callback objects and analyzers against a real headless Pd; container controls replay as list messages.
  • Max driver replays strings, impulses, messages and list-valued controls; its report loader tolerates Max's 32 KB File-write line splitting.
  • Dump metadata classifies container value types and carries channel/init-arg info; help patches emit usable demo messages for them.

Results (real Pd/Max instances vs the C++ oracle)

  • Pd in-repo examples: 70/112 match (was 22); remaining mismatches are documented model/oracle limits (uninitialized golden outputs, stateful generators, multi-bus vs Pd-mono, offline-only rate/creation-args).
  • Max in-repo examples: 69/112 match, no crashes.
  • Generated Pd help patches: 98/98 load cleanly in real Pd.
  • score-addon-puara (against upstream HEAD): 23/26 objects output-match; the remaining 3 are xtensor-array outputs the harness cannot capture. This also caught undefined behavior in the vendored IMU_Sensor_Fusion library (uninitialized fusion state read on the first update), fixed in that repo.

🤖 Generated with Claude Code

jcelerier and others added 5 commits July 11, 2026 12:31
Record and sweep the cases a control/message backend actually exercises:
false/min/max/enum-mode control sweeps, string overrides, impulse
engagements, declared message-port invocations, and callback firings.
Container controls (vectors, arrays, scalar aggregates) are set and
recorded as JSON arrays. Tick-only objects (audio-free, `using tick`)
run one synthesized processing pass instead of the audio path, which
would static_assert. The recorded c_name is the CMake C_NAME (what the
external registers under), so an object may keep a differing
halp_meta(c_name).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SvvcvSnStMbRqdC2EYbcg
Refine the "unknown" value_type into list/array/aggregate (with a
component count) so patch generators and harnesses know a control takes
a value list. Audio ports carry their statically-known channel count,
and objects with an initialize(...) get its argument signature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SvvcvSnStMbRqdC2EYbcg
Several defects surfaced by output differential testing:

- Audio-in/control-out analyzers (Peak, etc.) were routed to the message
  processor and never ran their DSP; dispatch on avnd::audio_processor.
- The audio processor never emitted non-audio outputs. Create a message
  outlet per non-audio output and commit their values on bang; wire
  output callbacks.
- from_atoms distributed only the first atom to every field of a scalar
  aggregate (xy/rgb/rgba/...), so all components collapsed to the first
  value. Advance the field index.
- Tick-driven objects could not run in the message processor (no tick
  overload) and prepare() got no sample rate; synthesize a one-frame
  tick and pass the engine rate.
- Impulse ports could not be engaged: from_atoms rejected an empty
  optional value type. Engage on any message, reset one-shot after use.
- Unnamed ports all collided on the "unnamed" selector; address them
  positionally as p<index>.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SvvcvSnStMbRqdC2EYbcg
The Max counterparts of the pd binding fixes:

- Dispatch audio-in/control-out analyzers to the DSP processor.
- Give the audio processor a message outlet per non-audio output, to the
  right of the signal outlet, and commit their values on bang.
- from_atoms now advances the field index so aggregate controls receive
  every component instead of only the first.
- Synthesize a one-frame tick and pass the engine rate so tick-driven
  message objects run.
- Wire a no-op request_channels for audio busses so a variable_audio_bus
  no longer calls an empty std::function and crashes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SvvcvSnStMbRqdC2EYbcg
The object box now uses the CMake C_NAME the external registers under,
not the class c_name, so a renamed external's help patch instantiates.
Emit a valid symbolatom record for string controls, correct the hradio
cell count, drive impulse ports with a bang, feed multi-component and
container controls a value list, size signal connections to the audio
channel count, and pass initialize(...) default arguments. Non-parameter
inputs, messages and outlets get a reference section.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SvvcvSnStMbRqdC2EYbcg
@jcelerier jcelerier force-pushed the feat/pd-max-golden-coverage branch from 5ac2d36 to 50114bc Compare July 11, 2026 16:50
…rnesses

Extend the golden differential harnesses to the cases the golden now
records. The pd harness drives control/message/callback objects and
analyzers (bang-commit after one DSP block), replays container controls
as list messages, and captures outlet output from a headless pd. The Max
driver replays strings, impulses, messages and list-valued controls, and
its report loader rejoins lines split at Max's 32 KB File-write boundary;
a --recompute mode re-diffs an existing report. Shared control/callback
comparison handles sequences; the python/gst/td harnesses skip case
kinds they cannot drive.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SvvcvSnStMbRqdC2EYbcg
@jcelerier jcelerier force-pushed the feat/pd-max-golden-coverage branch from 50114bc to 04c6f10 Compare July 11, 2026 21:32
@jcelerier jcelerier merged commit 00827f5 into main Jul 12, 2026
23 checks passed
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