From 13d024ee5217011b491f49f53ba666798500ca9a Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Fri, 9 Oct 2026 20:07:30 -0500 Subject: [PATCH 1/2] Documentation for both objects, in Max's own system and the ReadMe (9.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ReadMe gains "Objects without audio: tap.python" — loading, the output table, outlets and the dumpout, messages and anything, the threads and their limits — and points the reader to the Overview, the help files and the Documentation window. For that window: tap.python's reference page, by hand (docs/tap.python.maxref.xml; a plain SDK class has no generator), with see-also links both ways (tap.python~'s through MIN_RELATED, its page rewritten by Max); a package topic (docs/topics/), the guide "Writing Max Objects in Python" (docs/vignettes/), and three tutorials with their patchers (docs/tutorials/pythontap-tut/: a gain per sample and per vector, a filter with prepare(), an object without audio). The help patchers are in tabs — tap.python's basic, lists, outlets, messages and threads; tap.python~'s basic, numpy, allpass and mc. — with Max's ? tab; the PythonTap Overview is in extras/, the Extras menu and the package's home patcher. Writing the mc. tab found that [mc.tap.python~] was "No such object": Max wraps an external in mc. through an init/ mapping, as its own init/ maps mc.cycle~, and the package had none. init/ tap.python.txt now maps mc.tap.python~ to Max's MC wrapper, assembling a package requires the line, and a runtime test pins it (failing eight checks without the line): two instances, setvalue and applyvalues reaching the class's attributes and messages — which, being each instance's own, a plain message to the wrapper does not. The ReadMe says so. scripts/check-docs.py checks what is written by hand, in CI: the pages well-formed with the right roots, every link between them resolving, every patcher's patch cords whole. Checked in Max 9.1.5 on macOS: every page and patcher opened, the help tabs clicked through, the tutorial patchers run (euclid's rhythm, note_name's C# and 4, scale's list), the Package Docs' Topics, Guides and Tutorials listing the topic, the guide and the tutorials, a tutorial's Open Tutorial opening its patcher, and the Overview in the Extras menu; every runtime test passes. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz --- .github/workflows/build.yml | 6 + CHANGELOG.md | 12 + CLAUDE.md | 21 +- ReadMe.md | 50 +- docs/tap.python.maxref.xml | 136 + docs/tap.python~.maxref.xml | 1 + docs/topics/pythontap_topic.maxvig.xml | 37 + .../pythontap-tut/pythontap_01_gain.maxpat | 645 ++++ .../pythontap_01_gain.maxtut.xml | 81 + .../pythontap-tut/pythontap_02_filter.maxpat | 737 ++++ .../pythontap_02_filter.maxtut.xml | 86 + .../pythontap-tut/pythontap_03_control.maxpat | 1078 ++++++ .../pythontap_03_control.maxtut.xml | 85 + docs/vignettes/pythontap_writing.maxvig.xml | 149 + extras/PythonTap Overview.maxpat | 674 ++++ help/tap.python.maxhelp | 2451 ++++++++++++++ help/tap.python~.maxhelp | 2965 ++++++++++++++--- init/tap.python.txt | 1 + package-info.json.in | 2 +- runtime-tests/make_patchers.py | 33 +- .../patchers/tap.python~.mc.maxtest.maxpat | 2407 +++++++++++++ scripts/assemble-package.py | 25 +- scripts/check-docs.py | 144 + .../tap.python_tilde/tap.python_tilde.h | 2 +- 24 files changed, 11298 insertions(+), 530 deletions(-) create mode 100644 docs/tap.python.maxref.xml create mode 100644 docs/topics/pythontap_topic.maxvig.xml create mode 100644 docs/tutorials/pythontap-tut/pythontap_01_gain.maxpat create mode 100644 docs/tutorials/pythontap-tut/pythontap_01_gain.maxtut.xml create mode 100644 docs/tutorials/pythontap-tut/pythontap_02_filter.maxpat create mode 100644 docs/tutorials/pythontap-tut/pythontap_02_filter.maxtut.xml create mode 100644 docs/tutorials/pythontap-tut/pythontap_03_control.maxpat create mode 100644 docs/tutorials/pythontap-tut/pythontap_03_control.maxtut.xml create mode 100644 docs/vignettes/pythontap_writing.maxvig.xml create mode 100644 extras/PythonTap Overview.maxpat create mode 100644 help/tap.python.maxhelp create mode 100644 runtime-tests/patchers/tap.python~.mc.maxtest.maxpat create mode 100644 scripts/check-docs.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5014585..90effd3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -136,6 +136,12 @@ jobs: libpython=$(python -c "import sysconfig; print(sysconfig.get_config_var('LIBDIR') + '/' + sysconfig.get_config_var('LDLIBRARY'))") scripts/check-data-imports.sh externals/tap.python~.mxl_x86_64 "$libpython" + # What is written by hand, which no build makes and no test runs (plan 9.4): the reference + # pages, the guide and the tutorials well-formed and linked, the patchers' patch cords whole. + - name: Check the hand-made documentation + if: matrix.sanitize == '' + run: python scripts/check-docs.py + # The release packaging's license collection (plan 4.6), against this interpreter's own # install, which is laid out like the macOS runtime and has the locked packages in it. - name: Collect the licenses a package would ship diff --git a/CHANGELOG.md b/CHANGELOG.md index d924a71..238fa6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,8 +28,20 @@ lands. - **`default.py` has a `bang`**, which returns the gain: `[tap.python]` with no argument outputs it. In `tap.python~` a `bang` now calls that method, its result dropped there as every method's is. +- **Documentation in Max's own system** (plan 9.4): a reference page for `tap.python`, a guide — + *Writing Max Objects in Python* — and three tutorials with their patchers in the Documentation + window (Package Docs › PythonTap), the help patchers of both objects in tabs, and a *PythonTap + Overview* in the Extras menu, which is also the package's home patcher. + ### Fixed +- **`mc.tap.python~` was "No such object".** Max wraps an external in `mc.` through a mapping in + an `init/` file, as its own `init/` maps `mc.cycle~`; the package had none, so the ReadMe's "wrap + the object in `mc.`" did not work. `init/tap.python.txt` now maps `mc.tap.python~` to Max's MC + wrapper around `tap.python~` (found writing the help patcher's `mc.` tab). The wrapper passes a + class's attributes and messages to its instances through `setvalue` and `applyvalues`; a plain + message to every instance is not understood, as each instance's are its own. Pinned by a runtime + test in Max. - **`tap.python~`: an attribute set or read on the scheduler thread while the class reloaded** raced the reload's changes to the object's list of attributes, read on one thread while written on the other. The list now has a lock of its own. Found while building `tap.python`, which looks diff --git a/CLAUDE.md b/CLAUDE.md index 8f87e88..09c9dfc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,20 @@ in the same binary by 9.3; its documentation (9.4) and runtime tests (9.5) are s (`tap.python_package.h`) and the file watcher (`tap.python_filewatch.h`). Its `_test.cpp` drives the Max glue of both through min-api's mock kernel, with stubs for what that kernel lacks made faithful to the SDK (the obex's dumpout, `outlet_insert_after`, the box's dynlets). +- **Documentation in Max's own system** (plan 9.4) — `docs/tap.python~.maxref.xml` (min's, never by + hand) and `docs/tap.python.maxref.xml` (by hand: a plain SDK class has no generator); the package + topic in `docs/topics/`, the guide *Writing Max Objects in Python* in `docs/vignettes/`, and three + tutorials with their patchers in `docs/tutorials/pythontap-tut/` — the Documentation window's + Package Docs › PythonTap lists them under Topics, Guides and Tutorials, by those folders; the help + patchers `help/tap.python~.maxhelp` and `help/tap.python.maxhelp`, in tabs (`[p name]` with + `showontab`, and an empty `[p ?]` that Max fills); and `extras/PythonTap Overview.maxpat`, in the + Extras menu and the package's home patcher (`package-info.json.in`). The Documentation window + lists `tap.python` as a reference page, not an object — it has no file of its own, as Max's + `init`-mapped objects (`mc.abs~`) have not. `scripts/check-docs.py` (CI) checks them all: the XML + well-formed, every link resolving, every patch cord whole. +- **`init/tap.python.txt`** — read by Max at launch: `tap.python` and `mc.tap.python~` are mapped to + the `tap.python~` binary (the second through Max's MC wrapper, as Max's own `init/` maps + `mc.cycle~`); `assemble-package.py` fails a package without both lines. - **`python/`** — the user's script folder in the package (and the examples: `default.py`, `numpy_gain.py`, `allpass.py`, `numpy_allpass.py` — the same filter per vector, checked equal sample for sample by the core battery — `stereo_width.py`, two inputs and two outputs, and @@ -106,7 +120,7 @@ python3 runtime-tests/run.py ``` CI (`build.yml`): `linux-core` and `linux-max-glue` (each release, asan-ubsan, tsan — the glue -test's `TAP_PYTHON_SANITIZE`, as the core's), `macos` (universal + `lipo`/`otool` checks, including +test's `TAP_PYTHON_SANITIZE`, as the core's; the release row also runs `scripts/check-docs.py`), `macos` (universal + `lipo`/`otool` checks, including no absolute rpath, and the package assembled, which fails without `init/tap.python.txt`), `windows`. `style.yml`: TapHouse drift check, clang-format, clang-tidy (a clang-tidy failure or crash fails the gate, not just a finding). Workflows run with `contents: read`, pin third-party actions by commit SHA (tag noted beside it), and cancel @@ -207,7 +221,8 @@ attaches all the zips + SHA256s to a release — a pre-release for 0.x, a draft (`MIN_DESCRIPTION`, argument and message descriptions — min regenerates `docs/tap.python~.maxref.xml` from them when Max loads an external newer than the page, so never hand-edit the page: rebuild, run Max once — `runtime-tests/run.py` says when the page was - rewritten — and commit it), `help/tap.python~.maxhelp`, the examples and their notebook - (committed executed), and the plan. + rewritten — and commit it), `docs/tap.python.maxref.xml` (by hand), the guide and tutorials in + `docs/`, both help patchers and the Overview — checked open in Max, not only by + `scripts/check-docs.py` — the examples and their notebook (committed executed), and the plan. - **Implement from documentation and published sources only** — the CPython C-API docs, the Max SDK docs — never by reverse-engineering another product. diff --git a/ReadMe.md b/ReadMe.md index ff2422b..2d47c9d 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -31,6 +31,8 @@ class default: [tap.python~ default] ← loads python/default.py, exposes a 'gain' attribute ``` +`tap.python`, in the same package, runs a class as an object without audio — messages in, messages out — and **outputs what its methods return**: a bang to `[tap.python euclid]` outputs a Euclidean rhythm as a list. See [Objects without audio](#objects-without-audio-tappython). + ## Requirements - **Max 9** or later (macOS 11.0+ on Intel or Apple Silicon, Windows 10 22H2+ / Windows 11, 64-bit). @@ -44,7 +46,7 @@ Download a package from the [releases page](https://github.com/tap/PythonTap/rel - `PythonTap--macos-x86_64.zip` — Intel Macs, and Apple Silicon Macs running Max under Rosetta. - `PythonTap--windows-x64.zip` — Windows. -Unzip it into your `Documents/Max 9/Packages` folder, restart Max, and open the `tap.python~` help patcher. Each zip has a `.sha256` beside it, and the release's `SHA256SUMS` lists them all. The package's `licenses/` folder holds the license of everything it ships. +Unzip it into your `Documents/Max 9/Packages` folder, restart Max, and open **PythonTap Overview** from the Extras menu — or the help patcher of `tap.python~` or `tap.python`, or the package's guide and tutorials in the Documentation window. Each zip has a `.sha256` beside it, and the release's `SHA256SUMS` lists them all. The package's `licenses/` folder holds the license of everything it ships. Releases are not code-signed yet. On a Mac, a downloaded unsigned external is quarantined and Max will not load it; clear the quarantine once after unzipping: ```sh @@ -94,7 +96,7 @@ Python sources live in the package's `python` folder. `[tap.python~ name]` loads - `process(self, x: np.ndarray) -> np.ndarray` is called **once per signal vector** with a numpy array of the input and must return an array of the same length (any numeric dtype, or a list; it is converted). This is the form to use for anything that must run in real time — see `python/numpy_gain.py`, and `python/numpy_allpass.py` for a filter with feedback. The input array is reused from one call to the next: copy it if you want to keep it. - `process(self, x: float) -> float` is called **once per sample** — simplest for sketching; the call is cheap, but every line of Python in it runs once per sample (see [the performance note](#a-note-on-performance)). - Its parameters are the object's signal inlets and its return hint its outlets: `process(self, left: np.ndarray, right: np.ndarray) -> tuple[np.ndarray, np.ndarray]` makes an object with two of each (see `python/stereo_width.py`), and `process(self) -> float`, with no inputs, a generator — the first inlet is always there, for messages. The inputs are all `np.ndarray` or all per sample, and a tuple return must say how many values it has (`tuple[float, float]`). A save that changes how many changes the object's inlets and outlets to match, keeping the patch cords of those that stay. Wrap the object in `mc.` to run one instance per channel of a multichannel signal. Output that is not a number, or not finite (NaN, infinity), is replaced with 0.0 and reported once in the Max console, as is a return with the wrong number of values. + Its parameters are the object's signal inlets and its return hint its outlets: `process(self, left: np.ndarray, right: np.ndarray) -> tuple[np.ndarray, np.ndarray]` makes an object with two of each (see `python/stereo_width.py`), and `process(self) -> float`, with no inputs, a generator — the first inlet is always there, for messages. The inputs are all `np.ndarray` or all per sample, and a tuple return must say how many values it has (`tuple[float, float]`). A save that changes how many changes the object's inlets and outlets to match, keeping the patch cords of those that stay. Wrap the object in `mc.` — `[mc.tap.python~ numpy_gain]` — to run one instance per channel of a multichannel signal. A class's attributes and messages are each instance's own, so Max's MC wrapper passes them on through its own messages, not as they are: `setvalue 2 gain 0.1` to the second instance, `applyvalues gain 0.5 0.25` one value to each; a plain `gain 0.5` is not understood. Output that is not a number, or not finite (NaN, infinity), is replaced with 0.0 and reported once in the Max console, as is a return with the wrong number of values. - **Worker mode** — by default `process()` runs on Max's audio thread, so whatever else holds Python's interpreter — a reload, a message, another instance — holds up the audio while it does. With `@mode worker` it runs on a thread of its own instead, `@latency` milliseconds behind the audio (30 by default, rounded up to whole signal vectors): the audio thread then only copies vectors to and from that thread, and never waits for Python. Max computes a whole I/O vector's worth of signal vectors at once, so `@latency` must be longer than the I/O vector (Options > Audio Status: 512 samples is 11.6 ms at 44.1 kHz) — what is left over is the time Python has to keep up; raise it with a larger I/O vector. If Python falls further behind, the vectors it is late for are output as silence and the console says so, once per load; the delay stays the same. The read-only `@latencysamples` gives the delay in samples, for aligning other signal paths (a `delay~`, say); in direct mode it is 0. The worker thread has the real-time scheduling of an audio thread. `@mode` and `@latency` take effect as soon as they are set, by rebuilding the signal chain. Stopping the worker (DSP off, a chain rebuild, the object deleted) waits up to 100 ms for the vector in progress; a `process()` that has not returned by then is interrupted with a `WorkerStopped` exception (a `BaseException`, so `except Exception:` does not swallow it), that vector is silence, the console says so once, and the class stays bound; one that still has not returned after a further 250 ms is blocked in a call Python cannot interrupt (`time.sleep`, a long C call) and is abandoned: audio continues on a new worker, while the abandoned thread keeps costing a core until Max quits. - **Audio settings** — an optional method `prepare(self, sample_rate: float, vector_size: int) -> None` is called with Max's sample rate and vector size before the object processes any audio, again whenever they change, and on every reload before the new code runs. `python/allpass.py` uses it to size its delay line. - **Hot reload** — saving the `.py` file reloads it in place, as a fresh module (names you deleted from the file are gone): the attributes and messages follow the new class, and audio resumes with the new code. Until the new code is ready the object keeps running the old one, so a successful reload swaps in without a gap in the audio. Attribute values carry over — set from the patcher or by your own code — for every attribute the new class still has with the same type; an attribute whose type changed starts from its new default, and one you removed disappears from the object. If the file has an error, the object prints the traceback to the Max console and outputs silence until the next successful reload (and the attribute values come back with it); a save that breaks a file shared by many objects is reported once, not by each, while an object created later with the file still broken says so again. Every object using the same file shares one execution of it per save, and the console says so once — `Loaded name.py: process() bound, one call per sample` (or per vector), from whichever object ran the file — along with anything true of the class, such as a method skipped for its name; a save that changes nothing reloads silently. Errors that belong to one object, such as an exception in its constructor, are still reported by each. Max's file watcher notices a save within a couple of seconds, but it coalesces saves made in quick succession — a script saving once a second, say — so the object then reloads for only some of them, and can lag behind until they stop; sending the object `filechanged` reloads it at once. @@ -141,6 +143,50 @@ Max's own DSP CPU meter (`adstatus cpu`): the mean of ten readings a second apar +## Objects without audio: `tap.python` + +`[tap.python name]` runs a class as an ordinary Max object — messages in, messages out, no signal — with everything [Writing a class](#writing-a-class) says but the audio: the same `python` folder and loader (one file can serve a `tap.python~` and a `tap.python` at once, executed once per save), the same attributes, messages, hot reload, console and error guards. And one thing more: **what a method returns is what the object outputs**. + +```python +from attrs import define, field + +@define +class euclid: + steps: int = field(default = 8) + pulses: int = field(default = 3) + + def bang(self) -> list[int]: + steps = max(1, self.steps) + return [int((i * self.pulses) % steps < self.pulses) for i in range(steps)] +``` + +``` +[tap.python euclid] ← a bang outputs 1 0 0 1 0 0 1 0; steps 16, @pulses 5 or an attrui change it +``` + +- **Output** — what a method returns goes out of the object before the message returns, converted by its type: + + | The method returns | The object outputs | + |---|---| + | `None` | nothing | + | a `bool` (numpy's too) | an int, 0 or 1 | + | an `int` (anything with `__index__`: `np.int64`, an `IntEnum`) | an int (64-bit: a value past it is reported, and nothing is output) | + | a `float` (anything with `__float__`: `np.float32`) | a float — NaN and infinity as they are | + | a `str` | `symbol `: one symbol, whatever it holds — `"hello world"` is one symbol, `"60"` the symbol and not the number | + | a `list`, a `range`, a 1-D `np.ndarray`, or a `tuple` from a method not hinted `-> tuple[…]` | a list, each element by the rows above — or, when the first element is a `str`, the message it names (`["note", 60, 100]` outputs `note 60 100`); an empty one outputs nothing | + | *n* values from a method hinted `-> tuple[…]` of *n* members | one value per outlet, right to left; `None` in a slot outputs nothing from that outlet | + | anything else — a `dict`, a `set`, `bytes`, a 2-D array, an object — or a list holding `None` or another list | nothing, and the console says why | + + A `str` is output as `symbol ` because that is what reaches every receiver as the string it is: as the message it names, `"bang"`, `"int"` and `"list"` would not be data at all. `[sel C]` matches it; `[route C]` does not (put `[route symbol]` first), `[prepend]` keeps the word `symbol`, and a message box's `[set $1(` displays it. To output the message a string names, return it in a list — `["start"]`. +- **Outlets** — the object has as many outlets as the widest `tuple[…]` return hint among its methods, at least one, plus a dumpout at the right: `getsteps` outputs `steps 8` from it, as a Max object with attributes does. The hint is the one place a type hint changes what a value does: unhinted, a returned tuple is a list from the first outlet. A tuple of unsaid length (`tuple[int, ...]`) keeps one outlet and is output as a list, which the console says once. A save that changes how many outlets the class needs changes the object's outlets in place, keeping the patch cords of those that stay. +- **Messages** — as in `tap.python~`, called according to their signatures, with `int`, `float`, `symbol`, `bang` and `list` answering those standard messages. A method named `anything` answers every message the class has no method or attribute for, with the selector first — `def anything(self, selector: str, *args)`; without one, the object says it doesn't understand, as Max objects do. `process()` and `prepare()` are ordinary methods here: `process 0.5` calls the class's `process()` and outputs what it returns, a way to try a filter sample by sample. The names Max or the object handle themselves are `tap.python~`'s less the audio ones (`dsp64`, `mode`, `latency` and the rest are free), plus `dumpout`. +- **Threads** — a message runs on the thread it arrives on, as an ordinary Max object's does — Max's main thread, or its scheduler thread with Overdrive on — and its result is output before the message returns, so `trigger`, `metro` and the rest compose with it as with any object. The limits: + - A method holds the thread it runs on for as long as it takes: under Overdrive, a heavy method driven by a `metro` holds Max's scheduler meanwhile. Sent through `[deferlow]`, it runs on the main thread instead. + - With **Scheduler in Audio Interrupt** on (and audio running), the scheduler thread is the audio thread, so a message from a `metro` runs Python on the audio thread, where it waits for any other Python — a reload, another object's message — and can interrupt the audio. The object says so in the console, once per session; `[deferlow]` takes the work off the audio thread. + - What a method returns is the only output: a thread your class starts (`threading.Thread`) has no outlet to reach. + - A `dict` is not output as a Max dictionary yet. +- **Performance** — a message costs the conversion of its arguments in, your method, and the conversion of what it returns out, all on the thread the message came on; [the performance note](#a-note-on-performance) has what the bridge itself costs. + ## Building from source Requires CMake 3.19+ and a C++20 compiler (Xcode 12+ / Visual Studio 2019+). The build links against the embedded runtime, so install it first (see [Installation](#installation)). diff --git a/docs/tap.python.maxref.xml b/docs/tap.python.maxref.xml new file mode 100644 index 0000000..bc0e801 --- /dev/null +++ b/docs/tap.python.maxref.xml @@ -0,0 +1,136 @@ + + + + + + + + + Run a Python class as a Max object + + + Run a Python class as a Max object without audio: messages in, and what its methods return out. The argument names a file in the package's python folder, which must define a class of the same name. The class's type-annotated public fields become attributes and its public methods messages, as in tap.python~; what a method returns is output, converted by its type, from as many outlets as the widest tuple return hint among its methods asks for. The file is watched and reloaded when saved, keeping attribute values. + + + +

