Patcher: modular synthesis in code
==================================

Goal: build a small synthesis graph programmatically, drive its
parameters at runtime, then save and reload the graph as JSON.

This tutorial walks through ``example/demo13_patcher.dart`` line by
line. By the end you will have a sine oscillator with a named control
that re-tunes it from Dart code, plus a JSON round trip that can
rebuild the same graph from a string.

Source:
`example/demo13_patcher.dart
<https://github.com/yvanvds/dart-yse/blob/main/example/demo13_patcher.dart>`_.

If you have read the upstream libYSE ``05_patcher`` tutorial that
covers ``Demo13_Patcher.cpp`` and ``Demo14_LoadPatcher.cpp``, the
underlying graph is the same — only the call site moves from C++ to
Dart.

Mental model
------------

A :class:`Patcher` is a graph of small objects. Each object has zero
or more **inlets** (left side, accepting data) and zero or more
**outlets** (right side, emitting data). You wire them together with
:py:meth:`Patcher.connect`; data flows from outlets into inlets.

Object type strings follow a two-character convention that mirrors
Max/MSP:

- ``~`` prefix — audio-rate (DSP) object. ``~sine`` evaluates one
  sample per audio frame.
- ``.`` prefix — control-rate (event) object. ``.r`` (receive) fires
  only when you push data into it with
  :py:meth:`Patcher.passFloat` / :py:meth:`Patcher.passInt` /
  :py:meth:`Patcher.passString` / :py:meth:`Patcher.passBang`.

The full list of supported types is exposed as constants on the
:class:`Obj` class — pass any of them to
:py:meth:`Patcher.createObject` in place of raw string literals.
Every object's accepted inlets, outlets, parameters, and message
types are listed on the :doc:`/api/patcher_objects/index` reference page,
generated directly from the engine source so it can never drift from
what ``createObject`` actually accepts.

Creating a patcher and attaching it to a sound
----------------------------------------------

A patcher is just an object; you initialise it with the number of
audio outputs it should expose, then hand it to
:py:meth:`Sound.fromPatcher` so the engine drives the graph as a
sound source:

.. code-block:: dart

   final p = Patcher(mainOutputs: 1);                 // 1 = mono; 2 = stereo
   // ... build the graph here ...
   final sound = Sound.fromPatcher(p, volume: 0.3);
   sound.play();

The first argument to :py:meth:`Sound.fromPatcher` is the
:class:`Patcher`; the optional ``channel`` argument routes through a
specific :class:`Channel` (default: master mix). The Sound holds a
borrowed reference to the patcher — keep the ``Patcher`` instance
alive as long as the ``Sound`` is alive.

Building a graph
----------------

:py:meth:`Patcher.createObject` returns a :class:`PHandle` — an owned
pointer into the patcher's object list. There are two ways to
identify a type: the literal string (``'~sine'``) or the corresponding
constant on :class:`Obj` (:py:attr:`Obj.dSine`). Both are
interchangeable — :py:attr:`Obj.dSine` literally is the string
``'~sine'`` at runtime. Use whichever style reads better at the call
site.

.. code-block:: dart

   final sine = p.createObject(Obj.dSine, args: '220');
   final dac  = p.createObject(Obj.dDac);
   final recv = p.createObject(Obj.gReceive, args: 'freq');

The optional ``args`` parameter to :py:meth:`Patcher.createObject` is
the object's creation argument string, parsed by the object exactly
as it would in a saved patch. ``~sine`` with ``'220'`` starts
oscillating at 220 Hz; ``.r`` with ``'freq'`` registers the receive
under the name ``"freq"``; an empty ``args`` leaves the object at its
default configuration.

Connecting objects is the next step. :py:meth:`Patcher.connect` wires
one outlet to one inlet — both are zero-indexed:

.. code-block:: dart

   p.connect(sine, outlet: 0, to: dac, inlet: 0);    // audio out
   p.connect(recv, outlet: 0, to: sine, inlet: 0);   // freq control

The signal flow in this graph is: ``~sine`` (initially 220 Hz, but
overridable through the ``"freq"`` receive) → ``~dac`` (hands the
buffer back to the engine).

