# vmic 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. --- ## Features - **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. --- ## Installation Requires a Rust toolchain, and PipeWire + pipewire-pulse (`pactl`, `pw-loopback`) on `PATH`. ```sh cargo build --release ``` 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 ``` then add to `.cargo/config.toml`: ```toml [env] LIBCLANG_PATH = "/home//.local/share/vmic-build/libclang-venv/lib/python3.*/site-packages/clang/native" ``` --- ## Commands | 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. | Run `vmic` or `vmic --help` for the full flag reference. ### `create` flags | 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 # 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 ``` --- ## How the topology adapts 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: ``` 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 ``` `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. 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: 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 ```