+ tap.python loads its class file exactly as tap.python~ does — the same python folder, the same attributes and messages from the class's type hints, the same hot reload, console and error guards; one file can serve both objects at once — but runs no audio. Instead, what each method returns is what the object outputs, before the message returns: +

+
    +
  • None outputs nothing; a bool an int, 0 or 1; an int an int; a float a float.
  • +
  • A str outputs symbol followed by the string, one symbol whatever it holds: sel matches it, route needs route symbol first, and a message box's set $1 displays it.
  • +
  • A list, a range, a one-dimensional numpy array, or a tuple from a method not hinted to return a tuple outputs a list; when its first element is a str, the message that names, with the rest as arguments (["note", 60, 100] outputs note 60 100).
  • +
  • A method hinted to return tuple[A, B, ...] fills one outlet per value, right to left; None in a slot outputs nothing from that outlet.
  • +
  • Anything else — a dict, a set, a two-dimensional array, an object — outputs nothing, and the console says why.
  • +
+

+ A message runs on the thread it arrives on, as an ordinary Max object's does: the main thread, or the scheduler thread with Overdrive on. With Scheduler in Audio Interrupt on, a message from the scheduler runs Python on the audio thread, which the object says once per session: send such messages through deferlow to run them on the main thread. +

+
+ + + + Tim Place + PythonTap + Programming + Languages + + + + + + Messages for the class's methods, and its attributes + A message calls the class's method of that name with its arguments; an attribute's name sets it, and get followed by its name outputs it from the dumpout. + + + + + + + What a method returns + What a method returns, converted by its type — or, from a method hinted to return a tuple of several values, the first of them. A method's tuple return hint of n values gives the object n such outlets, filled right to left. + + + Dumpout: attribute values + The rightmost outlet: an attribute's name and value, in answer to get followed by its name (getsteps outputs steps 8). + + + + + + + Python class file + The name of a .py file in the package's python folder, without the extension, which must define a class of the same name; it must be a valid Python identifier. Without an argument, python/default.py is loaded. + + + + + + + + Call the class's bang method + Calls the class's method named bang, and outputs what it returns. Without one, the class's anything method, if it has one. + + + + + + Call the class's int method + Calls the class's method named int with the number, and outputs what it returns. Without one, the class's anything method, if it has one. + + + + + + Call the class's float method + Calls the class's method named float with the number, and outputs what it returns. Without one, the class's anything method, if it has one. + + + + + + Call the class's list method + Calls the class's method named list with the list's elements — all of them, as one list or numpy array, when its last parameter is hinted list[...] or np.ndarray — and outputs what it returns. Without one, the class's anything method, if it has one. + + + + + + Call the class's method of that name + A message calls the class's public method of that name, its arguments converted by the method's type hints, and outputs what it returns; an attribute's name sets the attribute. A message the class has no method or attribute for goes to the class's method named anything, with the message's name as its first argument — def anything(self, selector: str, *args) — and is otherwise not understood. + + + + Reload the Python file + Reload the Python file. The object's file watcher sends it when the file is saved; send it yourself to force a reload. + + + + + + + + + + Each type-annotated public field of the class is an attribute: int and bool fields are long (a bool an on/off toggle), float fields float64, and any other a symbol. They are set and read as any attribute is — in the object box (@steps 16), by message, with attrui and getattr, and get followed by the name outputs the value from the dumpout — and keep their values when the file is saved. + + + + + + What a method returns, converted by its type: nothing for None; an int or a float; symbol and the string for a str; a list for a list, a range or a numpy array, or the message a list names when its first element is a str; one value per outlet from a method hinted to return a tuple. + + + + + + + + + + + + +
diff --git a/docs/tap.python~.maxref.xml b/docs/tap.python~.maxref.xml index 0be3718..80bb377 100644 --- a/docs/tap.python~.maxref.xml +++ b/docs/tap.python~.maxref.xml @@ -67,6 +67,7 @@ + diff --git a/docs/topics/pythontap_topic.maxvig.xml b/docs/topics/pythontap_topic.maxvig.xml new file mode 100644 index 0000000..221acdb --- /dev/null +++ b/docs/topics/pythontap_topic.maxvig.xml @@ -0,0 +1,37 @@ + + + + + Tim Place + PythonTap + Python + Languages + + +

PythonTap

+

+ Write Max objects in Python. PythonTap runs a Python class as a Max object: the class's typed + fields become attributes, its methods messages, and saving the file reloads it in place. It comes + with CPython 3.13, numpy and attrs. +

+ +

Objects

+
    +
  • tap.python~ - Process audio with a Python class: its process() method runs on the signal, per sample or per vector.
  • +
  • tap.python - Run a Python class as a Max object without audio: what a method returns is what it outputs.
  • +
+ +

Learning

+
    +
  • Writing Max Objects in Python - the guide to both objects.
  • +
  • Tutorial 1: A gain, per sample and per vector
  • +
  • Tutorial 2: A filter that knows the sample rate
  • +
  • Tutorial 3: An object without audio
  • +
  • PythonTap Overview - both objects at work, from the Extras menu.
  • +
