Skip to content

Add Python control-side bindings (pyarco) - #5

Open
oltyan wants to merge 4 commits into
rbdannenberg:mainfrom
Musical-Mycology:pyarco-bindings
Open

Add Python control-side bindings (pyarco)#5
oltyan wants to merge 4 commits into
rbdannenberg:mainfrom
Musical-Mycology:pyarco-bindings

Conversation

@oltyan

@oltyan oltyan commented Apr 14, 2026

Copy link
Copy Markdown

Summary

  • Adds pyarco/ — pure-Python control bindings for Arco using o2litepy, parallel to the Serpent bindings in serpent/srp/
  • ArcoEngine manages connection lifecycle and a UgenID pool; live ugens are tracked in a weak registry so Python refcounting drives ugen lifetime
  • ~55 ugen wrapper classes mirroring the Serpent API, plus the Instrument/Synth/Note/Score framework
  • NiceGUI interactive demo app at apps/test/python/init.py
  • Offline pytest suite (pyarco/tests/, 64 tests) that runs in the Arco tree with no server and no o2litepy dependency
  • See pyarco/README.md for architecture, the ugen lifecycle model, usage, and testing.

Ugen lifecycle model

  • ArcoEngine._ugens is a weakref.WeakValueDictionary. Dropping the last Python reference to a pool-allocated ugen fires __del__, sends /arco/free, and returns the id to the pool — no session-long ID leak.
  • Client-side references mirror the server graph so nothing is GC'd while still wired: each ugen's inputs dict, container members (Sum/Sumb/Add/Addb/Route/Stdistr), Mix.inputs, play()/fade() output pinning, and borrow/set_alternate argument pinning.
  • owns_id: pool-allocated ids are owned (freed by __del__); ids passed explicitly (system ugens, Instrument wrappers borrowing their output's id) are borrowed and never freed by the wrapper.
  • Action targets (atend/register_action) are held weakly, with dead-target pruning and per-callback exception isolation.

Module layout

File Role
pyarco/README.md Architecture, lifecycle model, usage, ugen catalog, testing
pyarco/arco_engine.py ArcoEngine, weak-ref UgenID registry/pool, action system, constants, utilities
pyarco/arco_ugens.py Ugen base class and all concrete wrappers
pyarco/arco_instr.py Instrument framework (Param, Synth, Note/Score, Reverb, Supersaw)
pyarco/arco.py Re-export layer — from arco import Sine works
pyarco/tests/ Offline pytest suite (FakeO2Lite transport; no server needed)
pyarco/requirements-dev.txt Test dependency pin (pytest)
apps/test/python/init.py NiceGUI demo for interactive testing

Test plan

  • python -m pytest pyarco/tests -v → 64 passed (offline; no Arco server or o2litepy required), including a full Supersaw_synth noteon/noteoff cycle
  • Start Arco server (source apps/common/setpath.sh && cd apps/test && ./daserpent.app/Contents/MacOS/daserpent)
  • Run demo app (cd apps/test/python && python init.py)
  • Verify Connect, play/mute Sine, fade in/out
  • Verify ugen cleanup: drop references / close engine and confirm no leaked IDs (no Slot is already free / No free slots)

Known follow-ups

  • /actl/act is not yet registered as an o2lite handler and nothing calls atend(), so server-initiated note recycling (Synth.is_finished) doesn't fire against a live server yet (the method itself is correct and tested).
  • term(dur) outside the fade helpers still desyncs the client pool from the server.

🤖 Generated with Claude Code

Chris Oltyan and others added 4 commits April 14, 2026 12:41
Pure-Python control bindings for Arco using o2litepy. Replaces direct
O2 message construction with a Ugen class hierarchy that mirrors the
Serpent bindings.

- arco_engine.py: ArcoEngine lifecycle (connect/close/context manager),
  UgenID pool, action system, constants, utilities
- arco_ugens.py: Ugen base class and ~50 concrete wrappers
- arco_instr.py: Instrument framework (Param, Synth, Note/Score, etc.)
- arco.py: re-export layer for backward compatibility
- apps/test/python/init.py: NiceGUI interactive demo app

ArcoEngine holds strong references to all ugen shadows, preventing
accidental GC from sending premature /arco/free messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Brings the vendored pyarco/ copy up to date with the standalone
Musical-Mycology/pyarco repo's memory-lifecycle overhaul:

- weak-reference ugen registry (WeakValueDictionary) so dropping the last
  Python reference frees the server ugen and reclaims its pool slot
- owns_id ownership rule (Instruments borrow their output ugen's id;
  no more per-Instrument slot leak / double-free)
- client-side graph mirroring (container members, play/mute/fade pinning,
  borrow/set_alternate pinning) so nothing is GC'd while wired server-side
- weak action targets with prune-on-delivery and per-callback isolation
- Smoothb construction + newn payload fix; single-sourced ACTION_* consts
- fade() race/double-fade guards; engine-scoped sawtooth singleton;
  per-thread instrument-construction stacks; Synth.is_finished releases
  the recycled note's Mix input
- new offline pytest suite (pyarco/tests/, FakeO2Lite, 36 tests) — runs
  in the Arco tree with no server and no o2litepy dependency

Vendored-location sys.path in arco_engine.py preserved
(../apps/test/python); arco.py and init.py unchanged.
Brings the vendored copy up to Musical-Mycology/pyarco main:

- arco_ugens.py: block-rate ugen constructors (Sineb, Resonb, Mathb,
  Unaryb, Tableoscb) now accept C_RATE Const inputs, rejecting only
  a-rate. Per Arco's own type system a `b` spec expands to `bc`. This
  unblocks Supersaw_instr construction, which was previously impossible.
- tests: add test_rate_guards.py and test_supersaw.py (full
  Supersaw_synth noteon/noteoff cycle). Vendored suite is now 64 tests.
- add pyarco/requirements-dev.txt (pins pytest for the offline suite).

arco_engine.py / arco_instr.py / arco.py / init.py unchanged.
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