Compare commits
3 Commits
3bac7c7472
...
f1acb92392
| Author | SHA1 | Date | |
|---|---|---|---|
| f1acb92392 | |||
| 617dd743a5 | |||
| d7753670fe |
241
README.md
241
README.md
@@ -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
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user