diff --git a/README.md b/README.md index 04103e3..bd0436d 100644 --- a/README.md +++ b/README.md @@ -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 | | 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 | -| Cross-platform | Linux (ALSA/PipeWire), macOS (CoreAudio), Windows (WASAPI) | +| Cross-platform | Linux (ALSA/PipeWire), Windows (WASAPI), macOS (CoreAudio) | ### Presets @@ -161,7 +161,7 @@ vois.rs превращает твой микрофон в войс-чендже | TUI | Полноэкранный интерфейс с управлением стрелками и мышью, живые уровни | | Запись в WAV | Клавиша `R` или `--record` | | Тестовый тон | `--tone 220` — послушать пресет без микрофона | -| Кроссплатформенность | Linux (ALSA/PipeWire), macOS (CoreAudio), Windows (WASAPI) | +| Кроссплатформенность | Linux (ALSA/PipeWire), Windows (WASAPI), macOS (CoreAudio) | ### Пресеты diff --git a/src/audio/virtual_mic.rs b/src/audio/virtual_mic.rs index e0d3deb..fd27453 100644 --- a/src/audio/virtual_mic.rs +++ b/src/audio/virtual_mic.rs @@ -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". -// The app does NOT hijack the default sink permanently: it briefly points the -// default sink at `vois.rs` while its own output stream is created (so the -// 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. -use anyhow::{bail, Context, Result}; +// Linux/FreeBSD (PipeWire/PulseAudio): creates a "vois.rs" null sink and +// remaps its monitor source to "vois.rs", keeps the app's stream routed there +// and restores the default sink on exit. +// +// Other platforms (Windows WASAPI, macOS CoreAudio): no built-in virtual mic. +// 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::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; @@ -14,7 +20,11 @@ use std::time::Duration; 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 { let base = std::env::var("XDG_CONFIG_HOME") .map(std::path::PathBuf::from) @@ -28,6 +38,7 @@ fn wireplumber_conf() -> std::path::PathBuf { .join("51-vois.conf") } +#[cfg(any(target_os = "linux", target_os = "freebsd"))] impl VirtualMic { pub fn setup() -> Result> { let pactl = "pactl"; @@ -221,6 +232,7 @@ impl VirtualMic { } } +#[cfg(any(target_os = "linux", target_os = "freebsd"))] fn sink_id_named(name: &str) -> Option { let out = Command::new("pactl") .args(["list", "short", "sinks"]) @@ -235,6 +247,7 @@ fn sink_id_named(name: &str) -> Option { None } +#[cfg(any(target_os = "linux", target_os = "freebsd"))] fn client_names() -> Result> { let out = Command::new("pactl") .args(["list", "short", "clients"]) @@ -250,6 +263,7 @@ fn client_names() -> Result> { Ok(map) } +#[cfg(any(target_os = "linux", target_os = "freebsd"))] fn sink_inputs() -> Result> { let out = Command::new("pactl") .args(["list", "short", "sink-inputs"]) @@ -269,7 +283,39 @@ fn sink_inputs() -> Result> { Ok(inputs) } +#[cfg(any(target_os = "linux", target_os = "freebsd"))] fn has_entry(list: &str, name: &str) -> bool { list.lines() .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> { + Ok(None) + } + + pub fn get_default_sink() -> Option { + None + } + + pub fn set_default_sink(_name: &str) {} + + pub fn wait_default_sink(_name: &str) {} + + pub fn keep_others_off(stop: Arc, _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(|| {}) + } +} diff --git a/src/config.rs b/src/config.rs index e895ee8..122d031 100644 --- a/src/config.rs +++ b/src/config.rs @@ -101,7 +101,8 @@ impl AppConfig { virtual_mic: if cli.no_virtual_mic { false } 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), } diff --git a/src/main.rs b/src/main.rs index dbb65d3..85ee171 100644 --- a/src/main.rs +++ b/src/main.rs @@ -104,7 +104,9 @@ fn main() -> Result<()> { "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:#}"), } }