Driving parameters at runtime
-----------------------------

The ``.r`` (receive) objects are the patcher's external interface.
Each one has a name; calling
:py:meth:`Patcher.passFloat` /
:py:meth:`Patcher.passInt` /
:py:meth:`Patcher.passString` /
:py:meth:`Patcher.passBang` on the patcher delivers the value to
every ``.r`` registered under that name.

.. code-block:: dart

   final ok = p.passFloat(440, 'freq');     // change pitch to A4
   if (!ok) print('No receiver named "freq" — graph not wired correctly');

Return value of every ``pass*`` method is ``false`` when no receiver
was registered under that name. The four methods cover the four data
types the engine knows about:

- :py:meth:`Patcher.passFloat` for continuous controls (frequency,
  gain).
- :py:meth:`Patcher.passInt` for discrete steps (MIDI note numbers,
  selector indices).
- :py:meth:`Patcher.passString` for symbolic values (a preset name,
  an arbitrary tag the graph dispatches on).
- :py:meth:`Patcher.passBang` for bare triggers with no payload (fire
  an envelope, advance a counter).

Driving an object directly via PHandle
--------------------------------------

The :class:`PHandle` returned by :py:meth:`Patcher.createObject` is
not just a token — it exposes the object's full state to Dart. Use
this to:

- **Read structural information.** :py:attr:`PHandle.type`,
  :py:attr:`PHandle.inputs`, :py:attr:`PHandle.outputs`, and
  :py:meth:`PHandle.isDspInput` describe the object's I/O surface.
  :py:meth:`PHandle.outputDataType` returns an :class:`OutType` enum
  value (``bang``, ``float``, ``integer``, ``buffer``, ``list``,
  ``any``) describing what a given outlet emits — useful when you
  build an external patcher editor on top of the engine.
- **Walk connections.** :py:meth:`PHandle.connectionCount`,
  :py:meth:`PHandle.connectionTargetId`, and
  :py:meth:`PHandle.connectionTargetInlet` return the wiring graph
  outward from each outlet so you can serialise it yourself or check
  what is connected to what. Both return ``null`` when there is no such
  connection, and :py:attr:`PHandle.id` is ``null`` for a handle that
  refers to no object. IDs start at 0 in every patcher and the smallest free number
  is reused after a delete, so ``0`` is a real object, never "none".
- **Send messages directly to a specific inlet** — bypassing the
  named-receiver layer. :py:meth:`PHandle.sendBang`,
  :py:meth:`PHandle.sendInt`, :py:meth:`PHandle.sendFloat`, and
  :py:meth:`PHandle.sendList` are the same API a connected outlet
  would call.
- **Reconfigure an object after the fact.**
  :py:meth:`PHandle.setParams` re-runs the object's creation parser
  on a new argument string — equivalent to deleting and recreating it
  but with the connections preserved.
- **Bridge a GUI.** :py:attr:`PHandle.guiValue` (see the "GUI controls"
  section of :doc:`/api/patcher`),
  :py:meth:`PHandle.getGuiProperty`, and
  :py:meth:`PHandle.setGuiProperty` expose the per-object metadata an
  external editor reads and writes back.

In short: ``Patcher.pass*`` is the convenient broadcast path through
named receives, and ``PHandle.send*`` is the targeted path to one
specific inlet.

Enumerating the graph
---------------------

You can ask the patcher how many objects it contains and fetch a
handle to each one by index or by ID:

.. code-block:: dart

   for (var i = 0; i < p.objects; i++) {
     final h = p.getHandleAt(i);
     print('${h.id}: ${h.type} (${h.inputs} in, ${h.outputs} out)');
   }

This is the entry point for tooling that walks an existing graph —
visualisers, exporters, automated tests that assert on object counts.

Persistence
-----------

The graph — every object, parameter, and connection — can be
serialised to JSON with :py:meth:`Patcher.dumpJson`. The result is
plain UTF-8 text, suitable for writing to disk or shipping as a
preset:

.. code-block:: dart

   final json = p.dumpJson();
   File('mypatch.yap').writeAsStringSync(json);

