Compare commits

..

3 Commits

211
README.md
View File

@@ -1,36 +1,63 @@
# vmic # vmic
Create and manage PipeWire virtual microphones easily, from the command line. Create and manage PipeWire virtual microphones easily.
## What it does 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.
A vmic is two chained loopback stages: ---
``` ## Features
apps -> vmic_X_sink =(stage 1)=> vmic_X_mid -> vmic_X_mix =(stage 2)=> vmic_X_mic -> recorders
^ hardware mic joins here (route -s)
```
Point an app's output device at `vmic_X_sink`, and a recording app's input device at - **Dynamic topology**: a vmic starts as a single `pw-loopback` process (2
`vmic_X_mic`. Optionally mix in a real microphone downstream, and optionally hear your nodes, sink straight into mic) and only upgrades to an isolated 4-node
own output via a self-monitor loopback (which never carries the mixed-in mic). 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.
## Requirements ---
- PipeWire + pipewire-pulse (`pactl`, `pw-loopback`, `pw-link` on `PATH`) ## Installation
- Rust (edition 2021)
## Build Requires a Rust toolchain, and PipeWire + pipewire-pulse (`pactl`,
`pw-loopback`) on `PATH`.
``` ```sh
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
``` ```
@@ -42,39 +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> -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 and their status
vmic wipe # tear down every vmic, tracked or not
```
Aliases: `create` = `mk`/`make`, `delete` = `rm`/`remove`, `list` = `ls`, `wipe` = `reset`. | 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. |
Shell completions: `vmic completions <bash|zsh|fish|...>`. Run `vmic` or `vmic --help` for the full flag reference.
## Example ### `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 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`).
## License 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
PipeWire nodes back them depends on what's active:
APGL-3 ```
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
```