- Added `Cargo.toml` to define dependencies and project metadata. - Implemented core CLI functionality for managing virtual microphones (`create`, `edit`, `delete`, `list`, `route`, and `wipe` commands) using `clap`. - Integrated `rusqlite` for persistent state storage and `pipewire` to manage PipeWire nodes. - Ensured graceful handling of feedback loops and system defaults during virtual mic creation and deletion. - Added error handling via `thiserror` for cleaner error definitions.
78 lines
2.6 KiB
Rust
78 lines
2.6 KiB
Rust
use crate::cli::CreateArgs;
|
|
use crate::error::{Result, VmicError};
|
|
use crate::pw::{loopback, PwGraph};
|
|
use crate::state::{self, VmicState};
|
|
use crate::ui;
|
|
use std::process::Command;
|
|
|
|
pub fn run(args: CreateArgs) -> Result<()> {
|
|
state::require_valid_name(&args.name)?;
|
|
let name = state::normalize(&args.name);
|
|
let conn = state::open_db()?;
|
|
|
|
if VmicState::exists(&conn, &name)? {
|
|
return Err(VmicError::AlreadyExists(name));
|
|
}
|
|
|
|
let names = loopback::NodeNames::for_vmic(&name);
|
|
let graph = PwGraph::connect()?;
|
|
|
|
// pw-loopback may steal the default sink/source; restore them afterward.
|
|
let orig_sink = default_output("get-default-sink");
|
|
let orig_source = default_output("get-default-source");
|
|
|
|
let stages = loopback::create_stages(&graph, &names)?;
|
|
|
|
if let Some(sink) = &orig_sink {
|
|
let _ = Command::new("pactl").args(["set-default-sink", sink]).status();
|
|
}
|
|
if let Some(source) = &orig_source {
|
|
let _ = Command::new("pactl").args(["set-default-source", source]).status();
|
|
}
|
|
|
|
// Insurance: a sink monitor feeding any vmic node is a feedback ring;
|
|
// it should never exist, but tear it down if one appears anyway.
|
|
let prefix = format!("vmic_{name}_");
|
|
let removed = graph.break_feedback_rings(&prefix)?;
|
|
if removed > 0 {
|
|
ui::warn(&format!("removed {removed} unexpected monitor feedback link(s)."));
|
|
}
|
|
|
|
let mut state = VmicState {
|
|
name: name.clone(),
|
|
sink_name: names.sink.clone(),
|
|
mid_name: names.mid,
|
|
mix_name: names.mix,
|
|
source_name: names.source.clone(),
|
|
sink_node_id: Some(stages.sink_node_id),
|
|
mix_node_id: Some(stages.mix_node_id),
|
|
sink_pid: Some(stages.sink_pid as i64),
|
|
mix_pid: Some(stages.mix_pid as i64),
|
|
..Default::default()
|
|
};
|
|
|
|
if args.loopback {
|
|
let loop_id = loopback::enable_self_monitor(&graph, &names.sink)?;
|
|
state.loopback_id = Some(loop_id);
|
|
}
|
|
|
|
state.save(&conn)?;
|
|
|
|
ui::ok(&format!("Created virtual microphone '{name}'."));
|
|
if let (Some(sink), Some(source)) = (&orig_sink, &orig_source) {
|
|
ui::info(&format!(" Default Output/Input kept intact ('{sink}' / '{source}')."));
|
|
}
|
|
println!(" In the source app, set output device to:");
|
|
println!(" '{}'", ui::blue(&names.sink));
|
|
println!(" In the receiving app, set input/mic device to:");
|
|
println!(" '{}'", ui::blue(&names.source));
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn default_output(subcommand: &str) -> Option<String> {
|
|
let out = Command::new("pactl").arg(subcommand).output().ok()?;
|
|
let s = String::from_utf8_lossy(&out.stdout).trim().to_string();
|
|
(!s.is_empty()).then_some(s)
|
|
}
|