Reloading is the inverse — read the file, hand the contents to
:py:meth:`Patcher.parseJson`, and the patcher rebuilds itself in
place:

.. code-block:: dart

   final p2 = Patcher(mainOutputs: 1);
   p2.parseJson(File('mypatch.yap').readAsStringSync());

:py:meth:`Patcher.parseJson` **adds to** the current graph — objects
already in the patcher are kept, so call :py:meth:`Patcher.clear`
first to replace the patch. Malformed JSON throws
:class:`YseException`, and a load that fails part-way removes the
objects it had already created, so the patcher is left as it was. After
loading, re-send the initial control values so the receives have
something to forward.

A few things about the file itself:

- **Object IDs are per patcher.** ``dumpJson`` numbers the objects from
  0 in ID order, so two identical patches save identically. A loaded
  patch is renumbered on its next save; if you keep object IDs outside
  the file, look them up again after loading.
- **The name is saved.** A name set with :py:attr:`Patcher.name` (or
  ``Patcher(name: ...)``) is written as a top-level ``"name"`` key and
  applied by ``parseJson`` to a patcher that still has its auto-name; a
  name you already set wins. The name is what puts the patcher's
  ``.s`` / ``.r`` slots on the bus as ``patcher.<name>.<slot>`` (see
  :doc:`/api/bus`).
- **GUI properties ride along.** Whatever an editor stored with
  :py:meth:`PHandle.setGuiProperty` is saved under each object's
  ``"gui"`` key.

The round trip — patch built once in code, saved to a string,
rebuilt from that string with no code-side knowledge of its internal
structure — is what makes external editors possible. Build the patch
in a desktop tool, ship the resulting JSON as an asset, deserialise
it at runtime.

Subpatchers
-----------

A subpatcher groups part of a graph into one object. The parent
connects to the group by pin number, like any other object, and does
not need to know what is inside. It is an object of type
:py:attr:`Obj.patcher` — **not** a separate :class:`Patcher`. Create
its contents with :py:meth:`Patcher.createObject` on the same patcher,
then move them inside with :py:meth:`Patcher.setContainer`.

The pins of a subpatcher are the **boundary objects** among its direct
contents. Each takes one argument, its index (default 0):

- ``.inlet`` / ``.outlet`` (:py:attr:`Obj.gInlet` /
  :py:attr:`Obj.gOutlet`) carry messages.
- ``~inlet`` / ``~outlet`` (:py:attr:`Obj.dInlet` /
  :py:attr:`Obj.dOutlet`) carry audio.

Messages and signals share **one** index space per side, so a
``.inlet 0`` next to a ``~inlet 1`` gives the subpatcher two inlets.
Give every boundary object its own number.

This builds a half-gain stage and plays a sine through it:

.. code-block:: dart

   //  ~sine -> [ ~inlet 0 -> ~* 0.5 -> ~outlet 0 ] -> ~dac
   final p = Patcher();
   final sub    = p.createObject(Obj.patcher);
   final inlet  = p.createObject(Obj.dInlet, args: '0');
   final gain   = p.createObject(Obj.dMultiply, args: '0.5');
   final outlet = p.createObject(Obj.dOutlet, args: '0');
   for (final h in [inlet, gain, outlet]) {
     p.setContainer(h, sub);
   }
   p.connect(inlet, outlet: 0, to: gain, inlet: 0);
   p.connect(gain, outlet: 0, to: outlet, inlet: 0);

   // The parent wires the group by pin number only.
   final sine = p.createObject(Obj.dSine, args: '440');
   final dac  = p.createObject(Obj.dDac);
   p.connect(sine, outlet: 0, to: sub, inlet: 0);
   p.connect(sub, outlet: 0, to: dac, inlet: 0);

Things to know:

- **Place objects before you connect.** Moving an object changes no
  connection. :py:meth:`Patcher.connect` and
  :py:meth:`Patcher.disconnect` on a subpatcher look up the boundary
  object that claims the pin and record the edge straight to it, so
  walking the graph (or :py:meth:`Patcher.dumpJson`) reports the
  boundary object's ID, not the subpatcher's.
