Compare commits

...

2 Commits

241
README.md
View File

@@ -1,54 +1,63 @@
# vmic # 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 ## Installation
`vmic_X_mic`. Optionally mix in a real microphone (`route -s`), and optionally hear
your own output via a self-monitor loopback (`edit -l`).
If you enable both a self-monitor loopback *and* a mixed-in hardware source at the Requires a Rust toolchain, and PipeWire + pipewire-pulse (`pactl`,
same time, vmic automatically splits into a second loopback stage so the hardware `pw-loopback`) on `PATH`.
source never leaks into your self-monitor:
``` ```sh
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
```
cargo build --release cargo build --release
``` ```
If the build fails inside `libspa-sys` (e.g. `no field 'data' on type 'spa_pod_builder'`), The binary is written to `target/release/vmic`. Place it on `PATH`, e.g.:
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:
```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 python3 -m venv ~/.local/share/vmic-build/libclang-venv
~/.local/share/vmic-build/libclang-venv/bin/pip install libclang ~/.local/share/vmic-build/libclang-venv/bin/pip install libclang
``` ```
@@ -60,63 +69,139 @@ then add to `.cargo/config.toml`:
LIBCLANG_PATH = "/home/<you>/.local/share/vmic-build/libclang-venv/lib/python3.*/site-packages/clang/native" LIBCLANG_PATH = "/home/<you>/.local/share/vmic-build/libclang-venv/lib/python3.*/site-packages/clang/native"
``` ```
## Usage ---
``` ## Commands
vmic create <name> [-l] # create a vmic, optionally with self-monitor
vmic route <name> -i <app[:media]> # move an app's playback into the vmic
vmic route <name> -o <app[:media]> # move an app's recording off the vmic
vmic route <name> -s <source|none> # mix a hardware mic in, or remove it
vmic edit <name> -l [<true|false>] # toggle the self-monitor loopback
vmic edit <name> --loopback-no-mix [<true|false>] # keep 1 stage even with loopback + a mixed source
vmic edit <name> -v <0.8|80> # self-monitor loopback volume
vmic edit <name> -sv <0.8|80> # mixed-in source volume
vmic delete <name> # tear down one vmic
vmic list # show all vmics, their status, and current node shape
vmic wipe # tear down every vmic, tracked or not
```
`-l` and `--loopback-no-mix` accept a bare form (means `true`) or an explicit | Command | Aliases | Description |
`true`/`false`, e.g. `vmic edit podcast -l` and `vmic edit podcast -l false` both work. |---------------|----------------|------------------------------------------------|
| `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 <bash|zsh|fish|...>`. ### `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 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" 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 ## How the topology adapts
`VMIC_STATE_DIR_OVERRIDE`).
## Testing A vmic always exposes the same two ports - `vmic_<name>_sink` for apps to
play into, `vmic_<name>_mic` for recorders to capture from - but how many
`cargo test` runs a handful of fast unit tests only - no PipeWire needed. PipeWire nodes back them depends on what's active:
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:
``` ```
tests/live_test.sh --input-app firefox --output-app chromium --hw-source fifine Simple2Node (default):
cargo test --test live_test -- --ignored --nocapture 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 `Pure4Node` only exists to keep a mixed-in hardware source out of the
for either (`VMIC_TEST_INPUT_APP`, `VMIC_TEST_OUTPUT_APP`, `VMIC_TEST_HW_SOURCE`, self-monitor loopback's ears - it's the shape vmic switches to the moment
`VMIC_TEST_NAME`, `VMIC_TEST_SKIP_WIPE=1`) - so point them at whatever apps and both `edit -l` and `route -s` are active at once, and switches back out of
hardware source you actually have available. They only ever touch vmics named the moment either stops being true. `edit --loopback-no-mix` opts a vmic out
`<name>`/`<name>_a`/`<name>_b`, refuse to run the `wipe` phase if any other vmic of ever making that switch: it stays at `Simple2Node`, and the mixed source
exists at that point, and always clean up after themselves even on failure. 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
```