+ + + + + +
diff --git a/docs/tutorials/pythontap-tut/pythontap_01_gain.maxpat b/docs/tutorials/pythontap-tut/pythontap_01_gain.maxpat new file mode 100644 index 0000000..8eabc74 --- /dev/null +++ b/docs/tutorials/pythontap-tut/pythontap_01_gain.maxpat @@ -0,0 +1,645 @@ +{ + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 760.0, + 560.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "PythonTap Tutorial 1", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A gain, per sample and per vector", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The Documentation window has this tutorial's text. Turn on the audio with the speaker.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 150.0, + 74.0, + 24.0 + ], + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 190.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 200.0, + 190.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "message", + "text": "float 0.5", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 225.0, + 92.39999999999999, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "message", + "text": "greet max", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 120.39999999999999, + 225.0, + 92.39999999999999, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ default", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 265.0, + 151.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ numpy_gain", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 200.0, + 265.0, + 172.0, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "comment", + "text": "per sample: default.py", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 295.0, + 170, + 26.849999999999998 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "comment", + "text": "per vector: numpy_gain.py", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 200.0, + 295.0, + 190, + 26.849999999999998 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "message", + "text": "1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 335.0, + 40.0, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "message", + "text": "2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 68.0, + 335.0, + 40.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "comment", + "text": "listen to one or the other", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 120.0, + 335.0, + 220, + 26.849999999999998 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "selector~ 2 1", + "numinlets": 3, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 375.0, + 109.0, + 24.0 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 415.0, + 60.0, + 24.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 20.0, + 450.0, + 45.0, + 45.0 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "metro 250 @active 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 200.0, + 415.0, + 151.0, + 24.0 + ], + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "adstatus cpu", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "int" + ], + "patching_rect": [ + 200.0, + 445.0, + 102.0, + 24.0 + ], + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 200.0, + 475.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-21" + } + }, + { + "box": { + "maxclass": "comment", + "text": "CPU %", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 265.0, + 475.0, + 60, + 26.849999999999998 + ], + "id": "obj-22" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Make your own: save a copy of default.py as python/mygain.py, rename its class mygain, and type tap.python~ mygain into a new object box. Every save reloads it.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 420.0, + 150.0, + 300, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-23" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-5", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-16", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-16", + 2 + ] + } + }, + { + "patchline": { + "source": [ + "obj-16", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-18", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-19", + 0 + ], + "destination": [ + "obj-20", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-20", + 0 + ], + "destination": [ + "obj-21", + 0 + ] + } + } + ] + } +} diff --git a/docs/tutorials/pythontap-tut/pythontap_01_gain.maxtut.xml b/docs/tutorials/pythontap-tut/pythontap_01_gain.maxtut.xml new file mode 100644 index 0000000..7bb898b --- /dev/null +++ b/docs/tutorials/pythontap-tut/pythontap_01_gain.maxtut.xml @@ -0,0 +1,81 @@ + + + + + + +
PythonTap Tutorial 1: A Gain, per Sample and per Vector
+ + A Python class as an audio object + + tap.python~ runs a Python class as an audio object. The class lives in a file in the + PythonTap package's python folder; the object loads it, makes attributes of its typed fields and + messages of its methods, and calls its process() method on the signal. In this tutorial a + gain — the smallest audio object there is — is written two ways, and then you write your own. + + Open the tutorial patcher, and turn on the audio with the speaker. + + The class + + The left object, [tap.python~ default], loads python/default.py and makes an object of the + class default in it — the file, the class and the argument share one name. Here is the part that + matters (open the file in any text editor to see the rest): + + from attrs import define, field + +@define +class default: + gain: float = field(default = 1.0) + + def process(self, x: float) -> float: + return x * self.gain + + The field gain, annotated float, is an attribute of the object: the attrui + above it sets it, and so would the message gain 0.5 or gain 0.5 typed into the + object box. Its public methods are messages: float 0.5 calls the class's method named + float, and greet max prints a line to the Max console. + + + Once per sample + + process() is hinted to take a float and return one, so the object calls it once per + sample. That is the simplest way to write audio code, and the call itself is cheap — but every + line of Python in it runs tens of thousands of times a second. + + + Once per vector + + The right object loads numpy_gain.py, the same gain written with numpy: + + def process(self, x: np.ndarray) -> np.ndarray: + return x * self.gain + + Hinted with numpy arrays, process() is called once per signal vector, with the whole vector, + and numpy multiplies it at C speed. Switch between the two with the message boxes and watch the + CPU meter: for a gain the difference is small, but for anything with real work in it the vector + form is the one that keeps up. + + + Your own + + Save a copy of default.py in the python folder as mygain.py, rename the class inside to mygain, + and type tap.python~ mygain into a new object box. Then change process() to soften + the peaks — return math.tanh(x * self.gain), with import math at the top — and save: + the object loads the file again, keeping gain's value, and the audio carries on with the new code. + A mistake prints its traceback to the Max console, and the object outputs silence until a save + fixes it. + + + Summary + + A class's typed fields are attributes, its methods messages, and its process() method runs + on the signal: once per sample when hinted with floats, once per vector when hinted with numpy + arrays. Saving the file reloads it in place. + + + + + + + +
diff --git a/docs/tutorials/pythontap-tut/pythontap_02_filter.maxpat b/docs/tutorials/pythontap-tut/pythontap_02_filter.maxpat new file mode 100644 index 0000000..e7201db --- /dev/null +++ b/docs/tutorials/pythontap-tut/pythontap_02_filter.maxpat @@ -0,0 +1,737 @@ +{ + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 760.0, + 560.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "PythonTap Tutorial 2", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A filter that knows the sample rate", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The Documentation window has this tutorial's text. Turn on the audio with the speaker.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "noise~", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 150.0, + 60.0, + 24.0 + ], + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 185.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "delay", + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 180.0, + 185.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "alpha", + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "message", + "text": "alpha 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 180.0, + 220.0, + 77.19999999999999, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "message", + "text": "clear", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 265.2, + 220.0, + 62.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ allpass", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 260.0, + 151.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ numpy_allpass", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 220.0, + 260.0, + 193.0, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "message", + "text": "1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 305.0, + 40.0, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 68.0, + 305.0, + 40.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "comment", + "text": "allpass.py, or numpy_allpass.py", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 120.0, + 305.0, + 220, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "selector~ 2 1", + "numinlets": 3, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 345.0, + 109.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "+~", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 380.0, + 40.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "comment", + "text": "with the dry signal: the notches", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 65.0, + 380.0, + 240, + 26.849999999999998 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 420.0, + 60.0, + 24.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 20.0, + 455.0, + 45.0, + 45.0 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "metro 250 @active 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 220.0, + 420.0, + 151.0, + 24.0 + ], + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "adstatus cpu", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "int" + ], + "patching_rect": [ + 220.0, + 450.0, + 102.0, + 24.0 + ], + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 220.0, + 480.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-21" + } + }, + { + "box": { + "maxclass": "comment", + "text": "CPU %", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 285.0, + 480.0, + 60, + 26.849999999999998 + ], + "id": "obj-22" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Both objects get every message: the attrui and the message boxes reach the one you hear and the one you don't alike.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 420.0, + 150.0, + 300, + 64.55 + ], + "linecount": 3, + "id": "obj-23" + } + }, + { + "box": { + "maxclass": "comment", + "text": "alpha 1 is refused by the class's validator: its traceback prints to the Max console, and alpha keeps its value.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 420.0, + 230.0, + 300, + 64.55 + ], + "linecount": 3, + "id": "obj-24" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-5", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-5", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-14", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-14", + 2 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-15", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-18", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-19", + 0 + ], + "destination": [ + "obj-20", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-20", + 0 + ], + "destination": [ + "obj-21", + 0 + ] + } + } + ] + } +} diff --git a/docs/tutorials/pythontap-tut/pythontap_02_filter.maxtut.xml b/docs/tutorials/pythontap-tut/pythontap_02_filter.maxtut.xml new file mode 100644 index 0000000..343612a --- /dev/null +++ b/docs/tutorials/pythontap-tut/pythontap_02_filter.maxtut.xml @@ -0,0 +1,86 @@ + + + + + + + +
PythonTap Tutorial 2: A Filter That Knows the Sample Rate
+ + State, and the audio settings + + A gain needs nothing but its input. A filter remembers the samples before, and to turn a time in + milliseconds or a frequency in hertz into samples it needs the sample rate. This tutorial uses + allpass.py, a Schroeder allpass filter, to show both. + + Open the tutorial patcher, and turn on the audio. + + prepare() + + An optional method prepare(self, sample_rate: float, vector_size: int) -> None is called + with Max's sample rate and signal vector size before the object processes any audio, again + whenever either changes, and on every reload before the new code runs. allpass.py uses it to size + its delay line, and prints what it did: look in the Max console for "Setting delay to 1.0 ms". + + + Hearing an allpass + + An allpass filter passes every frequency at the same level and shifts their phase: alone, it + sounds like its input. Mixed with the dry signal, as in the patcher, the phase shifts cancel some + frequencies and reinforce others. Change delay and alpha with the attrui + objects and listen to the notches move. + + + Validators + + The class's fields are attrs fields, and alpha has a validator: it must stay strictly between -1 + and 1. Click alpha 1: the validator raises an exception, its traceback prints to the Max + console, and alpha keeps the value it had. An exception in your code never takes Max down. + + + The same filter, per vector + + numpy_allpass.py computes exactly what allpass.py does — sample for sample — but a vector at a + time, with numpy. Switch to it and compare the CPU meter: per sample, every line of the filter's + Python runs once per sample; per vector, a few numpy expressions run once per vector. + + + Your own + Here is a one-pole lowpass filter, per sample, with its cutoff in hertz: + import math +from attrs import define, field + +@define +class onepole: + cutoff: float = field(default = 1000.0) # Hz + + _fs: float = field(init = False, default = 48000.0) + _y: float = field(init = False, default = 0.0) + + def prepare(self, sample_rate: float, vector_size: int) -> None: + self._fs = sample_rate + + def process(self, x: float) -> float: + a = math.exp(-2.0 * math.pi * self.cutoff / self._fs) + self._y = (1.0 - a) * x + a * self._y + return self._y + + Save it as python/onepole.py and make a tap.python~ onepole object. Fields whose names + start with an underscore are private: they are not attributes, so only cutoff appears in the + inspector. Try computing a in prepare() and again only when cutoff changes, instead + of on every sample. + + + Summary + + prepare() tells the class the sample rate and vector size whenever they change. Its state + lives in private fields, which start afresh on a reload — prepare() is called again before + the new code runs — while its attributes keep their values, guarded by their validators. + + + + + + + +
diff --git a/docs/tutorials/pythontap-tut/pythontap_03_control.maxpat b/docs/tutorials/pythontap-tut/pythontap_03_control.maxpat new file mode 100644 index 0000000..b6d575c --- /dev/null +++ b/docs/tutorials/pythontap-tut/pythontap_03_control.maxpat @@ -0,0 +1,1078 @@ +{ + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 820.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "PythonTap Tutorial 3", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "An object without audio", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The Documentation window has this tutorial's text. Turn on the metro, and the audio to hear the rhythm.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 360, + 64.55 + ], + "linecount": 3, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "int" + ], + "patching_rect": [ + 20.0, + 150.0, + 24.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "metro 150", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 185.0, + 81.0, + 24.0 + ], + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "t b b", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "bang", + "bang" + ], + "patching_rect": [ + 20.0, + 220.0, + 53.0, + 24.0 + ], + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 120.0, + 220.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "steps", + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 280.0, + 220.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "pulses", + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "message", + "text": "getsteps", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 440.0, + 220.0, + 84.8, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python euclid", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 120.0, + 260.0, + 137.0, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "t l l", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 120.0, + 295.0, + 53.0, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 280.0, + 330.0, + 95.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 280.0, + 365.0, + 200, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 500.0, + 295.0, + 95.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 500.0, + 330.0, + 110, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "comment", + "text": "the dumpout", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 500.0, + 365.0, + 100, + 26.849999999999998 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "zl.len", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 120.0, + 330.0, + 60.0, + 24.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "counter", + "numinlets": 3, + "numoutlets": 4, + "outlettype": [ + "int", + "", + "", + "int" + ], + "patching_rect": [ + 20.0, + 330.0, + 67.0, + 24.0 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "%", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "int" + ], + "patching_rect": [ + 20.0, + 365.0, + 40.0, + 24.0 + ], + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "zl.lookup", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 400.0, + 81.0, + 24.0 + ], + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "sel 1", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "bang", + "" + ], + "patching_rect": [ + 20.0, + 435.0, + 53.0, + 24.0 + ], + "id": "obj-21" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "click~", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 470.0, + 60.0, + 24.0 + ], + "id": "obj-22" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.5", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 505.0, + 60.0, + 24.0 + ], + "id": "obj-23" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 20.0, + 540.0, + 45.0, + 45.0 + ], + "id": "obj-24" + } + }, + { + "box": { + "maxclass": "comment", + "text": "note_name.py: two outlets, right to left", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 280.0, + 410.0, + 260, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-25" + } + }, + { + "box": { + "maxclass": "kslider", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "int", + "int" + ], + "patching_rect": [ + 280.0, + 440.0, + 168.0, + 53.0 + ], + "parameter_enable": 0, + "id": "obj-26" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python note_name", + "numinlets": 1, + "numoutlets": 3, + "outlettype": [ + "", + "", + "" + ], + "patching_rect": [ + 280.0, + 500.0, + 158.0, + 24.0 + ], + "id": "obj-27" + } + }, + { + "box": { + "maxclass": "message", + "text": "set $1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 280.0, + 535.0, + 69.6, + 24.0 + ], + "id": "obj-28" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 280.0, + 570.0, + 60, + 24.0 + ], + "id": "obj-29" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 360.0, + 570.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-30" + } + }, + { + "box": { + "maxclass": "comment", + "text": "scale.py: a list through numpy", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 520.0, + 405.0, + 230, + 26.849999999999998 + ], + "id": "obj-31" + } + }, + { + "box": { + "maxclass": "message", + "text": "1 2 3 4", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 520.0, + 435.0, + 77.19999999999999, + 24.0 + ], + "id": "obj-32" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 520.0, + 470.0, + 140, + 24.0 + ], + "parameter_enable": 0, + "attr": "factor", + "id": "obj-33" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python scale", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 520.0, + 500.0, + 130.0, + 24.0 + ], + "id": "obj-34" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 520.0, + 535.0, + 95.0, + 24.0 + ], + "id": "obj-35" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 520.0, + 570.0, + 150, + 24.0 + ], + "id": "obj-36" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-5", + 0 + ], + "destination": [ + "obj-6", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 1 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 1 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 1 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-18", + 0 + ], + "destination": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-19", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-19", + 0 + ], + "destination": [ + "obj-20", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-20", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-20", + 0 + ], + "destination": [ + "obj-21", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-21", + 0 + ], + "destination": [ + "obj-22", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-22", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-23", + 0 + ], + "destination": [ + "obj-24", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-23", + 0 + ], + "destination": [ + "obj-24", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-26", + 0 + ], + "destination": [ + "obj-27", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-27", + 0 + ], + "destination": [ + "obj-28", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-28", + 0 + ], + "destination": [ + "obj-29", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-27", + 1 + ], + "destination": [ + "obj-30", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-32", + 0 + ], + "destination": [ + "obj-34", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-33", + 0 + ], + "destination": [ + "obj-34", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-34", + 0 + ], + "destination": [ + "obj-35", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-35", + 0 + ], + "destination": [ + "obj-36", + 0 + ] + } + } + ] + } +} diff --git a/docs/tutorials/pythontap-tut/pythontap_03_control.maxtut.xml b/docs/tutorials/pythontap-tut/pythontap_03_control.maxtut.xml new file mode 100644 index 0000000..6b1bc4e --- /dev/null +++ b/docs/tutorials/pythontap-tut/pythontap_03_control.maxtut.xml @@ -0,0 +1,85 @@ + + + + + + +
PythonTap Tutorial 3: An Object without Audio
+ + What a method returns is what the object outputs + + Most Max objects are not audio objects: a bang asks for the next value, a list comes in and a list + goes out, a number becomes a name. tap.python runs a Python class as such an object. It + loads the class as tap.python~ does — attributes from its typed fields, messages from its + methods, reloaded when saved — and adds one thing: what a method returns is what the object + outputs. + + Open the tutorial patcher, and turn on the audio to hear the rhythm. + + A rhythm from a bang + The top of the patcher loads euclid.py: + from attrs import define, field + +@define +class euclid: + steps: int = field(default = 8) + pulses: int = field(default = 3) + + def bang(self) -> list[int]: + steps = max(1, self.steps) + return [int((i * self.pulses) % steps < self.pulses) for i in range(steps)] + + A bang calls bang(), and the list it returns — a Euclidean rhythm, steps beats with pulses + of them on, spread as evenly as they go — comes out of the left outlet. The metro bangs it + on every step, so a change to steps or pulses is heard at once; zl.lookup + picks the current step out of the list, and a 1 clicks. + + + The right outlet is the dumpout: getsteps outputs steps 8 from it, as any Max + object with attributes does. + + + Two outlets + The middle of the patcher loads note_name.py: + def int(self, note: int) -> tuple[str, int]: + return NAMES[note % 12], note // 12 - 1 + + The return hint, a tuple of two, gives the object two outlets, filled right to left as Max objects + do: the octave, then the name. Play the keyboard. The name is a str, which the object outputs as + symbol and the string — one symbol, whatever it holds — so sel matches it, and a + message box's set $1 shows it. + + + Lists through numpy + The bottom loads scale.py, whose list method takes a whole list as one numpy array: + def list(self, values: np.ndarray) -> np.ndarray: + return values * self.factor + self.offset + + A last parameter hinted np.ndarray — or list[float] — takes every argument of the + message, and an array returned is output as a list. + + + Threads + + A message runs on the thread it arrives on, as any object's does, and the method's result is output + before the message returns, so trigger, metro and the rest work with it as usual. + With Overdrive on, a metro's bangs arrive on the scheduler thread, which a long method + holds while it runs; with Scheduler in Audio Interrupt also on, that is the audio thread, and the + object says so in the console. Send such messages through deferlow to run them on the main + thread. + + + Summary + + tap.python outputs what a method returns: a number as a number, a str as a symbol, a list + or an array as a list, and the values of a method hinted -> tuple[...] one per outlet. + The ReadMe and the help file have the whole table. + + + + + + + + +
diff --git a/docs/vignettes/pythontap_writing.maxvig.xml b/docs/vignettes/pythontap_writing.maxvig.xml new file mode 100644 index 0000000..2dda281 --- /dev/null +++ b/docs/vignettes/pythontap_writing.maxvig.xml @@ -0,0 +1,149 @@ + + + + + Tim Place + PythonTap + Python + Languages + + +

Writing Max Objects in Python

+ +

+ PythonTap runs a Python class as a Max object. You write the class in a file; the object loads + it, makes Max attributes of its fields and Max messages of its methods, and loads it again every + time you save the file. There are two objects: +

+
    +
  • tap.python~ runs the class as an audio object: its process() method runs on the signal.
  • +
  • tap.python runs it as an ordinary object, without audio: what a method returns is what the object outputs.
  • +
+

+ Both come with CPython 3.13, numpy and attrs, and share one interpreter: Python's own modules, + anything you install into the package's runtime, and your own helper modules are there for every + class. This guide is the short form; the package's ReadMe + says every rule in full, and the tutorials build three objects step by step. +

+ +

The class and its file

+

+ Classes live in the package's python folder. [tap.python~ name] or [tap.python name] loads + python/name.py and instantiates the class name in it: the file, the class and the argument share + one name, which must be a valid Python identifier. Without an argument, python/default.py is + loaded. Open the file in any text editor; other files in the folder can be imported by the class + as helper modules. +

+ from attrs import define, field + +@define +class default: + gain: float = field(default = 1.0) + + def greet(self, name: str) -> None: + print(f"hello {name}, from python!") + + def process(self, x: float) -> float: + return x * self.gain + +

Fields are attributes, methods are messages

+
    +
  • + Attributes. Each public field with a type annotation is a Max attribute: an int a long, + a float a float64, a bool an on/off toggle, anything else a symbol. Set it in the object box + (gain 0.5), by message (gain 0.5), with attrui or getattr. +
  • +
  • + Messages. Each public method is a message, called according to its signature: its type + hints convert the message's arguments, parameters with defaults are optional, and + *args takes any number. Methods named int, float, symbol, + bang and list answer those standard messages, and a last parameter hinted + list[float] or np.ndarray takes all the arguments left, as one list or array. +
  • +
  • + Names Max keeps. A few names belong to Max or the object (dsp64, assist, + notify, filechanged, ...): a method or field of such a name is not exposed, and the + console says so, so that you can rename it. +
  • +
+ +

Audio: tap.python~

+

+ A method named process runs on the signal, in one of two forms chosen by its type hints: + process(self, x: float) -> float is called once per sample — simplest to write, but + every line of Python in it runs tens of thousands of times a second — and + process(self, x: np.ndarray) -> np.ndarray once per signal vector, where numpy works on + the whole vector at C speed: the form for anything with real work in it. Its parameters are the + object's signal inlets and its return hint its outlets (tuple[np.ndarray, np.ndarray] for two), and + an optional prepare(self, sample_rate: float, vector_size: int) is told Max's audio + settings before the audio starts and whenever they change. Wrap the object in mc. to run one + instance per channel. +

+

+ By default process() runs on the audio thread, so anything else holding Python's + interpreter — a reload, a message, another instance — holds the audio up meanwhile. With + mode worker it runs on a thread of its own, latency milliseconds behind the + audio, and the audio thread never waits for Python. +

+ +

Control: tap.python

+

+ tap.python outputs what a method returns, before the message returns: nothing for None, a + number for a number, symbol and the string for a str, a list for a list or a numpy array — + or the message a list names, when its first element is a str — and, from a method hinted + -> tuple[str, int], one value per outlet, right to left. The object has as many outlets + as its widest such hint, and a dumpout at the right that answers get followed by an + attribute's name. A method named anything answers every message the class has no method + for, with the message's name first. +

+ from attrs import define, field + +@define +class euclid: + steps: int = field(default = 8) + pulses: int = field(default = 3) + + def bang(self) -> list[int]: + steps = max(1, self.steps) + return [int((i * self.pulses) % steps < self.pulses) for i in range(steps)] +

+ A message runs on the thread it arrives on, as any Max object's does: Max's main thread, or the + scheduler thread with Overdrive on — where a long method holds the scheduler for its duration. + With Scheduler in Audio Interrupt on, that is the audio thread, and the object says so once: send + such messages through deferlow to run them on the main thread instead. +

+ +

Saving reloads

+

+ Save the file and the object loads it again, in place: the attributes and messages follow the new + class, attribute values carry over, and audio continues with the new code without a gap. A save + that changes process()'s inputs or outputs, or a tap.python class's outlets, changes + the object's inlets and outlets to match, keeping the patch cords of those that stay. Every object + using the file shares one execution of it per save, and the console says so once. Send + filechanged to reload by hand. +

+ +

Errors and the console

+

+ print() writes to the Max console. An exception your code raises — in a method, an + attribute's validator, the constructor, or the file itself — prints its traceback there and never + takes Max down; sys.exit() included. A file that fails to load is reported once, and the + object outputs silence, or nothing, until a save fixes it. What no object can catch is what + happens below Python: a crash in a C extension, os._exit(), or a loop that never ends. +

+ +

More

+
    +
  • The tutorials: A gain, per sample and per vector, A filter that knows the sample rate and An object without audio.
  • +
  • The help files: tap.python~ and tap.python.
  • +
  • The examples, in the package's python folder: default.py, numpy_gain.py, allpass.py and numpy_allpass.py, stereo_width.py, euclid.py, scale.py and note_name.py.
  • +
  • The package's ReadMe: every rule, the performance measurements, and how to add Python packages to the runtime.
  • +
+ + + + + + + +
diff --git a/extras/PythonTap Overview.maxpat b/extras/PythonTap Overview.maxpat new file mode 100644 index 0000000..66a5419 --- /dev/null +++ b/extras/PythonTap Overview.maxpat @@ -0,0 +1,674 @@ +{ + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 820.0, + 620.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "PythonTap", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Write Max objects in Python", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "PythonTap runs a Python class as a Max object: its typed fields become attributes, its methods messages, and saving the file reloads it in place. The classes live in the package's python folder; CPython 3.13, numpy and attrs come with it.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 360, + 102.25 + ], + "linecount": 5, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "tap.python~ — process audio with a Python class", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 200.0, + 360, + 26.849999999999998 + ], + "fontname": "Ableton Sans Bold Regular", + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 235.0, + 74.0, + 24.0 + ], + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 110.0, + 235.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ numpy_gain", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 270.0, + 172.0, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 20.0, + 305.0, + 60.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 20.0, + 340.0, + 45.0, + 45.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "message", + "text": "help tap.python~", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 200.0, + 305.0, + 145.6, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "comment", + "text": "tap.python — a Python class as a Max object: what a method returns, it outputs", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 400.0, + 360, + 45.699999999999996 + ], + "linecount": 2, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "button", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "bang" + ], + "patching_rect": [ + 20.0, + 455.0, + 24.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 55.0, + 455.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "steps", + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python euclid", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 490.0, + 137.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 525.0, + 95.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 560.0, + 200, + 24.0 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "message", + "text": "help tap.python", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 220.0, + 490.0, + 138.0, + 24.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Learn", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 440.0, + 200.0, + 300, + 26.849999999999998 + ], + "fontname": "Ableton Sans Bold Regular", + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The guide, Writing Max Objects in Python, and three tutorials are in the Documentation window: Reference > Package Docs > PythonTap. The tutorials' patchers:", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 440.0, + 230.0, + 330, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "message", + "text": "load pythontap_01_gain.maxpat", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 440.0, + 305.0, + 244.39999999999998, + 24.0 + ], + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "message", + "text": "load pythontap_02_filter.maxpat", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 440.0, + 340.0, + 259.6, + 24.0 + ], + "id": "obj-21" + } + }, + { + "box": { + "maxclass": "message", + "text": "load pythontap_03_control.maxpat", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 440.0, + 375.0, + 267.2, + 24.0 + ], + "id": "obj-22" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "pcontrol", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 440.0, + 555.0, + 74.0, + 24.0 + ], + "id": "obj-23" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Every rule, the performance measurements, and how to add Python packages to the runtime:", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 440.0, + 420.0, + 330, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-24" + } + }, + { + "box": { + "maxclass": "message", + "text": ";\rmax launchbrowser https://github.com/tap/PythonTap#readme", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 440.0, + 465.0, + 330, + 42.0 + ], + "linecount": 2, + "id": "obj-25" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-5", + 0 + ], + "destination": [ + "obj-7", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-7", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-20", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-21", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-22", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + } + ] + } +} diff --git a/help/tap.python.maxhelp b/help/tap.python.maxhelp new file mode 100644 index 0000000..02c010b --- /dev/null +++ b/help/tap.python.maxhelp @@ -0,0 +1,2451 @@ +{ + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 800.0, + 620.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "newobj", + "text": "p basic", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 20.0, + 67.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "tap.python", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Run a Python class as a Max object", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "[tap.python name] loads python/name.py from this package and makes an object of the class name in it: its typed fields are attributes, its methods are messages, and what a method returns is what the object outputs.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 102.25 + ], + "linecount": 5, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "This one is euclid.py. A bang outputs a Euclidean rhythm: steps beats, pulses of them on, spread as evenly as they go. steps and pulses are attributes, from the class's two typed fields.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 185.0, + 330.0, + 102.25 + ], + "linecount": 5, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Open python/euclid.py in a text editor to see the class. Save a change and the object loads it again, keeping steps and pulses; a mistake prints its traceback to the Max console, and the object outputs nothing until a save fixes it.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 280.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Every object using the file shares one load of it per save, tap.python~ objects included.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 390.0, + 330.0, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "button", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "bang" + ], + "patching_rect": [ + 400.0, + 90.0, + 24.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "comment", + "text": "bang: output the rhythm", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 430.0, + 90.0, + 200, + 26.849999999999998 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 130.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "steps", + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 560.0, + 130.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "pulses", + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "message", + "text": "getsteps", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 560.0, + 170.0, + 84.8, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "filechanged", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 652.8, + 170.0, + 107.6, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python euclid", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 400.0, + 215.0, + 137.0, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 265.0, + 95.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 300.0, + 230, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 650.0, + 265.0, + 95.0, + 24.0 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 650.0, + 300.0, + 120, + 24.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "comment", + "text": "what a method returns", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 400.0, + 335.0, + 200, + 26.849999999999998 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "comment", + "text": "the dumpout: get + an attribute's name", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 650.0, + 335.0, + 190, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "comment", + "text": "filechanged reloads the file at once; saving it does that for you.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 400.0, + 390.0, + 330, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "comment", + "text": "More in the other tabs: lists, several outlets, messages and methods, threads.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 400.0, + 435.0, + 330, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-21" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 1 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-16", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "p lists", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 120.0, + 20.0, + 67.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "Lists in, lists out", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A list message through numpy", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "scale.py's method list(self, values: np.ndarray) takes the whole list as one numpy array — a last parameter hinted np.ndarray, or list[float], takes every argument left — and returns an array, which the object outputs as a list.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "What a method returns is output by its type: a number as a number, a str as symbol and the string, and a list, a range or a numpy array as a list.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 205.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A list whose first element is a str outputs the message it names: return [\"note\", 60, 100] and the object outputs note 60 100. A dict, a set or an object outputs nothing, and the console says why.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 285.0, + 330.0, + 102.25 + ], + "linecount": 5, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "message", + "text": "1 2 3 4", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 90.0, + 77.19999999999999, + 24.0 + ], + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "message", + "text": "0.5 0.25 0.125", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 485.2, + 90.0, + 130.39999999999998, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 130.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "factor", + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 560.0, + 130.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "offset", + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python scale @factor 2 @offset 1", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 400.0, + 175.0, + 270.0, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 225.0, + 95.0, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 260.0, + 300, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "comment", + "text": "each value times factor, plus offset", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 400.0, + 295.0, + 300, + 26.849999999999998 + ], + "id": "obj-13" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "p outlets", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 220.0, + 20.0, + 81.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "One value per outlet", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A method hinted to return a tuple", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "note_name.py's method int(self, note: int) -> tuple[str, int] returns two values, so the object has two outlets for them, and outputs them right to left, as Max objects do: the octave, then the name.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 102.25 + ], + "linecount": 5, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The name is a str, which the object outputs as symbol and the string: sel matches it, and route needs route symbol first. Return it in a list, [\"C\"], to output the message C instead.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 205.0, + 330.0, + 102.25 + ], + "linecount": 5, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The object has as many outlets as the widest tuple return hint among the class's methods — a method returning one value fills the first — plus the dumpout at the right. A save that changes the number changes the outlets in place, keeping their patch cords.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 305.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "kslider", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "int", + "int" + ], + "patching_rect": [ + 400.0, + 90.0, + 168.0, + 53.0 + ], + "parameter_enable": 0, + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 615.0, + 105.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python note_name", + "numinlets": 1, + "numoutlets": 3, + "outlettype": [ + "", + "", + "" + ], + "patching_rect": [ + 400.0, + 175.0, + 158.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "message", + "text": "set $1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 225.0, + 69.6, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 260.0, + 120, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 540.0, + 225.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "sel C", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 620.0, + 225.0, + 53.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "button", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "bang" + ], + "patching_rect": [ + 620.0, + 260.0, + 24.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "comment", + "text": "name", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 400.0, + 290.0, + 120, + 26.849999999999998 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "comment", + "text": "octave", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 540.0, + 255.0, + 70, + 26.849999999999998 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "comment", + "text": "every C", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 650.0, + 260.0, + 80, + 26.849999999999998 + ], + "id": "obj-16" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 1 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "p messages", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 320.0, + 20.0, + 88.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "Messages and methods", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "The class's methods are the object's messages", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "With no argument the object loads python/default.py, which tap.python~ loads too. Its public methods are messages, called according to their signatures: bang returns the gain, float and int set it, greet prints to the Max console.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "process() is an ordinary method here: process 0.25 calls it, and outputs what it returns — a way to try a tap.python~ class sample by sample.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 205.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A method named anything answers every message the class has no method or attribute for, with the message's name first: def anything(self, selector: str, *args). Without one, the object doesn't understand it — try nonesuch.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 280.0, + 330.0, + 102.25 + ], + "linecount": 5, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Names Max and the object keep — dumpout, filechanged, assist and Max's own messages — are not exposed: the console says so, so that you can rename the method.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 380.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "message", + "text": "bang", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 90.0, + 54.4, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "message", + "text": "float 0.5", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 462.4, + 90.0, + 92.39999999999999, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "message", + "text": "int 2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 562.8, + 90.0, + 62.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "message", + "text": "greet max", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 632.8, + 90.0, + 92.39999999999999, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "message", + "text": "process 0.25", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 125.0, + 115.19999999999999, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "nonesuch 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 523.2, + 125.0, + 100.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 165.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 400.0, + 210.0, + 88.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 260.0, + 95.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 295.0, + 120, + 24.0 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "comment", + "text": "what the method returned", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 530.0, + 295.0, + 180, + 26.849999999999998 + ], + "id": "obj-17" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "p threads", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 420.0, + 20.0, + 81.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "Threads", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A message runs on the thread it arrives on", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "As with any Max object, a message runs on the thread it arrives on, and what the method returns is output before the message returns. From a click or a message box that is Max's main thread; from a metro with Overdrive on, the scheduler thread — where a long method holds the scheduler while it runs.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 139.95 + ], + "linecount": 7, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "With Scheduler in Audio Interrupt on, and audio running, the scheduler thread is the audio thread: Python then runs on it, where a long method, a reload or another object's Python can interrupt the audio. The object says so in the console, once. deferlow moves the message to the main thread.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 225.0, + 330.0, + 139.95 + ], + "linecount": 7, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Output is what a method returns: a thread the class starts itself has no outlet to reach.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 360.0, + 330.0, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "toggle", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "int" + ], + "patching_rect": [ + 400.0, + 90.0, + 24.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "metro 125", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 125.0, + 81.0, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python euclid", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 400.0, + 210.0, + 137.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "deferlow", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 550.0, + 165.0, + 74.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python euclid", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 550.0, + 210.0, + 137.0, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 260.0, + 95.0, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 295.0, + 140, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "prepend set", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 550.0, + 260.0, + 95.0, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "message", + "text": "", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 550.0, + 295.0, + 140, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "comment", + "text": "on the scheduler thread (Overdrive on)", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 400.0, + 335.0, + 140, + 64.55 + ], + "linecount": 3, + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "comment", + "text": "on the main thread", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 550.0, + 335.0, + 140, + 26.849999999999998 + ], + "id": "obj-16" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-7", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-14", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "p ?", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 520.0, + 20.0, + 40.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [], + "lines": [], + "showontab": 1 + }, + "id": "obj-6" + } + } + ], + "lines": [], + "showrootpatcherontab": 0, + "showontab": 0 + } +} diff --git a/help/tap.python~.maxhelp b/help/tap.python~.maxhelp index 0eef15f..1e67da0 100644 --- a/help/tap.python~.maxhelp +++ b/help/tap.python~.maxhelp @@ -1,522 +1,2461 @@ { - "patcher" : { - "fileversion" : 1, - "appversion" : { - "major" : 8, - "minor" : 3, - "revision" : 2, - "architecture" : "x64", - "modernui" : 1 - } -, - "classnamespace" : "box", - "rect" : [ 100.0, 100.0, 780.0, 700.0 ], - "bglocked" : 0, - "openinpresentation" : 0, - "default_fontsize" : 13.0, - "default_fontface" : 0, - "default_fontname" : "Ableton Sans Light Regular", - "gridonopen" : 1, - "gridsize" : [ 5.0, 5.0 ], - "gridsnaponopen" : 2, - "objectsnaponopen" : 0, - "statusbarvisible" : 2, - "toolbarvisible" : 1, - "lefttoolbarpinned" : 2, - "toptoolbarpinned" : 2, - "righttoolbarpinned" : 2, - "bottomtoolbarpinned" : 2, - "toolbars_unpinned_last_save" : 15, - "tallnewobj" : 0, - "boxanimatetime" : 200, - "enablehscroll" : 1, - "enablevscroll" : 1, - "devicewidth" : 0.0, - "description" : "", - "digest" : "", - "tags" : "", - "style" : "", - "subpatcher_template" : "tap.dark-template", - "assistshowspatchername" : 0, - "boxes" : [ { - "box" : { - "fontsize" : 18.0, - "hint" : "", - "id" : "obj-23", - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 405.0, 435.0, 67.0, 28.0 ], - "presentation" : 1, - "presentation_rect" : [ 172.698013305664062, 11.679183959960938, 70.0, 28.0 ], - "text" : "CPU", - "textcolor" : [ 0.968627, 0.968627, 0.968627, 1.0 ] + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 800.0, + 680.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "newobj", + "text": "p basic", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 20.0, + 67.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "tap.python~", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Process audio with a Python class", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "[tap.python~ name] loads python/name.py from this package and runs the class name in it as an audio object. With no argument it loads python/default.py: open it to follow along.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Its typed fields are attributes (gain), its public methods are messages called according to their signatures (greet, float, int, bang), and its process() method runs on the signal — here once per sample; the numpy tab has the form that runs once per vector.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 170.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Save the .py file and the object loads it again, keeping its attribute values, and the audio carries on with the new code. A mistake prints its traceback to the Max console, and the object outputs silence until a save fixes it.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 285.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "comment", + "text": "@mode worker runs process() on a thread of its own, @latency milliseconds (30) behind the audio, so that a reload or a message never holds the audio up.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 400.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 90.0, + 74.0, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 221", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 480.0, + 90.0, + 74.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.5", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 125.0, + 60.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "message", + "text": "greet max", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 160.0, + 92.39999999999999, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "message", + "text": "float 0.5", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 500.4, + 160.0, + 92.39999999999999, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "int 2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 600.8, + 160.0, + 62.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "message", + "text": "filechanged", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 670.8, + 160.0, + 107.6, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 195.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 235.0, + 95.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "comment", + "text": "filechanged reloads at once; saving the file does it for you", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 490.0, + 235.0, + 260, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "scope~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 490.0, + 280.0, + 150, + 70 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 280.0, + 60.0, + 24.0 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 400.0, + 315.0, + 45.0, + 45.0 + ], + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "metro 250 @active 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 660.0, + 280.0, + 151.0, + 24.0 + ], + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "adstatus cpu", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "int" + ], + "patching_rect": [ + 660.0, + 310.0, + 102.0, + 24.0 + ], + "id": "obj-21" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 660.0, + 340.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-22" + } + }, + { + "box": { + "maxclass": "comment", + "text": "CPU %", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 725.0, + 340.0, + 60, + 26.849999999999998 + ], + "id": "obj-23" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-18", + 0 + ], + "destination": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-18", + 0 + ], + "destination": [ + "obj-19", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-20", + 0 + ], + "destination": [ + "obj-21", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-21", + 0 + ], + "destination": [ + "obj-22", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-1" } - - } -, { - "box" : { - "id" : "obj-15", - "maxclass" : "newobj", - "numinlets" : 2, - "numoutlets" : 2, - "outlettype" : [ "", "int" ], - "patching_rect" : [ 385.0, 385.771667999999977, 81.0, 24.0 ], - "text" : "adstatus cpu" - } - - } -, { - "box" : { - "id" : "obj-8", - "maxclass" : "newobj", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "bang" ], - "patching_rect" : [ 385.0, 355.0, 127.0, 24.0 ], - "text" : "metro 250 @active 1" - } - - } -, { - "box" : { - "activebgcolor" : [ 0.909804, 0.909804, 0.909804, 0.0 ], - "bordercolor" : [ 0.196078, 0.196078, 0.196078, 0.0 ], - "focusbordercolor" : [ 0.0, 0.019608, 0.078431, 0.0 ], - "fontsize" : 18.0, - "id" : "obj-22", - "ignoreclick" : 1, - "maxclass" : "live.numbox", - "numinlets" : 1, - "numoutlets" : 2, - "outlettype" : [ "", "float" ], - "parameter_enable" : 1, - "patching_rect" : [ 383.715331999999989, 462.0, 96.0, 25.0 ], - "presentation" : 1, - "presentation_rect" : [ 222.698013305664062, 13.679183959960938, 61.0, 25.0 ], - "saved_attribute_attributes" : { - "activebgcolor" : { - "expression" : "" - } -, - "bordercolor" : { - "expression" : "" - } -, - "focusbordercolor" : { - "expression" : "" - } -, - "textcolor" : { - "expression" : "" - } -, - "valueof" : { - "parameter_initial" : [ 0.0 ], - "parameter_initial_enable" : 1, - "parameter_longname" : "live.numbox", - "parameter_mmax" : 100.0, - "parameter_shortname" : "live.numbox", - "parameter_type" : 0, - "parameter_units" : "%i %", - "parameter_unitstyle" : 9 - } - - } -, - "textcolor" : [ 0.513725, 0.529412, 0.545098, 1.0 ], - "varname" : "live.numbox" + }, + { + "box": { + "maxclass": "newobj", + "text": "p numpy", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 120.0, + 20.0, + 67.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "Per vector, with numpy", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "One call per signal vector instead of one per sample", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "process(self, x: float) -> float is called once per sample: simplest to write, but every line of Python in it runs tens of thousands of times a second.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "process(self, x: np.ndarray) -> np.ndarray is called once per signal vector, with the vector as a numpy array, and numpy then works on the whole of it at C speed: the form for anything with real work in it. numpy_gain.py is default.py's gain written that way.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 170.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Choose one with the message boxes and watch the CPU meter: the ReadMe's performance note has the measurements, per sample and per vector, at 48 and 96 kHz.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 285.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Turn on audio with the speaker.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 365.0, + 330.0, + 26.849999999999998 + ], + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 90.0, + 74.0, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ default", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 165.0, + 151.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ numpy_gain", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 570.0, + 165.0, + 172.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 130.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 570.0, + 130.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "gain", + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "message", + "text": "1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 210.0, + 40.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "message", + "text": "2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 448.0, + 210.0, + 40.0, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "comment", + "text": "per sample, per vector", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 500.0, + 210.0, + 170, + 26.849999999999998 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "selector~ 2 1", + "numinlets": 3, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 250.0, + 109.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 290.0, + 60.0, + 24.0 + ], + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 400.0, + 325.0, + 45.0, + 45.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "metro 250 @active 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 570.0, + 290.0, + 151.0, + 24.0 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "adstatus cpu", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "int" + ], + "patching_rect": [ + 570.0, + 320.0, + 102.0, + 24.0 + ], + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "number", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "bang" + ], + "patching_rect": [ + 570.0, + 350.0, + 60.0, + 24.0 + ], + "parameter_enable": 0, + "id": "obj-20" + } + }, + { + "box": { + "maxclass": "comment", + "text": "CPU %", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 635.0, + 350.0, + 60, + 26.849999999999998 + ], + "id": "obj-21" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-15", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-15", + 2 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-16", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-16", + 0 + ], + "destination": [ + "obj-17", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-18", + 0 + ], + "destination": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-19", + 0 + ], + "destination": [ + "obj-20", + 0 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-2" } - - } -, { - "box" : { - "id" : "obj-6", - "maxclass" : "newobj", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "signal" ], - "patching_rect" : [ 271.0, 90.0, 44.0, 24.0 ], - "text" : "*~ 0.5" + }, + { + "box": { + "maxclass": "newobj", + "text": "p allpass", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 220.0, + 20.0, + 81.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "A filter that knows the sample rate", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "prepare(sample_rate, vector_size)", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "allpass.py is a Schroeder allpass filter: it passes every frequency at the same level and shifts their phase, so mixed with the dry signal it notches the spectrum. Its delay is an attribute in milliseconds, which it turns into samples.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "For that it needs the sample rate: an optional method prepare(self, sample_rate: float, vector_size: int) -> None is called before the object processes any audio, again whenever the rate or the vector size changes, and on every reload before the new code runs.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 205.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Its fields are attrs fields with validators: alpha must stay strictly between -1 and 1. Try alpha 1 — the validator's error prints to the console, and alpha keeps its value.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 325.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "comment", + "text": "numpy_allpass.py computes exactly the same, a vector at a time.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 415.0, + 330.0, + 45.699999999999996 + ], + "linecount": 2, + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "noise~", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 90.0, + 60.0, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 125.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "delay", + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 560.0, + 125.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "alpha", + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "message", + "text": "alpha 1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 560.0, + 160.0, + 77.19999999999999, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "message", + "text": "clear", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 645.2, + 160.0, + 62.0, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ allpass", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 195.0, + 151.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "+~", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 235.0, + 40.0, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "comment", + "text": "with the dry signal", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 445.0, + 235.0, + 150, + 26.849999999999998 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 275.0, + 60.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "ezdac~", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 400.0, + 310.0, + 45.0, + 45.0 + ], + "id": "obj-16" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-13", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-13", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-16", + 1 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-3" } - - } -, { - "box" : { - "id" : "obj-5", - "maxclass" : "newobj", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "signal" ], - "patching_rect" : [ 340.0, 50.0, 64.0, 24.0 ], - "text" : "saw~ 221" + }, + { + "box": { + "maxclass": "newobj", + "text": "p mc", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 320.0, + 20.0, + 46.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [ + { + "box": { + "maxclass": "comment", + "text": "Many channels", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 15.0, + 600.0, + 45.699999999999996 + ], + "fontsize": 26.0, + "fontname": "Ableton Sans Bold Regular", + "id": "obj-1" + } + }, + { + "box": { + "maxclass": "comment", + "text": "mc., and a class with several inputs", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 55.0, + 600.0, + 29.75 + ], + "fontsize": 15.0, + "textcolor": [ + 0.976470588235294, + 0.733333333333333, + 0.258823529411765, + 1.0 + ], + "id": "obj-2" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Wrap the object in mc. to run one instance per channel of a multichannel signal: [mc.tap.python~ numpy_gain] here, over two channels. The class's attributes and messages are each instance's own, so the MC wrapper passes them on through its own messages: setvalue, an instance's number and the message, for one; applyvalues, the name and a value for each, for all.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 90.0, + 330.0, + 177.65 + ], + "linecount": 9, + "id": "obj-3" + } + }, + { + "box": { + "maxclass": "comment", + "text": "Or write one class with several channels: process()'s parameters are the object's signal inlets and its return hint its outlets. stereo_width.py takes left and right, returns tuple[np.ndarray, np.ndarray], and gets two of each.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 255.0, + 330.0, + 121.1 + ], + "linecount": 6, + "id": "obj-4" + } + }, + { + "box": { + "maxclass": "comment", + "text": "A save that changes process()'s inputs or outputs changes the object's inlets and outlets to match, keeping the patch cords of those that stay.", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 370.0, + 330.0, + 83.39999999999999 + ], + "linecount": 4, + "id": "obj-5" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 90.0, + 74.0, + 24.0 + ], + "id": "obj-6" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 165", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 480.0, + 90.0, + 74.0, + 24.0 + ], + "id": "obj-7" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "mc.pack~ 2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "multichannelsignal" + ], + "patching_rect": [ + 400.0, + 125.0, + 88.0, + 24.0 + ], + "id": "obj-8" + } + }, + { + "box": { + "maxclass": "message", + "text": "applyvalues gain 0.5 0.25", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 400.0, + 160.0, + 214.0, + 24.0 + ], + "id": "obj-9" + } + }, + { + "box": { + "maxclass": "message", + "text": "setvalue 2 gain 0.1", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 622.0, + 160.0, + 168.4, + 24.0 + ], + "id": "obj-10" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "mc.tap.python~ numpy_gain", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "multichannelsignal" + ], + "patching_rect": [ + 400.0, + 195.0, + 193.0, + 24.0 + ], + "id": "obj-11" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "mc.*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "multichannelsignal" + ], + "patching_rect": [ + 400.0, + 230.0, + 81.0, + 24.0 + ], + "id": "obj-12" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "mc.dac~ 1 2", + "numinlets": 1, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 400.0, + 265.0, + 95.0, + 24.0 + ], + "id": "obj-13" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 330.0, + 74.0, + 24.0 + ], + "id": "obj-14" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "saw~ 110.5", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 480.0, + 330.0, + 88.0, + 24.0 + ], + "id": "obj-15" + } + }, + { + "box": { + "maxclass": "attrui", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 570.0, + 330.0, + 150, + 24.0 + ], + "parameter_enable": 0, + "attr": "width", + "id": "obj-16" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "tap.python~ stereo_width", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "signal", + "signal" + ], + "patching_rect": [ + 400.0, + 370.0, + 186.0, + 24.0 + ], + "id": "obj-17" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 400.0, + 405.0, + 60.0, + 24.0 + ], + "id": "obj-18" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "*~ 0.2", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 480.0, + 405.0, + 60.0, + 24.0 + ], + "id": "obj-19" + } + }, + { + "box": { + "maxclass": "newobj", + "text": "dac~ 1 2", + "numinlets": 2, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 400.0, + 440.0, + 74.0, + 24.0 + ], + "id": "obj-20" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-8", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-10", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-17", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-16", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 0 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-17", + 1 + ], + "destination": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-18", + 0 + ], + "destination": [ + "obj-20", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-19", + 0 + ], + "destination": [ + "obj-20", + 1 + ] + } + } + ], + "showontab": 1 + }, + "id": "obj-4" } - - } -, { - "box" : { - "id" : "obj-4", - "maxclass" : "ezdac~", - "numinlets" : 2, - "numoutlets" : 0, - "patching_rect" : [ 270.0, 445.0, 45.0, 45.0 ] - } - - } -, { - "box" : { - "id" : "obj-3", - "lastchannelcount" : 0, - "maxclass" : "live.gain~", - "numinlets" : 2, - "numoutlets" : 5, - "outlettype" : [ "signal", "signal", "", "float", "list" ], - "parameter_enable" : 1, - "patching_rect" : [ 270.0, 275.0, 48.0, 136.0 ], - "saved_attribute_attributes" : { - "valueof" : { - "parameter_longname" : "live.gain~", - "parameter_mmax" : 6.0, - "parameter_mmin" : -70.0, - "parameter_shortname" : "live.gain~", - "parameter_type" : 0, - "parameter_unitstyle" : 4 - } - - } -, - "varname" : "live.gain~" + }, + { + "box": { + "maxclass": "newobj", + "text": "p ?", + "numinlets": 0, + "numoutlets": 0, + "patching_rect": [ + 420.0, + 20.0, + 40.0, + 24.0 + ], + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 1, + "revision": 5, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 100.0, + 100.0, + 900.0, + 700.0 + ], + "bglocked": 0, + "openinpresentation": 0, + "default_fontsize": 13.0, + "default_fontface": 0, + "default_fontname": "Ableton Sans Light Regular", + "gridonopen": 1, + "gridsize": [ + 5.0, + 5.0 + ], + "gridsnaponopen": 2, + "objectsnaponopen": 0, + "statusbarvisible": 2, + "toolbarvisible": 1, + "boxanimatetime": 200, + "enablehscroll": 1, + "enablevscroll": 1, + "description": "", + "digest": "", + "tags": "", + "style": "", + "assistshowspatchername": 0, + "textcolor": [ + 0.847058823529412, + 0.847058823529412, + 0.847058823529412, + 1.0 + ], + "bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "editing_bgcolor": [ + 0.109803921568627, + 0.109803921568627, + 0.109803921568627, + 1.0 + ], + "boxes": [], + "lines": [], + "showontab": 1 + }, + "id": "obj-5" } - - } -, { - "box" : { - "id" : "obj-2", - "maxclass" : "newobj", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "signal" ], - "patching_rect" : [ 270.0, 50.0, 64.0, 24.0 ], - "text" : "saw~ 110" - } - - } -, { - "box" : { - "id" : "obj-1", - "maxclass" : "newobj", - "numinlets" : 1, - "numoutlets" : 1, - "outlettype" : [ "signal" ], - "patching_rect" : [ 270.0, 210.0, 56.0, 24.0 ], - "text" : "tap.python~" - } - - } -, { - "box" : { - "attr" : "gain", - "id" : "obj-7", - "maxclass" : "attrui", - "numinlets" : 1, - "numoutlets" : 1, - "outlettype" : [ "" ], - "parameter_enable" : 0, - "patching_rect" : [ 340.0, 155.0, 175.0, 24.0 ] - } - - } -, { - "box" : { - "fontsize" : 24.0, - "id" : "obj-30", - "linecount" : 1, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 20.0, 10.0, 240.0, 33.0 ], - "text" : "tap.python~" - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-31", - "linecount" : 3, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 20.0, 50.0, 235.0, 62.0 ], - "text" : "Process audio with a Python class. [tap.python~ name] loads python/name.py from this package and runs the class name defined in it." - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-38", - "linecount" : 2, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 20.0, 115.0, 235.0, 47.0 ], - "text" : "With no argument it loads python/default.py: open it to follow along." - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-39", - "linecount" : 5, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 20.0, 165.0, 235.0, 108.0 ], - "text" : "Its annotated fields become attributes (gain, below), its public methods become messages (greet, bang, float, int), process() runs on the signal, and prepare() is told the sample rate and vector size." - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-40", - "linecount" : 3, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 20.0, 277.0, 235.0, 62.0 ], - "text" : "Save the .py file and the object reloads, keeping its attribute values. Errors print to the Max console." - } - - } -, { - "box" : { - "id" : "obj-32", - "maxclass" : "message", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "" ], - "patching_rect" : [ 20.0, 360.0, 70.0, 24.0 ], - "text" : "greet max" - } - - } -, { - "box" : { - "id" : "obj-33", - "maxclass" : "message", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "" ], - "patching_rect" : [ 100.0, 360.0, 60.0, 24.0 ], - "text" : "float 0.5" - } - - } -, { - "box" : { - "id" : "obj-34", - "maxclass" : "message", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "" ], - "patching_rect" : [ 170.0, 360.0, 45.0, 24.0 ], - "text" : "int 2" - } - - } -, { - "box" : { - "id" : "obj-35", - "maxclass" : "message", - "numinlets" : 2, - "numoutlets" : 1, - "outlettype" : [ "" ], - "patching_rect" : [ 20.0, 395.0, 80.0, 24.0 ], - "text" : "filechanged" - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-36", - "linecount" : 2, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 105.0, 395.0, 150.0, 33.0 ], - "text" : "forces a reload; saving the file does it for you" - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-41", - "linecount" : 4, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 270.0, 510.0, 300.0, 76.0 ], - "text" : "process()'s parameters are the object's signal inlets and its return hint its outlets: [tap.python~ stereo_width] has two of each. A save that changes them changes the object's inlets and outlets." - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-42", - "linecount" : 3, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 270.0, 595.0, 300.0, 62.0 ], - "text" : "@mode worker runs process() on a thread of its own, @latency milliseconds (30) behind, so a reload or a message never holds up the audio." - } - - } -, { - "box" : { - "fontsize" : 13.0, - "id" : "obj-37", - "linecount" : 9, - "maxclass" : "comment", - "numinlets" : 1, - "numoutlets" : 0, - "patching_rect" : [ 20.0, 450.0, 235.0, 180.0 ], - "text" : "More examples in the python folder: [tap.python~ numpy_gain] processes a whole signal vector per call with numpy, the form to use when it has to run in real time; [tap.python~ allpass] is an allpass filter that sizes its delay line in prepare(), and [tap.python~ numpy_allpass] the same filter per vector: swap one for the other and watch the CPU meter." - } - - } - ], - "lines" : [ { - "patchline" : { - "destination" : [ "obj-3", 1 ], - "order" : 0, - "source" : [ "obj-1", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-3", 0 ], - "order" : 1, - "source" : [ "obj-1", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-22", 0 ], - "source" : [ "obj-15", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-6", 0 ], - "source" : [ "obj-2", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-4", 1 ], - "source" : [ "obj-3", 1 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-4", 0 ], - "source" : [ "obj-3", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-6", 0 ], - "source" : [ "obj-5", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-1", 0 ], - "source" : [ "obj-6", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-1", 0 ], - "source" : [ "obj-7", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-15", 0 ], - "source" : [ "obj-8", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-1", 0 ], - "source" : [ "obj-32", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-1", 0 ], - "source" : [ "obj-33", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-1", 0 ], - "source" : [ "obj-34", 0 ] - } - - } -, { - "patchline" : { - "destination" : [ "obj-1", 0 ], - "source" : [ "obj-35", 0 ] - } - - } - ], - "parameters" : { - "obj-22" : [ "live.numbox", "live.numbox", 0 ], - "obj-3" : [ "live.gain~", "live.gain~", 0 ], - "parameterbanks" : { - "0" : { - "index" : 0, - "name" : "", - "parameters" : [ "-", "-", "-", "-", "-", "-", "-", "-" ] - } - - } -, - "inherited_shortname" : 1 - } -, - "dependency_cache" : [ { - "name" : "tap.python~.mxo", - "type" : "iLaX" } - ], - "autosave" : 0, - "textcolor" : [ 0.847058823529412, 0.847058823529412, 0.847058823529412, 1.0 ], - "bgcolor" : [ 0.109803921568627, 0.109803921568627, 0.109803921568627, 1.0 ], - "editing_bgcolor" : [ 0.109803921568627, 0.109803921568627, 0.109803921568627, 1.0 ] + ], + "lines": [], + "showrootpatcherontab": 0, + "showontab": 0 } - } diff --git a/init/tap.python.txt b/init/tap.python.txt index 17a2420..1550f4a 100644 --- a/init/tap.python.txt +++ b/init/tap.python.txt @@ -1 +1,2 @@ max objectfile tap.python tap.python~; +max objectfile mc.tap.python~ mc.wrapper~ tap.python~; diff --git a/package-info.json.in b/package-info.json.in index 4bdca1b..1ec4058 100644 --- a/package-info.json.in +++ b/package-info.json.in @@ -3,7 +3,7 @@ "version" : "@GIT_VERSION_MAJ@.@GIT_VERSION_MIN@.@GIT_VERSION_SUB@", "author" : "Timothy Place", "description" : "Write Max objects in Python: tap.python~ runs a Python class as an audio object, and tap.python as an object without audio that outputs what its methods return. Both make Max attributes and messages from the class's type hints, and reload it when it is saved.", - "homepatcher" : "", + "homepatcher" : "PythonTap Overview.maxpat", "max_version_min" : "9.0", "max_version_max" : "none", "os" : { diff --git a/runtime-tests/make_patchers.py b/runtime-tests/make_patchers.py index 22caa98..1b6874b 100644 --- a/runtime-tests/make_patchers.py +++ b/runtime-tests/make_patchers.py @@ -764,6 +764,37 @@ def prepare() -> Test: return t +def multichannel() -> Test: + t = Test("tap.python~.mc.maxtest.maxpat", + "[mc.tap.python~ default]: one instance per channel, through the package's init/ mapping " + "of mc.tap.python~ to Max's MC wrapper (plan 9.4: before it, No such object). The class's " + "attributes and messages are each instance's own, so the wrapper passes them on through " + "setvalue (one instance) and applyvalues (one value each), not as plain messages.") + one = t.signal(1.0) + pack = t.obj("mc.pack~ 2", inlets=2, outlets=1) + t.patcher.connect(one, 0, pack) + t.patcher.connect(one, 0, pack, 1) + wrapped = t.obj("mc.tap.python~ default", outlets=1) + t.patcher.connect(pack, 0, wrapped) + unpack = t.obj("mc.unpack~ 2", inlets=1, outlets=2, signal=True) + t.patcher.connect(wrapped, 0, unpack) + first = t.obj("+~ 0.", inlets=2, signal=True) + second = t.obj("+~ 0.", inlets=2, signal=True) + t.patcher.connect(unpack, 0, first) + t.patcher.connect(unpack, 1, second) + t.step(t.sample_equals("first-instance-runs", first, 1.0), t.sample_equals("second-instance-runs", second, 1.0)) + t.step(t.send("setvalue 2 gain 0.1", wrapped)) + t.step(t.sample_equals("setvalue-leaves-the-first", first, 1.0), + t.sample_equals("setvalue-sets-the-second-attribute", second, 0.1)) + t.step(t.send("applyvalues gain 0.5 0.25", wrapped)) + t.step(t.sample_equals("applyvalues-first", first, 0.5), t.sample_equals("applyvalues-second", second, 0.25)) + t.step(t.send("setvalue 1 float 0.75", wrapped)) + t.step(t.sample_equals("setvalue-calls-the-first-method", first, 0.75), + t.sample_equals("setvalue-leaves-the-second", second, 0.25), + t.errors_are("console-clean", "== 0")) + return t + + def soak(minutes: float) -> Test: """Plan 6.2, run by `run.py --session soak`: not a .maxtest, so the quick suite skips it.""" duration = int(minutes * 60_000) @@ -888,7 +919,7 @@ def without_runtime_restart() -> Test: TESTS_TO_WRITE = [load, attributes_and_messages, reload, reload_under_audio, many_instances, channels, announce_once, faults, - prepare, worker, + prepare, worker, multichannel, without_runtime, without_runtime_restart] diff --git a/runtime-tests/patchers/tap.python~.mc.maxtest.maxpat b/runtime-tests/patchers/tap.python~.mc.maxtest.maxpat new file mode 100644 index 0000000..4051689 --- /dev/null +++ b/runtime-tests/patchers/tap.python~.mc.maxtest.maxpat @@ -0,0 +1,2407 @@ +{ + "patcher": { + "fileversion": 1, + "appversion": { + "major": 9, + "minor": 0, + "revision": 8, + "architecture": "x64", + "modernui": 1 + }, + "classnamespace": "box", + "rect": [ + 60.0, + 80.0, + 1290, + 898.0 + ], + "default_fontsize": 12.0, + "default_fontname": "Arial", + "gridsize": [ + 15.0, + 15.0 + ], + "description": "tap.python~.mc", + "boxes": [ + { + "box": { + "id": "obj-1", + "maxclass": "comment", + "numinlets": 1, + "numoutlets": 0, + "patching_rect": [ + 20.0, + 20.0, + 480, + 80.0 + ], + "text": "tap.python~.mc\n\n[mc.tap.python~ default]: one instance per channel, through the package's init/ mapping of mc.tap.python~ to Max's MC wrapper (plan 9.4: before it, No such object). The class's attributes and messages are each instance's own, so the wrapper passes them on through setvalue (one instance) and applyvalues (one value each), not as plain messages.", + "linecount": 4 + } + }, + { + "box": { + "id": "obj-2", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 520.0, + 20.0, + 72.0, + 22.0 + ], + "text": "sig~ 1.0" + } + }, + { + "box": { + "id": "obj-3", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 520.0, + 50.0, + 86.0, + 22.0 + ], + "text": "mc.pack~ 2" + } + }, + { + "box": { + "id": "obj-4", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 520.0, + 80.0, + 170.0, + 22.0 + ], + "text": "mc.tap.python~ default" + } + }, + { + "box": { + "id": "obj-5", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "signal", + "signal" + ], + "patching_rect": [ + 520.0, + 110.0, + 100.0, + 22.0 + ], + "text": "mc.unpack~ 2" + } + }, + { + "box": { + "id": "obj-6", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 520.0, + 140.0, + 51.0, + 22.0 + ], + "text": "+~ 0." + } + }, + { + "box": { + "id": "obj-7", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "signal" + ], + "patching_rect": [ + 520.0, + 170.0, + 51.0, + 22.0 + ], + "text": "+~ 0." + } + }, + { + "box": { + "id": "obj-8", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 20.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-9", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 50.0, + 121.0, + 22.0 + ], + "text": "test.equals 1.0" + } + }, + { + "box": { + "id": "obj-10", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 80.0, + 233.0, + 22.0 + ], + "text": "test.assert first-instance-runs" + } + }, + { + "box": { + "id": "obj-11", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 110.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-12", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 140.0, + 121.0, + 22.0 + ], + "text": "test.equals 1.0" + } + }, + { + "box": { + "id": "obj-13", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 170.0, + 240.0, + 22.0 + ], + "text": "test.assert second-instance-runs" + } + }, + { + "box": { + "id": "obj-14", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 270.0, + 20.0, + 149.0, + 22.0 + ], + "text": "setvalue 2 gain 0.1" + } + }, + { + "box": { + "id": "obj-15", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 200.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-16", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 230.0, + 121.0, + 22.0 + ], + "text": "test.equals 1.0" + } + }, + { + "box": { + "id": "obj-17", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 260.0, + 275.0, + 22.0 + ], + "text": "test.assert setvalue-leaves-the-first" + } + }, + { + "box": { + "id": "obj-18", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 290.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-19", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 320.0, + 121.0, + 22.0 + ], + "text": "test.equals 0.1" + } + }, + { + "box": { + "id": "obj-20", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 350.0, + 338.0, + 22.0 + ], + "text": "test.assert setvalue-sets-the-second-attribute" + } + }, + { + "box": { + "id": "obj-21", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 270.0, + 50.0, + 191.0, + 22.0 + ], + "text": "applyvalues gain 0.5 0.25" + } + }, + { + "box": { + "id": "obj-22", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 380.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-23", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 410.0, + 121.0, + 22.0 + ], + "text": "test.equals 0.5" + } + }, + { + "box": { + "id": "obj-24", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 440.0, + 219.0, + 22.0 + ], + "text": "test.assert applyvalues-first" + } + }, + { + "box": { + "id": "obj-25", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 470.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-26", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 500.0, + 128.0, + 22.0 + ], + "text": "test.equals 0.25" + } + }, + { + "box": { + "id": "obj-27", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 530.0, + 226.0, + 22.0 + ], + "text": "test.assert applyvalues-second" + } + }, + { + "box": { + "id": "obj-28", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 270.0, + 80.0, + 163.0, + 22.0 + ], + "text": "setvalue 1 float 0.75" + } + }, + { + "box": { + "id": "obj-29", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 560.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-30", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 590.0, + 128.0, + 22.0 + ], + "text": "test.equals 0.75" + } + }, + { + "box": { + "id": "obj-31", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 620.0, + 317.0, + 22.0 + ], + "text": "test.assert setvalue-calls-the-first-method" + } + }, + { + "box": { + "id": "obj-32", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 650.0, + 177.0, + 22.0 + ], + "text": "test.sample~ @autorun 0" + } + }, + { + "box": { + "id": "obj-33", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 680.0, + 128.0, + 22.0 + ], + "text": "test.equals 0.25" + } + }, + { + "box": { + "id": "obj-34", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 710.0, + 282.0, + 22.0 + ], + "text": "test.assert setvalue-leaves-the-second" + } + }, + { + "box": { + "id": "obj-35", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 20.0, + 65.0, + 22.0 + ], + "text": "error 1" + } + }, + { + "box": { + "id": "obj-36", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 50.0, + 40.0, + 22.0 + ], + "text": "t b" + } + }, + { + "box": { + "id": "obj-37", + "maxclass": "newobj", + "numinlets": 3, + "numoutlets": 4, + "outlettype": [ + "", + "", + "", + "" + ], + "patching_rect": [ + 1020.0, + 80.0, + 135.0, + 22.0 + ], + "text": "counter 1 1000000" + } + }, + { + "box": { + "id": "obj-38", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 110.0, + 40.0, + 22.0 + ], + "text": "i" + } + }, + { + "box": { + "id": "obj-39", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 140.0, + 44.0, + 22.0 + ], + "text": "== 0" + } + }, + { + "box": { + "id": "obj-40", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 740.0, + 191.0, + 22.0 + ], + "text": "test.assert console-clean" + } + }, + { + "box": { + "id": "obj-41", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 20.0, + 108.0, + 114.0, + 22.0 + ], + "text": "test.terminate" + } + }, + { + "box": { + "id": "obj-42", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 170.0, + 72.0, + 22.0 + ], + "text": "tosymbol" + } + }, + { + "box": { + "id": "obj-43", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 5, + "outlettype": [ + "", + "", + "", + "", + "" + ], + "patching_rect": [ + 1020.0, + 200.0, + 177.0, + 22.0 + ], + "text": "regexp \\\" @substitute '" + } + }, + { + "box": { + "id": "obj-44", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 230.0, + 86.0, + 22.0 + ], + "text": "fromsymbol" + } + }, + { + "box": { + "id": "obj-45", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 1020.0, + 260.0, + 51.0, + 22.0 + ], + "text": "t l b" + } + }, + { + "box": { + "id": "obj-46", + "maxclass": "newobj", + "numinlets": 3, + "numoutlets": 4, + "outlettype": [ + "", + "", + "", + "" + ], + "patching_rect": [ + 1020.0, + 290.0, + 135.0, + 22.0 + ], + "text": "counter 1 1000000" + } + }, + { + "box": { + "id": "obj-47", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 320.0, + 51.0, + 22.0 + ], + "text": "<= 50" + } + }, + { + "box": { + "id": "obj-48", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 1020.0, + 350.0, + 72.0, + 22.0 + ], + "text": "gate 1 1" + } + }, + { + "box": { + "id": "obj-49", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 0, + "outlettype": [], + "patching_rect": [ + 1020.0, + 380.0, + 170.0, + 22.0 + ], + "text": "test.log console-error" + } + }, + { + "box": { + "id": "obj-50", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 138.0, + 72.0, + 22.0 + ], + "text": "loadbang" + } + }, + { + "box": { + "id": "obj-51", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 168.0, + 51.0, + 22.0 + ], + "text": "t b b" + } + }, + { + "box": { + "id": "obj-52", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 198.0, + 93.0, + 22.0 + ], + "text": "; dsp start" + } + }, + { + "box": { + "id": "obj-53", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 228.0, + 93.0, + 22.0 + ], + "text": "delay 60000" + } + }, + { + "box": { + "id": "obj-54", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 4, + "outlettype": [ + "", + "", + "", + "" + ], + "patching_rect": [ + 20.0, + 258.0, + 79.0, + 22.0 + ], + "text": "dspstate~" + } + }, + { + "box": { + "id": "obj-55", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 288.0, + 51.0, + 22.0 + ], + "text": "sel 1" + } + }, + { + "box": { + "id": "obj-56", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 318.0, + 79.0, + 22.0 + ], + "text": "onebang 1" + } + }, + { + "box": { + "id": "obj-57", + "maxclass": "message", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 348.0, + 40.0, + 22.0 + ], + "text": "1" + } + }, + { + "box": { + "id": "obj-58", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 770.0, + 770.0, + 191.0, + 22.0 + ], + "text": "test.assert audio-started" + } + }, + { + "box": { + "id": "obj-59", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 378.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-60", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 3, + "outlettype": [ + "", + "", + "" + ], + "patching_rect": [ + 20.0, + 408.0, + 65.0, + 22.0 + ], + "text": "t b b b" + } + }, + { + "box": { + "id": "obj-61", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 438.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-62", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 468.0, + 51.0, + 22.0 + ], + "text": "t b b" + } + }, + { + "box": { + "id": "obj-63", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 498.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-64", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 3, + "outlettype": [ + "", + "", + "" + ], + "patching_rect": [ + 20.0, + 528.0, + 65.0, + 22.0 + ], + "text": "t b b b" + } + }, + { + "box": { + "id": "obj-65", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 558.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-66", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 588.0, + 51.0, + 22.0 + ], + "text": "t b b" + } + }, + { + "box": { + "id": "obj-67", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 618.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-68", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 3, + "outlettype": [ + "", + "", + "" + ], + "patching_rect": [ + 20.0, + 648.0, + 65.0, + 22.0 + ], + "text": "t b b b" + } + }, + { + "box": { + "id": "obj-69", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 678.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-70", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 708.0, + 51.0, + 22.0 + ], + "text": "t b b" + } + }, + { + "box": { + "id": "obj-71", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 738.0, + 79.0, + 22.0 + ], + "text": "delay 150" + } + }, + { + "box": { + "id": "obj-72", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 4, + "outlettype": [ + "", + "", + "", + "" + ], + "patching_rect": [ + 20.0, + 768.0, + 79.0, + 22.0 + ], + "text": "t b b b b" + } + }, + { + "box": { + "id": "obj-73", + "maxclass": "newobj", + "numinlets": 2, + "numoutlets": 1, + "outlettype": [ + "" + ], + "patching_rect": [ + 20.0, + 798.0, + 79.0, + 22.0 + ], + "text": "delay 500" + } + }, + { + "box": { + "id": "obj-74", + "maxclass": "newobj", + "numinlets": 1, + "numoutlets": 2, + "outlettype": [ + "", + "" + ], + "patching_rect": [ + 20.0, + 828.0, + 51.0, + 22.0 + ], + "text": "t b b" + } + } + ], + "lines": [ + { + "patchline": { + "source": [ + "obj-2", + 0 + ], + "destination": [ + "obj-3", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-2", + 0 + ], + "destination": [ + "obj-3", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-3", + 0 + ], + "destination": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-4", + 0 + ], + "destination": [ + "obj-5", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-5", + 0 + ], + "destination": [ + "obj-6", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-5", + 1 + ], + "destination": [ + "obj-7", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-8", + 0 + ], + "destination": [ + "obj-9", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-9", + 0 + ], + "destination": [ + "obj-10", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-11", + 0 + ], + "destination": [ + "obj-12", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-12", + 0 + ], + "destination": [ + "obj-13", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-14", + 0 + ], + "destination": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-15", + 0 + ], + "destination": [ + "obj-16", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-16", + 0 + ], + "destination": [ + "obj-17", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-18", + 0 + ], + "destination": [ + "obj-19", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-19", + 0 + ], + "destination": [ + "obj-20", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-21", + 0 + ], + "destination": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-22", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-22", + 0 + ], + "destination": [ + "obj-23", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-23", + 0 + ], + "destination": [ + "obj-24", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-25", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-25", + 0 + ], + "destination": [ + "obj-26", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-26", + 0 + ], + "destination": [ + "obj-27", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-28", + 0 + ], + "destination": [ + "obj-4", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-6", + 0 + ], + "destination": [ + "obj-29", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-29", + 0 + ], + "destination": [ + "obj-30", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-30", + 0 + ], + "destination": [ + "obj-31", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-7", + 0 + ], + "destination": [ + "obj-32", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-32", + 0 + ], + "destination": [ + "obj-33", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-33", + 0 + ], + "destination": [ + "obj-34", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-35", + 0 + ], + "destination": [ + "obj-36", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-36", + 0 + ], + "destination": [ + "obj-37", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-37", + 0 + ], + "destination": [ + "obj-38", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-38", + 0 + ], + "destination": [ + "obj-39", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-39", + 0 + ], + "destination": [ + "obj-40", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-35", + 0 + ], + "destination": [ + "obj-42", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-42", + 0 + ], + "destination": [ + "obj-43", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-43", + 0 + ], + "destination": [ + "obj-44", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-43", + 3 + ], + "destination": [ + "obj-44", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-44", + 0 + ], + "destination": [ + "obj-45", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-45", + 1 + ], + "destination": [ + "obj-46", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-46", + 0 + ], + "destination": [ + "obj-47", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-47", + 0 + ], + "destination": [ + "obj-48", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-45", + 0 + ], + "destination": [ + "obj-48", + 1 + ] + } + }, + { + "patchline": { + "source": [ + "obj-48", + 0 + ], + "destination": [ + "obj-49", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-50", + 0 + ], + "destination": [ + "obj-51", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-51", + 1 + ], + "destination": [ + "obj-53", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-51", + 0 + ], + "destination": [ + "obj-52", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-53", + 0 + ], + "destination": [ + "obj-41", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-54", + 0 + ], + "destination": [ + "obj-55", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-55", + 0 + ], + "destination": [ + "obj-56", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-56", + 0 + ], + "destination": [ + "obj-57", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-57", + 0 + ], + "destination": [ + "obj-58", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-56", + 0 + ], + "destination": [ + "obj-59", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-59", + 0 + ], + "destination": [ + "obj-60", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-60", + 2 + ], + "destination": [ + "obj-8", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-60", + 1 + ], + "destination": [ + "obj-11", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-60", + 0 + ], + "destination": [ + "obj-61", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-61", + 0 + ], + "destination": [ + "obj-62", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-62", + 1 + ], + "destination": [ + "obj-14", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-62", + 0 + ], + "destination": [ + "obj-63", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-63", + 0 + ], + "destination": [ + "obj-64", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-64", + 2 + ], + "destination": [ + "obj-15", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-64", + 1 + ], + "destination": [ + "obj-18", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-64", + 0 + ], + "destination": [ + "obj-65", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-65", + 0 + ], + "destination": [ + "obj-66", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-66", + 1 + ], + "destination": [ + "obj-21", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-66", + 0 + ], + "destination": [ + "obj-67", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-67", + 0 + ], + "destination": [ + "obj-68", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-68", + 2 + ], + "destination": [ + "obj-22", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-68", + 1 + ], + "destination": [ + "obj-25", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-68", + 0 + ], + "destination": [ + "obj-69", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-69", + 0 + ], + "destination": [ + "obj-70", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-70", + 1 + ], + "destination": [ + "obj-28", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-70", + 0 + ], + "destination": [ + "obj-71", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-71", + 0 + ], + "destination": [ + "obj-72", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-72", + 3 + ], + "destination": [ + "obj-29", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-72", + 2 + ], + "destination": [ + "obj-32", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-72", + 1 + ], + "destination": [ + "obj-38", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-72", + 0 + ], + "destination": [ + "obj-73", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-73", + 0 + ], + "destination": [ + "obj-74", + 0 + ] + } + }, + { + "patchline": { + "source": [ + "obj-74", + 1 + ], + "destination": [ + "obj-41", + 0 + ] + } + } + ] + } +} diff --git a/scripts/assemble-package.py b/scripts/assemble-package.py index 259e738..8756a37 100644 --- a/scripts/assemble-package.py +++ b/scripts/assemble-package.py @@ -53,15 +53,20 @@ # Package content copied as-is (relative to the package root). DOCUMENTS = ["ReadMe.md", "License.md", "CHANGELOG.md", "icon.png", "package-info.json"] # init/: text files Max reads at launch — tap.python.txt maps the object tap.python to the file -# tap.python~, which registers both classes (docs/TAP-PYTHON-PLAN.md, D11; plan 9.0) -FOLDERS = ["help", "docs", "python", "init"] +# tap.python~, which registers both classes (docs/TAP-PYTHON-PLAN.md, D11; plan 9.0), and +# mc.tap.python~ to Max's MC wrapper around it. extras/: the PythonTap Overview, in the Extras menu +# and the package's home patcher (plan 9.4). +FOLDERS = ["help", "docs", "python", "init", "extras"] # What a package must hold, checked once it is assembled or merged, so that a release cannot lack -# it: above all init/tap.python.txt, the mapping without which Max does not find tap.python in -# tap.python~'s binary ("tap.python: No such object", plan 9.0) — with the line it must say. +# it: above all init/tap.python.txt, with the mappings without which Max finds neither tap.python in +# tap.python~'s binary ("tap.python: No such object", plan 9.0) nor mc.tap.python~ (plan 9.4) — +# with the lines each must say. REQUIRED = { - "init/tap.python.txt": "max objectfile tap.python tap.python~;", - "python/default.py": None, + "init/tap.python.txt": ["max objectfile tap.python tap.python~;", + "max objectfile mc.tap.python~ mc.wrapper~ tap.python~;"], + "python/default.py": [], + "extras/PythonTap Overview.maxpat": [], } # Never shipped from the copied folders. @@ -248,12 +253,14 @@ def assemble(platform: str, output: Path, expected_version: str | None) -> Path: def check_complete(package: Path) -> None: """Fail unless `package` holds everything REQUIRED, each saying what it must.""" - for name, line in REQUIRED.items(): + for name, lines in REQUIRED.items(): path = package / name if not path.is_file(): raise AssemblyError(f"the package has no {name}") - if line is not None and line not in path.read_text(encoding="utf-8").splitlines(): - raise AssemblyError(f"the package's {name} does not say {line!r}") + said = path.read_text(encoding="utf-8").splitlines() + for line in lines: + if line not in said: + raise AssemblyError(f"the package's {name} does not say {line!r}") def same_content(a: Path, b: Path) -> bool: diff --git a/scripts/check-docs.py b/scripts/check-docs.py new file mode 100644 index 0000000..2e763aa --- /dev/null +++ b/scripts/check-docs.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: MIT +# Copyright 2022-2026 Timothy Place. +"""Check the documentation written by hand (plan 9.4): what no build makes, and no test runs. + + python3 scripts/check-docs.py + +- Every page in docs/ — the reference pages, the guide and topic (vignettes), the tutorials — is + well-formed XML with the root element its kind needs; a reference page names its object, and a + tutorial's patcher is beside it. +- Every link between them resolves: a guide's or tutorial's link to a tutorial or a vignette names + one that exists, an openfilelink names a file in the package, and a tutorial's previous and next + exist. +- Every patcher — the help files, the tutorials' patchers, the Overview — is JSON in which, in each + patcher and subpatcher, box ids are unique and every patch cord joins two boxes that exist, from + an outlet the source has to an inlet the destination has. + +Standard library only. Exits non-zero, naming each problem, if anything is wrong. +""" + +from __future__ import annotations + +import json +import sys +import xml.etree.ElementTree as ElementTree +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +DOCS = ROOT / "docs" + +problems: list[str] = [] + + +def problem(path: Path, what: str) -> None: + problems.append(f"{path.relative_to(ROOT)}: {what}") + + +def parse(path: Path) -> ElementTree.Element | None: + try: + return ElementTree.parse(path).getroot() + except ElementTree.ParseError as error: + problem(path, f"not well-formed XML ({error})") + return None + + +def stem(path: Path, suffix: str) -> str: + return path.name[: -len(suffix)] + + +def check_pages() -> None: + tutorials = {stem(p, ".maxtut.xml"): p for p in DOCS.rglob("*.maxtut.xml")} + vignettes = {stem(p, ".maxvig.xml"): p for p in DOCS.rglob("*.maxvig.xml")} + files = {p.name for folder in ("help", "extras", "docs") for p in (ROOT / folder).rglob("*") if p.is_file()} + + for path in sorted(DOCS.glob("*.maxref.xml")): + root = parse(path) + if root is None: + continue + name = stem(path, ".maxref.xml") + if root.tag != "c74object" or root.get("name") != name: + problem(path, f"the root must be ") + if root.find("digest") is None or not "".join(root.find("digest").itertext()).strip(): + problem(path, "has no digest") + + pages = list(vignettes.values()) + list(tutorials.values()) + for path in sorted(pages): + root = parse(path) + if root is None: + continue + tutorial = path.name.endswith(".maxtut.xml") + if root.tag != ("chapter" if tutorial else "vignette"): + problem(path, f"the root must be <{'chapter' if tutorial else 'vignette'}>") + if tutorial: + openfile = root.find("openfile") + if openfile is None or not (path.parent / openfile.get("patch", "")).is_file(): + problem(path, "its names no patcher beside it") + for element in ("previous", "next", "parent"): + for link in root.iter(element): + if link.get("name") not in tutorials: + problem(path, f"<{element} name=\"{link.get('name')}\"> names no tutorial") + for link in root.iter("link"): + kind, name = link.get("type"), link.get("name") + if kind == "tutorial" and name not in tutorials: + problem(path, f"links to the tutorial {name}, which does not exist") + if kind in ("vignette", "topic") and name not in vignettes: + problem(path, f"links to the vignette {name}, which does not exist") + for link in root.iter("openfilelink"): + if link.get("filename") not in files: + problem(path, f" names no file in the package") + + +def check_patcher(path: Path, patcher: dict, where: str) -> None: + boxes: dict[str, dict] = {} + for entry in patcher.get("boxes", []): + box = entry.get("box", {}) + box_id = box.get("id") + if box_id in boxes: + problem(path, f"{where}: the box id {box_id} is used twice") + boxes[box_id] = box + if "patcher" in box: + check_patcher(path, box["patcher"], f"{where} > [{box.get('text', box_id)}]") + for entry in patcher.get("lines", []): + line = entry.get("patchline", {}) + (source, outlet), (destination, inlet) = line.get("source", [None, 0]), line.get("destination", [None, 0]) + if source not in boxes or destination not in boxes: + problem(path, f"{where}: a patch cord joins {source} and {destination}, which are not both boxes") + continue + if outlet >= boxes[source].get("numoutlets", 0): + problem(path, f"{where}: a patch cord leaves outlet {outlet} of {source}, which has " + f"{boxes[source].get('numoutlets', 0)}") + if inlet >= boxes[destination].get("numinlets", 0): + problem(path, f"{where}: a patch cord reaches inlet {inlet} of {destination}, which has " + f"{boxes[destination].get('numinlets', 0)}") + + +def check_patchers() -> None: + paths = sorted(list((ROOT / "help").glob("*.maxhelp")) + list(DOCS.rglob("*.maxpat")) + + list((ROOT / "extras").glob("*.maxpat"))) + for path in paths: + try: + document = json.loads(path.read_text(encoding="utf-8")) + except (UnicodeDecodeError, json.JSONDecodeError) as error: + problem(path, f"not JSON ({error})") + continue + if "patcher" not in document: + problem(path, "has no patcher") + continue + check_patcher(path, document["patcher"], "the patcher") + + +def main() -> int: + check_pages() + check_patchers() + for line in problems: + print(line, file=sys.stderr) + if problems: + print(f"{len(problems)} problem(s) in the hand-made documentation", file=sys.stderr) + return 1 + print("the hand-made documentation is well-formed, and its links and patch cords hold") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/source/projects/tap.python_tilde/tap.python_tilde.h b/source/projects/tap.python_tilde/tap.python_tilde.h index 43f8343..1e7fcfb 100644 --- a/source/projects/tap.python_tilde/tap.python_tilde.h +++ b/source/projects/tap.python_tilde/tap.python_tilde.h @@ -55,7 +55,7 @@ class python : public object, public vector_operator<> { "printed to the Max console and never take Max down."}; MIN_TAGS{"programming"}; MIN_AUTHOR{"Tim Place"}; - MIN_RELATED{"js, node.script"}; + MIN_RELATED{"tap.python, js, node.script"}; // The first inlet and outlet; the constructor adds as many more as the class's process() // declares, and a reload changes them to match (plan 2.4). From e85477f1243620645a426a348abcf6133eeda939 Mon Sep 17 00:00:00 2001 From: Timothy Place Date: Fri, 9 Oct 2026 20:08:02 -0500 Subject: [PATCH 2/2] The plan: 9.4 done (#49), and what it found Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_019rVV9SkjkWmiBXFMT4whkz --- docs/PRODUCTION-PLAN.md | 2 +- docs/TAP-PYTHON-PLAN.md | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/PRODUCTION-PLAN.md b/docs/PRODUCTION-PLAN.md index 1a7dcd7..4224c8a 100644 --- a/docs/PRODUCTION-PLAN.md +++ b/docs/PRODUCTION-PLAN.md @@ -823,7 +823,7 @@ row shipped what only a host platform could show. notice; `init/tap.python.txt`; the glue test with faithful stubs; the examples (`euclid.py`, `scale.py`, `note_name.py`, `default.py`'s `bang`); packaging; `linux-max-glue` gains sanitizer rows for the lock and the reload race; CHANGELOG 2.0.0 started. -- [ ] **9.4 Documentation, in Max's own system and the ReadMe:** the ReadMe section, output +- [x] **9.4 Documentation, in Max's own system and the ReadMe** (#49)**:** the ReadMe section, output table and limits, CLAUDE.md; `docs/tap.python.maxref.xml` by hand (a plain SDK class has no min generator); a vignette and three tutorials with patchers for the Documentation window; an Extras overview named as the landing patcher; help patchers with tabs for both objects; CI diff --git a/docs/TAP-PYTHON-PLAN.md b/docs/TAP-PYTHON-PLAN.md index dec2b6f..df9c32b 100644 --- a/docs/TAP-PYTHON-PLAN.md +++ b/docs/TAP-PYTHON-PLAN.md @@ -8,7 +8,7 @@ change — and the plan to build it (Phase 9 of the production plan, which point Drafted 2026-10-02 against 1.0.0 and audited the same day (`AUDIT-TAP-PYTHON-PLAN.md`: two blockers, six major findings, nine minor; its addendum of 2026-10-09 records what 1.0.1 and 1.0.2 changed). **Revised 2026-10-09 against 1.0.2** for every finding; the [revision record](#revision-record) -at the end says what each changed. 9.0 to 9.3 are built — the spike ran in Max on a Mac +at the end says what each changed. 9.0 to 9.4 are built — the spike ran in Max on a Mac and on Windows: its answers are under [9.0](#what-90-found), and the design below follows them. Tick the items below (with the PR) as they land, and keep the design current where a PR decides differently. @@ -420,7 +420,7 @@ third was Windows-only. and Max's `doesn't understand "nonesuch"` from the forwarder. The Windows glue test found the one fault: its console checks redirected `std::cout`, which a DLL's kernel does not share; the test now stubs `object_post`/`object_warn`/`object_error` to hear them.)* -- [ ] **9.4 Documentation, in Max's own system and the ReadMe.** The ReadMe section, the output +- [x] **9.4 Documentation, in Max's own system and the ReadMe** (#49). The ReadMe section, the output table and the limits; CLAUDE.md. For the Documentation window: `docs/tap.python.maxref.xml` by hand, with see-also links between the two pages; a vignette, *Writing Max objects in Python* — the ReadMe's "Writing a class" and the threads and performance sections, rewritten for the @@ -436,8 +436,22 @@ third was Windows-only. Packages page says `patchers/`). CI checks for what is written by hand: the reference page, the vignette and the tutorials well-formed XML, and the help patchers' JSON as 5.2 checked by hand (ids unique, every line connects), in one script. All - checked open in Max in 9.6. -- [ ] **9.5 Runtime tests.** The patchers above in `make_patchers.py`; `run.py` aware of the + checked open in Max in 9.6. *(As built: the Documentation window's Package Docs › PythonTap lists + `docs/topics/` under Topics, `docs/vignettes/` under Guides and `docs/tutorials/` under Tutorials, + so the package has a short topic beside the guide; a tutorial's Open Tutorial finds its patcher + beside it. Cycling '74's packages keep the home patcher in `extras/`, and Max lists it in the + Extras menu, so `homepatcher` names it there. `tap.python` is listed as a reference page, not an + object — it has no file of its own — exactly as Max lists its own `init`-mapped objects + (`mc.abs~`), so no stub external for it. The help patchers were laid out by a script and checked + open in Max, every tab; `tap.python~`'s old single page, which overlapped its own objects at this + font, is its basic tab rebuilt. The mc. tab found `[mc.tap.python~]` "No such object": Max wraps + an external in mc. through an `init/` mapping, which the package lacked — now in + `init/tap.python.txt`, required by `assemble-package.py`, and pinned by a runtime test (one of + 9.5's), which also shows that the wrapper passes a class's attributes and messages on only through + `setvalue` and `applyvalues`. The ReadMe's message sentence is the perf script's, written when + the tables are next measured (9.6). Checked in Max 9.1.5 on the Mac; Windows in 9.6.)* +- [ ] **9.5 Runtime tests.** The patchers above in `make_patchers.py` (9.4 added `tap.python~.mc`, + for the `init/` mapping of `mc.tap.python~`); `run.py` aware of the second object (the `init/` mapping in the installed package, the `--package` mode, the reference-page rule for `tap.python~`'s page only). What 9.0's runner learned: a fresh Max needs Restore Windows on Launch off (a restored window loads the binary first); patchers are