Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 12 additions & 119 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,119 +1,12 @@
# AGENTS.md — audiocomponents

`audioinstruments` (55 instruments) and `audioeffects` (45 effect classes,
racks included): the pure-Python audio component tier that PyDevices owns,
built on [audiodsp](https://github.com/PyDevices/audiodsp)'s nodes. **This
repository publishes both** — `pydevices-audioinstruments` and
`pydevices-audioeffects` on TestPyPI, and the `audioinstruments` and
`audioeffects` entries in the MIP index. audiodsp publishes the core only.

## Read this before you change anything

This is the only home of both packages. audiodsp's pre-rewrite copy of
`lib/audioinstruments/` and `lib/audioeffects/` was deleted on 2026-09-03
([#2](https://github.com/PyDevices/audiocomponents/issues/2)), so every bug
fix and every piece of accuracy work belongs here.

## The floor is pinned

`AUDIODSP_PIN` names the exact audiodsp release every gate here runs against.
It is the same discipline as audiodsp's own `CIRCUITPYTHON_ORACLE`, for the
same reason: with a floating core underneath, a component failure is
unattributable — you cannot tell a rewritten instrument from a moved node
beneath it. Moving the pin is its own change, with the gates re-run, never a
side effect of other work.

## Layout

- `lib/<package>/` — the packages, each with its own `pyproject.toml`. That
layout is load-bearing twice over: it is what makes each a standalone
distribution, and the MIP publisher expects `<repo>/lib/<package>`.
`pyproject.toml`'s `project.urls` point here, and its `dependencies` carry
the audiodsp floor, `pydevices-audiodsp>=<release>` — the newest audiodsp
*release*. That is not the pin: `AUDIODSP_PIN` may name a commit ahead of
the floor, and the gates use the pin.
- `docs/reference/component-api.md` — the runtime contract (construction,
methods, properties). `docs/reference/metadata.md` — the static metadata
manifest. `tools/validate_api.py` and `tools/validate_metadata.py` enforce
them, and the tests import the latter.
- `tests/parity/` — the instrument parity harness, its probes and goldens.

## Gates

What CI runs, and what you should run before committing:

```bash
python -m unittest discover -s tests -p "test_*.py"
python tools/validate_api.py
python tests/parity/effects_library_smoke.py
python -m flake8
```

`.flake8` selects defect checks only (`F,E9,W6`) — no layout rules. The
instrument modules are deliberately compact and generated modules put
`MACRO_LABELS` above their imports; gating layout here would be a fight with
the house style, not a check.

The heavier gates are workspace-local:

```bash
python3 tests/parity/run_instruments_parity.py --verify --batch all
MICROPYPATH=$PWD:$PWD/lib ../bin/micropython \
tests/parity/scheduling_seam_live.py
```

The second is the scheduling seam against the **real** pump
(audiocomponents#92). `tests/test_scheduling_seam.py` proves which events
the seam writes, against a Python stand-in for `audiopump_events.c`, and
runs anywhere; this one asks what was *heard*, off `audiopump.Tap`, with
the loop advanced by `audiopump.service()` on the calling thread. It needs
a MicroPython build carrying audiodsp as a usermod, because `audiopump`
exists only where the pump does. Four cases, each with a planted fault that
must make it exit non-zero: `--fault early|flat|held|armed`.

It renders each component under every interpreter it finds and holds it to a
hash captured from the original micropython-vst3 script — read out of that
repository's **git history** at `ac87f13`, not its working tree, because its
tree imports these packages now and would no longer be an independent oracle.
Needs `../bin/micropython` (or `--micropython`) and an `mpvst` checkout beside this one (or `--old-root`).
Comparison is always within one interpreter; cross-interpreter agreement is
recorded as an observation, never enforced.

**`--capture-old` cannot absorb a deliberate change.** It re-reads
micropython-vst3 at the fixed revision `ac87f13`, so re-running it rewrites the
same hashes. An instrument whose sound was changed on purpose will never match
that oracle again.

**Changing an instrument's sound on purpose.** Instruments are tuned by ear, by
whoever is playing them: change the voice, play it, keep it or don't. When a
change is kept, the same commit names the instrument in `REBUILT` in
`tests/parity/run_instruments_parity.py`, with the date, and `--verify` then
reports it as `rebuilt` instead of failing. No separate ruling, dossier or
re-capture is needed. Everything not named there is still held to the old
oracle exactly as before, because the gate's job is to catch sound that changes
by accident: a refactor, a moved node, an audiodsp bump. `--include-rebuilt`
compares the rebuilt ones anyway.

What stays mechanical for a kept change is only breakage: the instrument still
plays, isn't silent, doesn't clip, and still runs in real time on a board.

## The release chain

- `prepare-release.yml` opens the release PR (`VERSION` + `CHANGELOG.md`);
`tag-release.yml` tags the merged `VERSION`; `publish-release-packages.yml`
runs the gates above against the pin, builds both packages from the tag
(`working-directory: lib/<package>`), publishes them to TestPyPI, and
requests the two MIP index entries (`mip-profile:
audioinstruments,audioeffects`, on one call — mip serializes them).
- **Versions are the maintainer's to name.** An agent never edits `VERSION`,
never tags, never dispatches a workflow. `VERSION` holds a placeholder until
the maintainer names the release, and `tag-release.yml` refuses to tag anything that is
not a release version, so the placeholder cannot leak into a tag.
- The two packages version and release together. A local editable install
reports `0.0.0` because the build workflow writes `lib/<package>/VERSION`
at build time; that is cosmetic.

## What is not here

- No C. The native nodes these components call live in audiodsp; if a fix needs
to go below the Python, it goes there.
# AGENTS.md

- Test the code you changed, not the suite: `python tools/scoped_tests.py class:DeEsser` (or `effects`, `contract`, `auto`; `--list` prints the choice and runs nothing). The full run is very long ([CONTRIBUTING.md](CONTRIBUTING.md#run-the-tests)).
- CI also runs `python tools/validate_api.py`, `python tests/parity/effects_library_smoke.py` and `python -m flake8` (defect checks only, no layout rules).
- Set up against the audiodsp commit `AUDIODSP_PIN` names, not a TestPyPI release; a gate result against a floating core can't be attributed ([setup](CONTRIBUTING.md#set-up)). Moving the pin is its own change with the gates re-run.
- In a git worktree, an editable install still imports the main clone's `lib`. Put `PYTHONPATH=$PWD/lib` first, or your edit won't run.
- Instrument sound is held to hashes of the original scripts. `--capture-old` can't absorb a deliberate change; name the instrument in `REBUILT` in `tests/parity/run_instruments_parity.py` instead ([how](CONTRIBUTING.md#the-instrument-parity-gate)). Don't re-capture a golden casually.
- The parity gate and the live scheduling-seam gate need a MicroPython build with audiodsp built in, so CI doesn't run them.
- Don't edit `VERSION`, tag or dispatch a release; the version is named in the release pull request ([Release](CONTRIBUTING.md#release)). An editable install reports `0.0.0`; that is cosmetic.
- No C here. If a fix has to go below the Python, it goes in [audiodsp](https://github.com/PyDevices/audiodsp).
- The pages under `docs/reference/` are generated on every build; don't edit or commit them.
- A frozen module wins over `/lib` on MicroPython and CircuitPython, so a copied or mip-installed `audioinstruments` never runs until you rebuild or put `/lib` first. On a source checkout set `PYTHONPATH` (CPython) or `MICROPYPATH` (MicroPython) to `lib`.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
27 changes: 27 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,33 @@ Re-capturing a golden is a maintainer's decision, not a routine step. A failure
here during a deliberate rebuild is expected; it stops being a gate the moment
a golden is re-captured casually.

`--capture-old` can't absorb a deliberate change: it re-reads the original at the
fixed revision (`ac87f13`), so re-running it rewrites the same hashes, and an
instrument whose sound was changed on purpose never matches that oracle again.

To change an instrument's sound on purpose, tune it by ear: change the voice,
play it, keep it or don't. When a change is kept, the same commit names the
instrument in `REBUILT` in `tests/parity/run_instruments_parity.py`, with the
date, and `--verify` reports it as `rebuilt` instead of failing. No separate
ruling or re-capture is needed. Everything not named there is still held to the
old oracle, because the gate's job is to catch sound that changes by accident:
a refactor, a moved node, an audiodsp bump. `--include-rebuilt` compares the
rebuilt ones anyway. What stays mechanical for a kept change is breakage: the
instrument still plays, isn't silent, doesn't clip, and still runs in real time
on a board.

A second heavy gate asks what the real pump plays, not which events the
scheduling seam writes (`tests/test_scheduling_seam.py` does that against a
Python stand-in, and runs anywhere):

```bash
MICROPYPATH=$PWD:$PWD/lib micropython tests/parity/scheduling_seam_live.py
```

It needs a MicroPython build with audiodsp as a usermod, because `audiopump`
exists only where the pump does. It has four cases, each with a planted fault
that must make it exit non-zero: `--fault early|flat|held|armed`.

## Build the docs

```bash
Expand Down
Loading