Windows (WASAPI) support: cfg-gate Linux-only virtual mic
The Linux/PipeWire virtual mic (pactl, WirePlumber fix, signal restore) is now behind target_os cfg; on Windows/macOS it compiles to stubs and virtual mic defaults to off. Use VB-CABLE/BlackHole as the output device. Cross-checked with cargo check --target x86_64-pc-windows-gnu (0 errors).
This commit is contained in:
parent
aee4f53fd2
commit
13f85b4220
4 changed files with 61 additions and 12 deletions
|
|
@ -40,7 +40,7 @@ vois.rs turns your microphone into a voice changer in real time: shift pitch and
|
||||||
| TUI | Full-screen terminal UI with arrow-key + mouse control and live meters |
|
| TUI | Full-screen terminal UI with arrow-key + mouse control and live meters |
|
||||||
| WAV recording | Record the processed audio with the `R` hotkey or `--record` |
|
| WAV recording | Record the processed audio with the `R` hotkey or `--record` |
|
||||||
| Test tone | `--tone 220` lets you hear what a preset does without a mic |
|
| Test tone | `--tone 220` lets you hear what a preset does without a mic |
|
||||||
| Cross-platform | Linux (ALSA/PipeWire), macOS (CoreAudio), Windows (WASAPI) |
|
| Cross-platform | Linux (ALSA/PipeWire), Windows (WASAPI), macOS (CoreAudio) |
|
||||||
|
|
||||||
### Presets
|
### Presets
|
||||||
|
|
||||||
|
|
@ -161,7 +161,7 @@ vois.rs превращает твой микрофон в войс-чендже
|
||||||
| TUI | Полноэкранный интерфейс с управлением стрелками и мышью, живые уровни |
|
| TUI | Полноэкранный интерфейс с управлением стрелками и мышью, живые уровни |
|
||||||
| Запись в WAV | Клавиша `R` или `--record` |
|
| Запись в WAV | Клавиша `R` или `--record` |
|
||||||
| Тестовый тон | `--tone 220` — послушать пресет без микрофона |
|
| Тестовый тон | `--tone 220` — послушать пресет без микрофона |
|
||||||
| Кроссплатформенность | Linux (ALSA/PipeWire), macOS (CoreAudio), Windows (WASAPI) |
|
| Кроссплатформенность | Linux (ALSA/PipeWire), Windows (WASAPI), macOS (CoreAudio) |
|
||||||
|
|
||||||
### Пресеты
|
### Пресеты
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
// Virtual microphone setup for PipeWire/PulseAudio.
|
// Virtual microphone setup.
|
||||||
//
|
//
|
||||||
// Creates a "vois.rs" null sink and remaps its monitor source to "vois.rs".
|
// Linux/FreeBSD (PipeWire/PulseAudio): creates a "vois.rs" null sink and
|
||||||
// The app does NOT hijack the default sink permanently: it briefly points the
|
// remaps its monitor source to "vois.rs", keeps the app's stream routed there
|
||||||
// default sink at `vois.rs` while its own output stream is created (so the
|
// and restores the default sink on exit.
|
||||||
// processed voice lands in the virtual mic) and restores it right after.
|
//
|
||||||
// Other apps' streams are not moved, so their audio never leaks into the mic.
|
// Other platforms (Windows WASAPI, macOS CoreAudio): no built-in virtual mic.
|
||||||
use anyhow::{bail, Context, Result};
|
// Point the app's output at a virtual cable (e.g. VB-CABLE "CABLE Input" on
|
||||||
|
// Windows, BlackHole on macOS) with `-o` and select it in the voice app.
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
use anyhow::{bail, Context};
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
@ -14,7 +20,11 @@ use std::time::Duration;
|
||||||
|
|
||||||
pub struct VirtualMic;
|
pub struct VirtualMic;
|
||||||
|
|
||||||
/// WirePlumber config that stops it from pinning streams to remembered sinks.
|
// ---------------------------------------------------------------------------
|
||||||
|
// Linux / FreeBSD implementation (PipeWire / PulseAudio + pactl)
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
fn wireplumber_conf() -> std::path::PathBuf {
|
fn wireplumber_conf() -> std::path::PathBuf {
|
||||||
let base = std::env::var("XDG_CONFIG_HOME")
|
let base = std::env::var("XDG_CONFIG_HOME")
|
||||||
.map(std::path::PathBuf::from)
|
.map(std::path::PathBuf::from)
|
||||||
|
|
@ -28,6 +38,7 @@ fn wireplumber_conf() -> std::path::PathBuf {
|
||||||
.join("51-vois.conf")
|
.join("51-vois.conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
impl VirtualMic {
|
impl VirtualMic {
|
||||||
pub fn setup() -> Result<Option<String>> {
|
pub fn setup() -> Result<Option<String>> {
|
||||||
let pactl = "pactl";
|
let pactl = "pactl";
|
||||||
|
|
@ -221,6 +232,7 @@ impl VirtualMic {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
fn sink_id_named(name: &str) -> Option<String> {
|
fn sink_id_named(name: &str) -> Option<String> {
|
||||||
let out = Command::new("pactl")
|
let out = Command::new("pactl")
|
||||||
.args(["list", "short", "sinks"])
|
.args(["list", "short", "sinks"])
|
||||||
|
|
@ -235,6 +247,7 @@ fn sink_id_named(name: &str) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
fn client_names() -> Result<std::collections::HashMap<String, String>> {
|
fn client_names() -> Result<std::collections::HashMap<String, String>> {
|
||||||
let out = Command::new("pactl")
|
let out = Command::new("pactl")
|
||||||
.args(["list", "short", "clients"])
|
.args(["list", "short", "clients"])
|
||||||
|
|
@ -250,6 +263,7 @@ fn client_names() -> Result<std::collections::HashMap<String, String>> {
|
||||||
Ok(map)
|
Ok(map)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
fn sink_inputs() -> Result<Vec<(String, String, String)>> {
|
fn sink_inputs() -> Result<Vec<(String, String, String)>> {
|
||||||
let out = Command::new("pactl")
|
let out = Command::new("pactl")
|
||||||
.args(["list", "short", "sink-inputs"])
|
.args(["list", "short", "sink-inputs"])
|
||||||
|
|
@ -269,7 +283,39 @@ fn sink_inputs() -> Result<Vec<(String, String, String)>> {
|
||||||
Ok(inputs)
|
Ok(inputs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
fn has_entry(list: &str, name: &str) -> bool {
|
fn has_entry(list: &str, name: &str) -> bool {
|
||||||
list.lines()
|
list.lines()
|
||||||
.any(|l| l.split_whitespace().nth(1).is_some_and(|n| n == name))
|
.any(|l| l.split_whitespace().nth(1).is_some_and(|n| n == name))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Other platforms: virtual mic is provided externally (VB-CABLE / BlackHole).
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "linux", target_os = "freebsd")))]
|
||||||
|
impl VirtualMic {
|
||||||
|
pub fn setup() -> Result<Option<String>> {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_default_sink() -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_default_sink(_name: &str) {}
|
||||||
|
|
||||||
|
pub fn wait_default_sink(_name: &str) {}
|
||||||
|
|
||||||
|
pub fn keep_others_off(stop: Arc<AtomicBool>, _prev_sink: String) -> thread::JoinHandle<()> {
|
||||||
|
thread::spawn(move || {
|
||||||
|
while !stop.load(Ordering::Relaxed) {
|
||||||
|
thread::sleep(Duration::from_millis(500));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_on_signal(_prev: String) -> thread::JoinHandle<()> {
|
||||||
|
thread::spawn(|| {})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,8 @@ impl AppConfig {
|
||||||
virtual_mic: if cli.no_virtual_mic {
|
virtual_mic: if cli.no_virtual_mic {
|
||||||
false
|
false
|
||||||
} else {
|
} else {
|
||||||
file.virtual_mic.unwrap_or(true)
|
file.virtual_mic
|
||||||
|
.unwrap_or(cfg!(any(target_os = "linux", target_os = "freebsd")))
|
||||||
},
|
},
|
||||||
pitch_correct: cli.pitch_correct.or(file.pitch_correct).unwrap_or(false),
|
pitch_correct: cli.pitch_correct.or(file.pitch_correct).unwrap_or(false),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,7 +104,9 @@ fn main() -> Result<()> {
|
||||||
"virtual mic ready: use \"{src}\" (or \"vois.rs.monitor\") as your microphone"
|
"virtual mic ready: use \"{src}\" (or \"vois.rs.monitor\") as your microphone"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Ok(None) => println!("virtual mic setup failed: no source reported"),
|
Ok(None) => println!(
|
||||||
|
"virtual mic not available on this platform: point -o at a virtual cable (e.g. VB-CABLE \"CABLE Input\") and select it in your voice app"
|
||||||
|
),
|
||||||
Err(e) => println!("warning: virtual mic setup failed: {e:#}"),
|
Err(e) => println!("warning: virtual mic setup failed: {e:#}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue