Files
stems/README.md
T
Bendik Aagaard LynghaugandClaude Fable 5.1 9484d0d578
Release / build (aarch64, aarch64, /var/lib/gitea-runner/sccache, aarch64-unknown-linux-gnu) (push) Successful in 1m36s
Release / publish (push) Successful in 14s
Release / build (x86_64, ubuntu-latest, /sccache, x86_64-unknown-linux-gnu) (push) Successful in 1m50s
stems: multitrack PipeWire recorder
Records every source feeding the default sink into sample-aligned float WAV
stems plus the sink mix, so sessions can be mixed and mastered later.
Release flow mirrors galdr: tag -> build tarballs on both arches -> Gitea
release -> publish to the uhhm Arch registry (no AUR package).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 17:11:47 +02:00

65 lines
2.8 KiB
Markdown

# stems
Multitrack recorder for PipeWire. Run it, play your session, press Ctrl-C:
every source that fed the default sink has its own WAV stem, plus the sink's
mix as `00-master.wav`, all the same length and sample-aligned. Drop the
folder into any DAW and mix or master later.
```
pw-play / galdr / browser / mic loopback ──▶ default sink ──▶ speakers
│ │ │ │
└── stems taps each source port ────────┴── 00-master (sink monitor)
```
## Usage
```
stems [--dir <path>] [--sink <node.name>] [--no-master] [--list]
```
- `--dir` session root, default `~/Recordings/stems`. Each run writes
`<dir>/<YYYY-MM-DD_HH-MM-SS>/NN-<source>.wav`.
- `--sink` record the sources of this sink instead of the default one.
- `--no-master` skip `00-master.wav`.
- `--list` print the sources that would be recorded right now and exit.
Files are 32-bit float WAV at the graph rate, one per source node, with the
source's channel count (mono sources stay mono). Sources that start mid-session
get a track the moment they link to the sink; sources that stop keep their
file. Ctrl-C (or SIGTERM) finalizes everything and prints a table:
```
# track ch length starts at dropped
00 00-master.wav 2 42.67s 0.04s 0
01 01-galdr.wav 2 42.67s 0.00s 0
02 02-fc.zoom.playback.wav 2 42.67s 0.00s 0
03 03-pw-play.wav 2 42.67s 12.30s 0
```
## How alignment works
PipeWire stamps every graph cycle with one timestamp that all streams in that
cycle see (`pw_time.now`). Each stem's first buffer is placed at that
timestamp's distance from the session origin, cycles a stream missed are
filled with silence, and at the end every file is padded to the common length.
So frame N in one file is frame N in every other file; "starts at" in the
table is just how much leading silence a late-joining track got.
Each track is a capture stream linked by hand (`link-factory`) to the source's
output ports, so the session manager never re-routes it, and the realtime
callback only copies frames into a lock-free ring that a writer thread drains.
## Limitations
- A source that appears mid-session is tapped a few graph cycles after it
starts (the link has to be created once the graph reports it), so roughly
the first 30 to 50 ms of a new source are only in the master, not in its stem.
- The default sink is read once at start; switching sinks during a session
is not followed. Use `--sink` to pin one.
- Stems capture what the source sends to the sink, pre-fader: the sink's
per-stream volume is not applied (the master has it).
## Build
Rust, PipeWire 1.x. `cargo build --release`; the binary is `stems`.