diff --git a/README.md b/README.md index d564962..a0007dc 100644 --- a/README.md +++ b/README.md @@ -1,54 +1,63 @@ # vmic -Create and manage PipeWire virtual microphones easily, from the command line. +Create and manage PipeWire virtual microphones easily. + +Vmic wraps `pw-loopback`/`pactl` to turn ad-hoc loopback chains into named +profiles: create a virtual sink/mic pair once, then route apps and hardware +sources into it by name. Each vmic runs the cheapest PipeWire node shape that +satisfies its current settings, and vmic migrates it live as those settings +change. --- -## What it does +## Features -A vmic starts as a single loopback stage: +- **Dynamic topology**: a vmic starts as a single `pw-loopback` process (2 + nodes, sink straight into mic) and only upgrades to an isolated 4-node + shape when it's actually needed - a self-monitor loopback *and* a mixed-in + hardware source active at once. It downgrades back the moment either goes + away. +- **`--loopback-no-mix` opt-out**: keep the cheaper 2-node shape even with + both active, accepting that the mixed-in source becomes audible in the + self-monitor loopback in exchange for never running the second stage. +- **Automatic stream reconnection**: app streams already routed into or out + of a vmic are moved to the new nodes when a topology migration happens - + nothing needs to be re-selected in the app. +- **Spawn-before-teardown migrations**: a topology change always spawns the + new node pair(s) and confirms they're alive before touching the old ones, + so a failed migration never leaves a vmic worse off than before the + command ran. +- **Self-monitor loopback** with its own adjustable volume, independent of + the mixed-in hardware source's volume. +- **Status and listing** showing liveness and the vmic's current node + architecture, so the tradeoff of any given shape is always visible. +- **`wipe`**, a full teardown of every vmic-related PipeWire node, module, + and process on the system - tracked or not. +- **Shell completions** for bash, zsh, fish, and others. -``` -apps -> vmic_X_sink =(pw-loopback)=> vmic_X_mic -> recorders -``` +--- -Point an app's output device at `vmic_X_sink`, and a recording app's input device at -`vmic_X_mic`. Optionally mix in a real microphone (`route -s`), and optionally hear -your own output via a self-monitor loopback (`edit -l`). +## Installation -If you enable both a self-monitor loopback *and* a mixed-in hardware source at the -same time, vmic automatically splits into a second loopback stage so the hardware -source never leaks into your self-monitor: +Requires a Rust toolchain, and PipeWire + pipewire-pulse (`pactl`, +`pw-loopback`) on `PATH`. -``` -apps -> vmic_X_sink =(stage 1)=> vmic_X_mid -> vmic_X_mix =(stage 2)=> vmic_X_mic -> recorders - ^ hardware mic joins here instead -``` - -It collapses back to the single-stage form the moment either the loopback or the -mixed-in source goes away. `edit --loopback-no-mix true` opts a vmic out of this -split entirely, keeping it at one stage even with both active - the mixed source -just becomes audible in your self-monitor too, as an explicit tradeoff for staying -simpler. Either transition recreates the vmic's underlying sink/mic nodes (same -names, new ids); any stream that was routed into or out of them gets moved back -automatically, and vmic reports what happened either way. - -## Requirements - -- PipeWire + pipewire-pulse (`pactl`, `pw-loopback` on `PATH`) -- Rust (edition 2021) - -## Build - -``` +```sh cargo build --release ``` -If the build fails inside `libspa-sys` (e.g. `no field 'data' on type 'spa_pod_builder'`), -your system clang is newer than the pinned `bindgen` version supports. Point `LIBCLANG_PATH` -at an older libclang - a quick fix is a pip-vendored one: +The binary is written to `target/release/vmic`. Place it on `PATH`, e.g.: +```sh +install -Dm755 target/release/vmic ~/.local/bin/vmic ``` + +If the build fails inside `libspa-sys` (e.g. `no field 'data' on type +'spa_pod_builder'`), your system clang is newer than the pinned `bindgen` +version supports. Point `LIBCLANG_PATH` at an older libclang - a quick fix +is a pip-vendored version: + +```sh python3 -m venv ~/.local/share/vmic-build/libclang-venv ~/.local/share/vmic-build/libclang-venv/bin/pip install libclang ``` @@ -60,63 +69,139 @@ then add to `.cargo/config.toml`: LIBCLANG_PATH = "/home//.local/share/vmic-build/libclang-venv/lib/python3.*/site-packages/clang/native" ``` -## Usage +--- -``` -vmic create [-l] # create a vmic, optionally with self-monitor -vmic route -i # move an app's playback into the vmic -vmic route -o # move an app's recording off the vmic -vmic route -s # mix a hardware mic in, or remove it -vmic edit -l [] # toggle the self-monitor loopback -vmic edit --loopback-no-mix [] # keep 1 stage even with loopback + a mixed source -vmic edit -v <0.8|80> # self-monitor loopback volume -vmic edit -sv <0.8|80> # mixed-in source volume -vmic delete # tear down one vmic -vmic list # show all vmics, their status, and current node shape -vmic wipe # tear down every vmic, tracked or not -``` +## Commands -`-l` and `--loopback-no-mix` accept a bare form (means `true`) or an explicit -`true`/`false`, e.g. `vmic edit podcast -l` and `vmic edit podcast -l false` both work. +| Command | Aliases | Description | +|---------------|----------------|------------------------------------------------| +| `create` | `mk`, `make` | Create a new virtual microphone. | +| `route` | | Move app streams and mix a hardware source in. | +| `edit` | | Change loopback and volume settings on a vmic. | +| `delete` | `rm`, `remove` | Delete a virtual microphone. | +| `list` | `ls` | List all virtual microphones with live status. | +| `wipe` | `reset` | Tear down every vmic, tracked or not. | +| `completions` | | Generate a shell completion script. | -Aliases: `create` = `mk`/`make`, `delete` = `rm`/`remove`, `list` = `ls`, `wipe` = `reset`. +Run `vmic` or `vmic --help` for the full flag reference. -Shell completions: `vmic completions `. +### `create` flags -## Example +| Flag | Value | Meaning | Default | +|------------------|--------|---------------------------------------------|:-------:| +| `-l, --loopback` | (bare) | Enable a self-monitor loopback at creation. | off | -``` +### `route` flags + +| Flag | Value | Meaning | Default | +|----------------|----------------|------------------------------------------------------|:-------:| +| `-i, --input` | `APP[:MEDIA]` | Move matching sink-inputs into this vmic's sink. | - | +| `-o, --output` | `APP[:MEDIA]` | Move matching source-outputs off this vmic's source. | - | +| `-s, --source` | `SOURCE\|none` | Mix a hardware source into the vmic, or remove it. | - | + +### `edit` flags + +| Flag | Value | Meaning | Default | +|------------------------|----------|--------------------------------------------------------------------------------|:-------:| +| `-l, --loopback` | `[BOOL]` | Enable/disable the self-monitor loopback (bare `-l` means `true`). | - | +| `--loopback-no-mix` | `[BOOL]` | Keep the 2-node shape even with loopback + a mixed source (bare means `true`). | `false` | +| `-v, --volume` | `PCT` | Self-monitor loopback volume: fraction (`0.8`) or percent (`80`). | - | +| `-sv, --source-volume` | `PCT` | Mixed-in source volume: fraction (`0.8`) or percent (`80`). | - | + +`edit` only touches the fields given on the command line; everything else +stays as-is. Any flag that changes `--loopback`/`--loopback-no-mix` may +trigger a topology migration - see below. + +--- + +### Examples + +```sh +# Create a vmic with a self-monitor loopback. vmic create podcast -l -# set your app's output device to vmic_podcast_sink -# set your recorder's input device to vmic_podcast_mic + +# Point your app's output device at vmic_podcast_sink, and your +# recorder's input device at vmic_podcast_mic, then mix in a real mic. vmic route podcast -s "USB Microphone" + +# Move Firefox's playback into the vmic, and OBS's recording off it. +vmic route podcast -i firefox +vmic route podcast -o obs + +# Stop isolating the mixed source from the self-monitor, collapsing back +# to the cheaper 2-node shape. +vmic edit podcast --loopback-no-mix + +# Adjust volumes independently. +vmic edit podcast -v 0.8 +vmic edit podcast -sv 60 + +# Remove the mixed source. +vmic route podcast -s none + +# Inspect status, including current node architecture. +vmic list + +# Tear everything down. +vmic wipe ``` -## State +--- -Tracked in a SQLite database at `~/.config/vmic/vmic.db` (override the directory with -`VMIC_STATE_DIR_OVERRIDE`). +## How the topology adapts -## Testing - -`cargo test` runs a handful of fast unit tests only - no PipeWire needed. - -Two live integration test suites exercise every command against a real PipeWire -session: they spawn real `pw-loopback` processes, move real streams, and (guarded) -run the destructive `wipe` command. Neither runs automatically; opt in explicitly: +A vmic always exposes the same two ports - `vmic__sink` for apps to +play into, `vmic__mic` for recorders to capture from - but how many +PipeWire nodes back them depends on what's active: ``` -tests/live_test.sh --input-app firefox --output-app chromium --hw-source fifine -cargo test --test live_test -- --ignored --nocapture +Simple2Node (default): + apps -> vmic_X_sink =(pw-loopback)=> vmic_X_mic -> recorders + +Pure4Node (loopback + mixed source, isolated): + apps -> vmic_X_sink =(stage 1)=> vmic_X_mid -> vmic_X_mix =(stage 2)=> vmic_X_mic -> recorders + ^ hardware source joins here instead ``` -Both take the same parameters - as flags for the shell version, or matching env vars -for either (`VMIC_TEST_INPUT_APP`, `VMIC_TEST_OUTPUT_APP`, `VMIC_TEST_HW_SOURCE`, -`VMIC_TEST_NAME`, `VMIC_TEST_SKIP_WIPE=1`) - so point them at whatever apps and -hardware source you actually have available. They only ever touch vmics named -``/`_a`/`_b`, refuse to run the `wipe` phase if any other vmic -exists at that point, and always clean up after themselves even on failure. +`Pure4Node` only exists to keep a mixed-in hardware source out of the +self-monitor loopback's ears - it's the shape vmic switches to the moment +both `edit -l` and `route -s` are active at once, and switches back out of +the moment either stops being true. `edit --loopback-no-mix` opts a vmic out +of ever making that switch: it stays at `Simple2Node`, and the mixed source +just becomes audible in the self-monitor too. -## License +Switching shape necessarily recreates the nodes behind `sink`/`mic` - same +names, new underlying ids - because a `pw-loopback` process's capture and +playback nodes share one lifecycle and can't be reconfigured live. Each +migration: -APGL-3 +1. Spawns the new node pair(s) first. This is the only step that can fail; + if it does, the old topology is completely untouched. +2. Only then tears down the old node pair(s), best-effort. +3. Reconnects any app streams that were routed into or out of the old nodes, + by id, so nothing needs to be re-selected. +4. Re-applies the self-monitor loopback and any mixed-in source to the new + nodes. +5. Reports what happened - always, whether triggered by `route -s` or + `edit -l`/`edit --loopback-no-mix`. + +`vmic list` shows each vmic's current shape under `architecture:`. + +--- + +## State on disk + +Tracked in a SQLite database at `$XDG_CONFIG_HOME/vmic/vmic.db`, one row per +vmic (names, node ids, pids, loopback/volume/mixed-source settings). The +directory can be overridden with the `VMIC_STATE_DIR_OVERRIDE` environment +variable. + +--- + +## Shell completions + +```sh +vmic completions bash > /etc/bash_completion.d/vmic +vmic completions zsh > "${fpath[1]}/_vmic" +vmic completions fish > ~/.config/fish/completions/vmic.fish +```