- **Ask the subpatcher for its shape.** A subpatcher owns no pins of
  its own, so :py:attr:`PHandle.inputs` / :py:attr:`PHandle.outputs`
  answer 0. :py:attr:`PHandle.subpatcherInlets` /
  :py:attr:`PHandle.subpatcherOutlets` report one past the highest
  index claimed on that side.
- **Containment reads back.** :py:attr:`PHandle.container` is the
  subpatcher an object is directly inside, or ``null`` at the top
  level. ``p.setContainer(obj, null)`` moves it back out. Subpatchers
  nest.
- **Illegal moves throw.** A container that is not a ``patcher``
  object, or a move that would put a subpatcher inside itself or one
  of its descendants, throws :class:`YseException` and changes
  nothing. A handle from another :class:`Patcher` throws
  ``ArgumentError``.
- **Deleting is transitive.** :py:meth:`Patcher.deleteObject` on a
  subpatcher deletes everything inside it, nested subpatchers
  included. Any :class:`PHandle` you held that pointed inside is
  dangling afterwards.
- **It costs nothing to nest.** Every object stays in the patcher's
  one flat graph, so the audio thread's work does not grow with
  nesting depth.
- **Names are not local.** ``.s`` / ``.r`` inside a subpatcher use the
  whole patcher's :py:attr:`Patcher.name`, like everything else.
- **Saving works unchanged.** :py:meth:`Patcher.dumpJson` writes each
  object's container, and :py:meth:`Patcher.parseJson` restores the
  same nesting.

Upstream's `patcher subpatchers guide
<https://github.com/yvanvds/yse-soundengine/blob/main/documentation/source/patcher/subpatchers.rst>`_
covers the same model from the C++ side.

Lifetime and cleanup
--------------------

Three rules cover the patcher's memory model:

1. A :class:`PHandle` is owned by its :class:`Patcher`. Don't try to
   ``dispose`` it directly — call :py:meth:`Patcher.deleteObject` or
   let :py:meth:`Patcher.clear` / :py:meth:`Patcher.dispose` handle
   it.
2. A :class:`Sound` created from a patcher
   (:py:meth:`Sound.fromPatcher`) holds a borrowed pointer to that
   patcher's native handle. The patcher must outlive the sound. The
   sound keeps a Dart reference to it, so dropping your own reference
   is safe; disposing the patcher before the sound is not.
3. ``Patcher`` is a :py:class:`Finalizable`, so a dropped reference
   eventually frees the native graph — but for predictable timing
   (especially when the sound that wraps it is also being disposed),
   call :py:meth:`Patcher.dispose` explicitly when you're done.

Where to find the full object reference
---------------------------------------

The full set of registered object types — every inlet, outlet,
parameter, and accepted message type — is on the
:doc:`/api/patcher_objects/index` reference, one page per category.
Those pages are generated from the registry of the engine this package
binds (a JSON snapshot that a test keeps in step with the registry), so
they list exactly what :py:meth:`Patcher.createObject` accepts.

What you learned
----------------

- A patcher is a node graph. Objects have inlets and outlets; data
  flows along :py:meth:`Patcher.connect` edges.
- ``~`` types run at audio rate, ``.`` types fire on events.
- Use ``.r`` (receive) objects plus
  :py:meth:`Patcher.passFloat` / :py:meth:`Patcher.passInt` etc. to
  drive the graph from application code, or
  :py:meth:`PHandle.sendFloat` etc. to address one specific inlet.
- :class:`PHandle` exposes every piece of structural and runtime
  state for an object — type, connections, GUI properties.
- :py:meth:`Patcher.dumpJson` and :py:meth:`Patcher.parseJson` round-
  trip the whole graph — ship patches as plain text or build an
  external editor on top.

Next
----

- :doc:`/api/patcher` — the full :class:`Patcher` and
  :class:`PHandle` class reference.
- :doc:`/api/patcher_objects/index` — every patcher object, with inlets,
  outlets, parameters, and value ranges.
- :doc:`/tutorials/index` — index of remaining tutorials.
