|
| 1 | +# AGENTS.md |
| 2 | + |
| 3 | +This file provides guidance to AI coding agents when working with code in this repository. |
| 4 | + |
| 5 | +## What this is |
| 6 | + |
| 7 | +python-rtmixer (`rtmixer` on PyPI) provides reliable low-latency audio playback and recording |
| 8 | +from Python. It builds on [sounddevice](https://python-sounddevice.readthedocs.io/) and |
| 9 | +PortAudio, but the audio callback is written in C (compiled with CFFI) so that it never enters |
| 10 | +the Python interpreter and is not affected by the GIL or garbage collection. It is a small |
| 11 | +project maintained by volunteers: one Python module, one C file, and one C header. |
| 12 | + |
| 13 | +## Working with humans (read first) |
| 14 | + |
| 15 | +Every change here is reviewed by a volunteer maintainer and then maintained by humans for years. |
| 16 | +The human you are working with is the author of anything that leaves their machine, and they are |
| 17 | +the one who talks to the maintainers. |
| 18 | + |
| 19 | +- **The human is the sole author.** Do not add `Co-Authored-By:` trailers for Claude / Copilot / |
| 20 | + Cursor / any other tool, and do not make a bot the commit author. The human opening the PR |
| 21 | + takes full responsibility for it and must be able to explain and defend every line on request. |
| 22 | + Disclose which tools were used, and for what, in the PR description instead. |
| 23 | +- **Do not speak for the human in public.** Do not post agent-written text to issues, PRs, review |
| 24 | + comments, or discussions, and do not open, comment on, or close them on the human's behalf. |
| 25 | + Draft for the human if they ask; they decide whether to post and rewrite it in their own words. |
| 26 | +- **Do not commit or push unless asked**, and never force-push, rewrite published history, or |
| 27 | + create tags or releases. |
| 28 | +- **Agree on the design before writing code.** For anything beyond a local fix, describe the |
| 29 | + approach to the human in a few sentences and confirm it is the smallest one that works. If the |
| 30 | + change touches the C callback or the Python/C memory-ownership contract (see Architecture), say |
| 31 | + so explicitly; those are the changes a maintainer will look at hardest. |
| 32 | +- **Report what actually happened.** If a build failed, an example was not run, or something |
| 33 | + could not be tested without audio hardware, say so plainly rather than implying it works. |
| 34 | + |
| 35 | +## Keep code and tests brief |
| 36 | + |
| 37 | +A big diff is a cost, not an accomplishment. The whole library is about 850 lines of Python and C; a change that |
| 38 | +adds a large fraction of that needs a very good reason. |
| 39 | + |
| 40 | +- **Stop and re-plan at roughly 50 new lines of implementation.** Growth past that, or reaching |
| 41 | + for a new module, a new dependency, or a new action type, is a signal that the design is too |
| 42 | + elaborate. Stop, tell the human, and offer the smaller version. |
| 43 | +- **YAGNI.** No retry logic, configuration knobs, platform-specific fallbacks, extension points, |
| 44 | + or "robustness" that nothing in the change needs. New public API (a method, a keyword argument, |
| 45 | + a new `actiontype`) needs a demonstrated user need, because it is documented and supported |
| 46 | + forever. Hard-code the single behavior that is wanted and add the option when someone asks. |
| 47 | +- **DRY.** Reuse what exists: `_Base._enqueue`, `_check_channels`, the `sounddevice` stream |
| 48 | + machinery, `pa_ringbuffer`. When the four `play_*`/`record_*` methods or the C action handlers |
| 49 | + need the same new logic, factor it into one private helper rather than editing four copies. |
| 50 | +- **Implement at the highest layer that exists.** Behavior shared by `Mixer`, `Recorder`, and |
| 51 | + `MixerAndRecorder` belongs in `_Base`. |
| 52 | +- **Keep tests and verification just as brief.** There is currently no unit test suite (CI only |
| 53 | + builds wheels and checks that `import rtmixer` works). Do not add a test framework, test |
| 54 | + directory, mock audio backend, or CI job without asking the human first. When a test is |
| 55 | + warranted, prefer a few compact assertions over a large harness, and prefer extending an |
| 56 | + existing example in `examples/` over writing a new script. Throwaway verification scripts go in |
| 57 | + a scratch directory, not the repo. |
| 58 | +- **Do not add or bump dependencies** (runtime or build) without asking first. |
| 59 | + |
| 60 | +## Common commands |
| 61 | + |
| 62 | +Development install (the `portaudio` git submodule is needed to compile the extension): |
| 63 | +```bash |
| 64 | +git submodule update --init |
| 65 | +python -m pip install -e . |
| 66 | +``` |
| 67 | + |
| 68 | +After changing `src/rtmixer.c`, `src/rtmixer.h`, or `rtmixer_build.py`, rebuild the extension |
| 69 | +(rerun the editable install; the compiled `_rtmixer*.so` lands in `src/`). A quick smoke test: |
| 70 | +```bash |
| 71 | +python -c "import rtmixer; print(rtmixer.__version__)" |
| 72 | +python examples/fetch_stats.py # needs a working audio device |
| 73 | +``` |
| 74 | + |
| 75 | +Docs (Sphinx, built on CircleCI and Read the Docs): |
| 76 | +```bash |
| 77 | +python -m pip install -r doc/requirements.txt |
| 78 | +python -m sphinx doc _build |
| 79 | +python -m sphinx doc _build -b linkcheck |
| 80 | +``` |
| 81 | + |
| 82 | +Wheels are built with cibuildwheel (`.github/workflows/build_wheels.yml`, configured in |
| 83 | +`pyproject.toml`) as abi3 wheels for CPython >= 3.10 plus PyPy. |
| 84 | + |
| 85 | +Before handing work back, at minimum rebuild the extension if C code changed, check that |
| 86 | +`import rtmixer` works, run whichever example exercises the changed code if an audio device is |
| 87 | +available (and say so if not), and build the docs if docstrings or `doc/` changed. |
| 88 | + |
| 89 | +## Boundaries |
| 90 | + |
| 91 | +Ask the human first before: |
| 92 | +- adding or bumping a dependency, or changing the minimum Python version; |
| 93 | +- editing CI / release configuration (`.github/`, `.circleci/`, `.readthedocs.yml`, `tools/`, |
| 94 | + `[tool.cibuildwheel]` in `pyproject.toml`) or the `portaudio` submodule pointer; |
| 95 | +- adding, renaming, or removing public API, including new action types or keyword arguments; |
| 96 | +- changing the version number or editing existing `NEWS.rst` entries for released versions. |
| 97 | + |
| 98 | +Never: |
| 99 | +- add AI co-authorship trailers or post agent-written text to issues or PRs (see above); |
| 100 | +- call into Python, allocate or free memory, take locks, or do I/O in the C audio callback; |
| 101 | +- silence compiler warnings or weaken a `CALLBACK_ASSERT` instead of fixing the cause; |
| 102 | +- commit build products (`_rtmixer*.so`, `*.egg-info`, `build/`, `_build/`) or audio files; |
| 103 | +- adapt code whose license is not compatible with this project's MIT license or cannot be |
| 104 | + determined. |
| 105 | + |
| 106 | +## Architecture |
| 107 | + |
| 108 | +### Files |
| 109 | +- `src/rtmixer.py`: the public Python API. `_Base` (a subclass of `sounddevice._StreamBase`) |
| 110 | + owns the queues and state; `Mixer` (output), `Recorder` (input), and `MixerAndRecorder` |
| 111 | + (duplex) add `play_buffer`/`play_ringbuffer` and `record_buffer`/`record_ringbuffer`. |
| 112 | + `RingBuffer` is re-exported from `pa_ringbuffer`. Upper-case constants from the C library |
| 113 | + (`PLAY_BUFFER`, `CANCEL`, ...) are copied into the module namespace at import. |
| 114 | +- `src/rtmixer.h`: declarations shared by C and CFFI (`enum actiontype`, `struct action`, |
| 115 | + `struct stats`, `struct state`). It is parsed by `ffibuilder.cdef()`, so it must stay within |
| 116 | + what CFFI's parser accepts (no `#include`, no macros beyond what is already there). |
| 117 | +- `src/rtmixer.c`: the PortAudio callback and its helpers. |
| 118 | +- `rtmixer_build.py`: the CFFI build script that produces the `_rtmixer` extension, compiling |
| 119 | + `rtmixer.c` together with `pa_ringbuffer.c` from the `portaudio` submodule. PortAudio itself |
| 120 | + is not linked; the callback is handed to sounddevice's already-loaded PortAudio. |
| 121 | +- `doc/`: Sphinx docs. `doc/api.rst` uses `literalinclude` on ranges of `src/rtmixer.h`, so |
| 122 | + editing `struct action` or `struct stats` changes the rendered docs. `doc/fake_rtmixer.py` and |
| 123 | + `doc/fake_sounddevice.py` stand in for the compiled modules during the docs build. |
| 124 | +- `examples/`: runnable scripts; these double as the manual test suite. |
| 125 | + |
| 126 | +### Actions and queues |
| 127 | +Each command (`play_buffer`, `cancel`, `fetch_and_reset_stats`, ...) allocates a |
| 128 | +`struct action` with `ffi.new()` in Python and writes the pointer into `action_q`, a lock-free |
| 129 | +PortAudio ring buffer. Once per audio block the callback reads new actions into a singly linked |
| 130 | +list (`CANCEL` actions go to the front, everything else to the back), processes each one against |
| 131 | +the block's timing info (`requested_time`, `allow_belated` encoded via `actual_time`), and moves |
| 132 | +finished actions to `done_actions`, which are flushed into `result_q` as space allows. Python |
| 133 | +drains `result_q` in `_drain_result_q()` (called from `_enqueue()` and the `actions` property) |
| 134 | +and drops its references. |
| 135 | + |
| 136 | +### Memory ownership |
| 137 | +The C callback only ever holds raw pointers; Python owns all memory. `_Base._actions` maps each |
| 138 | +in-flight action to the object it depends on (`keep_alive`: the audio buffer, the ring buffer, |
| 139 | +or the action being cancelled), and an entry is removed only after the callback has handed the |
| 140 | +action back through `result_q`. `_Base.__del__` aborts and closes the stream before that memory |
| 141 | +can be freed. Any change that adds a pointer to `struct action` or `struct state` must keep this |
| 142 | +contract, and is the most likely place for a use-after-free or crash. |
| 143 | + |
| 144 | +### Real-time constraints |
| 145 | +The callback runs on PortAudio's audio thread. It must be bounded-time and must not allocate, |
| 146 | +lock, block, or call into Python. `CALLBACK_ASSERT` prints and returns `paAbort` on failure |
| 147 | +(debug builds only; `NDEBUG` disables it). |
| 148 | + |
| 149 | +## Changelog (`NEWS.rst`) |
| 150 | + |
| 151 | +Add a `NEWS.rst` entry for changes that users would notice: new or changed API or behavior, bug |
| 152 | +fixes in the callback or Python API, new or dropped Python versions or wheel platforms, and new |
| 153 | +or changed dependencies. Skip it for refactoring, docs-only edits, examples, and CI or tooling |
| 154 | +changes that do not change what gets shipped. Put the entry at the top of the file under a |
| 155 | +`Version x.y.z -- unreleased` heading (create one with the next patch version if it is missing). |
| 156 | +The maintainer fills in the date and the `PyPI__` / `diff__` links at release time. Use the same |
| 157 | +style as the existing entries: one short bullet per change, starting with a verb ("Add", "Fix", |
| 158 | +"Drop"), describing the effect on users rather than the implementation. |
| 159 | + |
| 160 | +## Code conventions |
| 161 | + |
| 162 | +- Python: PEP 8, lines at most 79 characters (see `.editorconfig`); single quotes are used |
| 163 | + throughout. Docstrings are reStructuredText rendered by Sphinx with `default_role = 'any'`, so |
| 164 | + single backticks cross-reference (`` `wait()` ``, `` `sounddevice.Stream` ``) and |
| 165 | + `nitpicky = True` means unresolved references break the docs build. |
| 166 | +- C: C99, two-space indentation, braces on their own lines, leading commas on continued |
| 167 | + parameter lists, `//` comments, as in `src/rtmixer.c`. |
| 168 | +- Private names start with an underscore; imported third-party modules are aliased privately |
| 169 | + (`import sounddevice as _sd`) so they do not show up in the public namespace. |
| 170 | +- Match the surrounding style rather than reformatting existing code; there is no |
| 171 | + auto-formatter, and unrelated whitespace changes make review harder. |
| 172 | +- Code adapted from an outside source must be under an MIT-compatible license (MIT, BSD, ISC, |
| 173 | + Apache-2.0, public domain, ...); attribute it in a comment directly above the adapted code, |
| 174 | + naming the source and its license, as `setup.py` and `.circleci/config.yml` already do. |
0 commit comments