Metadata-Version: 2.4
Name: pylibresidfp
Version: 0.3.1
Summary: pybind11 binding of libresidfp (reSIDfp SID emulator) with chip state save/restore
License-Expression: GPL-2.0-or-later
License-File: COPYING
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: C++
Classifier: Operating System :: POSIX :: Linux
Classifier: Topic :: Multimedia :: Sound/Audio :: Sound Synthesis
Classifier: Topic :: System :: Emulators
Requires-Python: >=3.11
Requires-Dist: numpy>=1.26
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pytest-xdist; extra == "test"
Requires-Dist: vagrant-moses[cpu]; extra == "test"
Provides-Extra: dev
Requires-Dist: pylibresidfp[test]; extra == "dev"
Requires-Dist: black; extra == "dev"
Requires-Dist: pylint; extra == "dev"
Description-Content-Type: text/markdown

# pylibresidfp

pybind11 binding of [libresidfp](https://github.com/libsidplayfp/libresidfp) (the reSIDfp SID
emulator) with chip state save/restore, a deterministic output and a GIL-free render loop.

```python
from pylibresidfp import SID

sid = SID(model="6581", clock_hz=985248.0, sampling_hz=44100.0, method="decimate", filter=True)
sid.write(24, 15); sid.read(27); sid.peek(27); sid.reset()
sid.cycle                      # absolute cycle count, starts at 0
sid.clock(cycles)              # np.int16 samples for those cycles
sid.run(writes, until)         # writes: (N, 3) ints or [(cycle, reg, value), ...], absolute cycles
sid.serve(writes, cycle, reg)  # run to `cycle`, then read(reg): the body of a NativeSid on_read
sid.voices()                   # per-voice envelope (LFSR, rate, counter, state, ...) and oscillator state
sid.trace(writes, until)       # run without audio: (cycle, voice, envelope, state) at each envelope change
sid.envelope_trace(v, writes, until, lfsr=None)  # one voice's envelope alone from a copy (its
                               # rate counter's LFSR set to lfsr if given): the chip is untouched
sid.envelope_phases(v, writes, until)  # envelope_trace for every rate-counter phase at once: Phases
                               # (pieces, events, count, states) with .trajectory(k), exact for each k;
                               # events are ramps of envelope steps; .expand(table) -> (ids, times)
state = sid.save_state(); sid.restore_state(state)
twin = sid.copy()              # same parameters and state; SID is also picklable
```

`run` clocks to each write's cycle, writes it, then clocks to `until`. Write cycles must not
precede `sid.cycle` or each other and must not exceed `until`; violations raise `ValueError`.
`serve` discards the samples it produces (use `run` to keep audio).

`trace` clocks the oscillators and envelopes only (libresidfp's `clockDigital`), so a chip used for
tracing no longer renders the same audio as one that was not. `voices` and `trace` read libresidfp's
internals through `patches/0002-probe-access.patch`, which befriends a `reSIDfp::Probe` class.
`patches/0003-zero-state-padding.patch` zeroes the saved state's struct padding, so `save_state` is
reproducible byte for byte.

`clock`, `run`, `serve`, `trace`, `envelope_trace`, `envelope_phases`, `save_state` and `restore_state` release the GIL. Each instance has a busy
flag: use from a second thread while a call is in progress raises `RuntimeError`. Distinct
instances run in parallel.

Fed directly from `vagrant_moses`: `sid.run(native_sid.drain_writes(), until)`.

## Determinism

Two instances with the same parameters and writes produce identical samples, for both models and
both methods. Upstream libresidfp dithers the filter's float-to-int conversion from a table indexed by
a counter held in the process-wide filter-model singleton; `patches/0001-deterministic-rounding.patch`
(applied at build time) replaces the dither with plain rounding. Other process-wide state is
immutable after first use (filter and waveform tables). The static setters
`Filter6581::setFilterRange`/`setCaps` are not exposed. The pinned libresidfp has no `noiseLevel`, so there is no
`noise_level` option.

State is not portable across builds. `sid.cycle` is part of the saved state.

## Build

`git clone --recurse-submodules`, then `pip install .`. Needs CMake >= 3.20, `patch` and a C++17
compiler. libresidfp is the submodule `extern/libresidfp`, pinned to a release tag; its version is
read from the submodule's `configure.ac`, and `.github/workflows/libresidfp.yml` opens a PR when a
newer release appears. The build copies its sources and applies `patches/` to the copy. Tests: `pip install -e .[dev]`, then
`pytest -n auto --cov=pylibresidfp`. `docker build .` (build arg `PYTHON_VERSION`) runs the full
lint and test suite.

GPL-2.0-or-later (see COPYING).
