Files
vmic/src/commands/create.rs
Overlord 493f91bc28 Add support for --loopback-no-mix flag and topology synchronization
- Introduced the `--loopback-no-mix` flag to control 2-node vs 4-node loopback topologies.
- Updated topology handling logic to recompute and migrate as needed based on loopback flags.
- Enhanced database schema to include `loopback_no_mix` with automatic migration for backward compatibility.
- Refactored `create`, `edit`, and `route` commands to integrate new topology management capabilities.
- Improved `list` command output to include architecture details.
2026-08-11 15:33:36 +02:00

81 lines
2.9 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 stage = loopback::create_simple_stage(&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)."));
}
// A fresh vmic always starts as Simple2Node - no source can be mixed in
// yet, so there's nothing for a 4-node split to isolate (see
// commands::topology). mid_name/mix_name are still stored even though no
// live node backs them yet: cheap, and needed if this vmic later
// upgrades. mix_node_id/mix_pid stay at their Default (None).
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(stage.sink_node_id),
sink_pid: Some(stage.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)
}