Metadata-Version: 2.4
Name: unitracker-player
Version: 0.30.0
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Requires-Dist: unitracker-grammar>=0.21.0
Requires-Dist: jennings>=0.9.0
Requires-Dist: lark>=1.2
Requires-Dist: numpy>=1.26
Requires-Dist: tqdm>=4.66
Requires-Dist: black>=24 ; extra == 'dev'
Requires-Dist: pylint>=3 ; extra == 'dev'
Requires-Dist: pytest>=7 ; extra == 'dev'
Requires-Dist: pytest-cov>=4 ; extra == 'dev'
Requires-Dist: pytest-xdist>=3 ; extra == 'dev'
Requires-Dist: pysidtracker>=0.30.0 ; extra == 'dev'
Requires-Dist: zstandard>=0.21 ; extra == 'dev'
Provides-Extra: dev
License-File: LICENSE
Summary: Plays canonical unitracker documents to SID register state per tick, and builds them into standalone .sid files
Author: Josh Bailey
Requires-Python: >=3.12
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
Project-URL: Homepage, https://github.com/anarkiwi/unitracker-player

# unitracker-player

The generic player of canonical [unitracker](https://github.com/anarkiwi/unitracker) documents,
and their builder into standalone PSID `.sid` files.
It reads a document with the grammar of
[unitracker-grammar](https://github.com/anarkiwi/unitracker-grammar) and plays it to the state of
the 25 SID registers (`$D400`-`$D418`) at the end of each frame. It knows only the document: its
instruments, programs, expressions, phrases and timeline, and the note table `F(k)` at its clock.

## Install

```sh
pip install "unitracker-player @ git+https://github.com/anarkiwi/unitracker-player"
```

## Native core

Wheels carry a Rust implementation of the call machine that `play` uses by default; the Python
`Player` plays the same documents where the extension is absent (`UNITRACKER_PLAYER_NATIVE=0`
switches it off). See [docs/native.md](docs/native.md).

## Use

```python
from unitracker_player import load, play, Player

frames = play(text)               # np.uint8 array (frames, 25), to the end of an ending song
frames = play(text, ticks=3000)   # 3000 frames; a looping song needs ticks
song = load(text)                 # parse and compile once; play(song, ...) reuses it
player = Player(song)
regs = player.step()              # the 25 registers after one frame; None once the song ends
player.regs                       # the register file the last call wrote, one store per register
```

It plays formats 2.3 to 2.5, the grammar's formats: their sugar (prelude templates, chord
names, instrument and phrase templates, groups, sections, first-use definitions) as the
grammar's core expansion (`unitracker_grammar.expand.core`), and once bindings (`pw:'v + 3'!`), plain integers as once writes (`pw:800`),
`lag:k`, `calls`, `holds`, `global:[cut,res,route,mode,vol]`, the cells `ctrl ad sr res route mode vol`, and 2.4's program layers (`layers:{ctrl:[a,b]}`, `b.ctrl:c7`,
`b.run:c7`) and per-voice orderlists (` - voice:[p1,p2+2|loop:1]`) itself; 2.4's prelude hard restart
(`restart:hr(2)`, `restart:hr(2,15,0)`) plays as the restart rows it stands for. Code templates run as 6502 routines on a
[jennings](https://pypi.org/project/jennings/) MPU, with per-voice state from the document's
`state` line; templates, in the top-level `mod` section keyed by signatures (`vib(c,s):|`), are
bound by calls (`wob.freq:vib(3,40)`), and code reads the note table as `F.lo` / `F.hi`. A
program record may be a template keyed by its signature
(`arp(a,b):[{ctrl:65,note:+a},{wait:a,note:-b},{loop:0}]`), which a call binds wherever a
program's name stands (`ctrl:arp(7,4)`, `restart:hr(2,9,220)`, `run:arp(7,4)`).
Any definition may also stand inline at its one use (`C4.{ad:9}`, `run:pw[{pw:5},end]`,
`play:[[C4,-*3]+2]`, `C4 pw:asm{lda phi; sta v; rts}`, `C4 pw:'v + 40'`) and plays as its named
form. A cell's repeated keys and commands apply in the order written, `reset` restarts a
binding's phi (`C4 pw:ptab(3) pw:reset`), and a `regs` line gives the 25 registers before tick 0.

`read(text)` returns the document as unitracker-grammar's model (`unitracker_grammar.document.Document`) after its `expand.expanded`. A text outside the grammar raises
`unitracker_grammar.CanonicalError`; a document the player refuses raises `PlayError`.

```sh
unitracker-play song.yaml --ticks 3000 --out frames.npz   # array "frames"
unitracker-play song.yaml --out frames.csv                # one row per frame, d400..d418
unitracker-play song.yaml                                 # CSV to stdout
```

```sh
unitracker-sid song.yaml                         # song.sid
unitracker-sid song.yaml --out x.sid --ticks 3000 --load 0x2000
```

`unitracker-sid` renders the song here, proves where a looping song repeats, and assembles a
6502 playroutine (llvm-mos, locally or in the
[docker-mos-llvm-sdk](https://github.com/anarkiwi/docker-mos-llvm-sdk) image) whose raster IRQ
plays the same registers tick for tick.

## Docs

[docs/semantics.md](docs/semantics.md): the meaning of every construct of the grammar as played.
[docs/sid.md](docs/sid.md): how a document becomes a `.sid`, and the tests that check it.
`tools/corpus.py`: records the documents a test suite parses, plays a directory of them and
compares two such runs, to check that a change keeps what documents play.

