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>
65 lines
2.8 KiB
Markdown
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`.
|