# 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 ] [--sink ] [--no-master] [--list] ``` - `--dir` session root, default `~/Recordings/stems`. Each run writes `//NN-.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`.