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.
This commit is contained in:
@@ -51,20 +51,100 @@ pub struct StageHandles {
|
||||
pub mix_node_id: u32,
|
||||
}
|
||||
|
||||
/// Creates the two chained loopback stages as detached `pw-loopback`
|
||||
/// subprocesses (stdin is `/dev/null`, never waited on, so the OS
|
||||
/// reparents them to init on exit), then links stage 1 into stage 2
|
||||
/// (mid -> mix).
|
||||
///
|
||||
/// apps -> sink =(stage1)=> mid -> mix =(stage2)=> source -> recorders
|
||||
/// ^ hw mic joins here (route --source)
|
||||
pub fn create_stages(graph: &PwGraph, names: &NodeNames) -> Result<StageHandles> {
|
||||
let fmt = format!("audio.rate={SAMPLE_RATE} audio.position=[FL FR]");
|
||||
/// OS pid and sink PipeWire node id produced by [`create_simple_stage`].
|
||||
pub struct SingleStageHandles {
|
||||
pub pid: u32,
|
||||
pub sink_node_id: u32,
|
||||
}
|
||||
|
||||
fn log_file_for(names: &NodeNames) -> Result<(std::path::PathBuf, std::fs::File)> {
|
||||
let log_path = std::env::temp_dir().join(format!(
|
||||
"vmic_{}.log",
|
||||
names.sink.trim_start_matches("vmic_").trim_end_matches("_sink")
|
||||
));
|
||||
let log_file = std::fs::OpenOptions::new().create(true).append(true).open(&log_path)?;
|
||||
Ok((log_path, log_file))
|
||||
}
|
||||
|
||||
/// Creates the baseline single-stage loopback: apps -> sink =(pw-loopback)=>
|
||||
/// source_name -> recorders. Used whenever a 4-node split isn't needed (see
|
||||
/// `commands::topology::desired_topology`) - the common case, and the reason
|
||||
/// a fresh vmic only ever costs one process, not two.
|
||||
///
|
||||
/// `exclude_sink_ids`/`exclude_source_ids` matter only when this is called as
|
||||
/// part of a topology migration (`commands::topology::migrate`), where a
|
||||
/// same-named node from the topology being replaced may still be alive at
|
||||
/// spawn time; see `PwGraph::wait_for_new_node`. Pass empty slices for a
|
||||
/// brand-new vmic.
|
||||
pub fn create_simple_stage(
|
||||
graph: &PwGraph,
|
||||
names: &NodeNames,
|
||||
exclude_sink_ids: &[u32],
|
||||
exclude_source_ids: &[u32],
|
||||
) -> Result<SingleStageHandles> {
|
||||
let fmt = format!("audio.rate={SAMPLE_RATE} audio.position=[FL FR]");
|
||||
let (log_path, log_file) = log_file_for(names)?;
|
||||
|
||||
let mut child = spawn_loopback_stage(
|
||||
&format!(
|
||||
"media.class=Audio/Sink node.name={} node.description={} node.virtual=false {fmt}",
|
||||
names.sink, names.sink_desc
|
||||
),
|
||||
&format!(
|
||||
"media.class=Audio/Source node.name={} node.description={} node.virtual=false {fmt}",
|
||||
names.source, names.source_desc
|
||||
),
|
||||
&log_file,
|
||||
)?;
|
||||
|
||||
// Give the process a moment to fail fast (e.g. a name collision) before
|
||||
// trusting it.
|
||||
std::thread::sleep(STAGE_STARTUP_CHECK);
|
||||
if child.try_wait()?.is_some() {
|
||||
let _ = child.kill();
|
||||
return Err(VmicError::PipeWire(format!(
|
||||
"pw-loopback exited immediately, check '{}'.",
|
||||
log_path.display()
|
||||
)));
|
||||
}
|
||||
|
||||
let pid = child.id();
|
||||
|
||||
// Unlike create_stages, there's no link_stage retry loop to implicitly
|
||||
// wait through - both nodes must be waited for explicitly.
|
||||
let sink_node_id = (|| -> Result<u32> {
|
||||
let sink_node = graph.wait_for_new_node(&names.sink, exclude_sink_ids, NODE_WAIT_TIMEOUT)?;
|
||||
graph.wait_for_new_node(&names.source, exclude_source_ids, NODE_WAIT_TIMEOUT)?;
|
||||
Ok(sink_node.id)
|
||||
})();
|
||||
|
||||
let sink_node_id = match sink_node_id {
|
||||
Ok(id) => id,
|
||||
Err(e) => {
|
||||
let _ = child.kill();
|
||||
return Err(e);
|
||||
}
|
||||
};
|
||||
|
||||
Ok(SingleStageHandles { pid, sink_node_id })
|
||||
}
|
||||
|
||||
/// Creates the two chained loopback stages as detached `pw-loopback`
|
||||
/// subprocesses (stdin is `/dev/null`, never waited on, so the OS
|
||||
/// reparents them to init on exit), then links stage 1 into stage 2
|
||||
/// (mid -> mix). Used only when a mixed-in source needs to be kept out of
|
||||
/// the self-monitor loopback (see `commands::topology`).
|
||||
///
|
||||
/// apps -> sink =(stage1)=> mid -> mix =(stage2)=> source -> recorders
|
||||
/// ^ hw mic joins here (route --source)
|
||||
///
|
||||
/// `exclude_sink_ids` is the same migration-safety mechanism as
|
||||
/// `create_simple_stage`'s - `mid`/`mix` never need it, since those names are
|
||||
/// exclusive to this topology and can't already exist across a migration
|
||||
/// boundary.
|
||||
pub fn create_stages(graph: &PwGraph, names: &NodeNames, exclude_sink_ids: &[u32]) -> Result<StageHandles> {
|
||||
let fmt = format!("audio.rate={SAMPLE_RATE} audio.position=[FL FR]");
|
||||
let (log_path, log_file) = log_file_for(names)?;
|
||||
|
||||
let mut sink_child = spawn_loopback_stage(
|
||||
&format!(
|
||||
@@ -106,7 +186,7 @@ pub fn create_stages(graph: &PwGraph, names: &NodeNames) -> Result<StageHandles>
|
||||
let mix_pid = mix_child.id();
|
||||
|
||||
let stage_ids = (|| -> Result<(u32, u32)> {
|
||||
let sink_node = graph.wait_for_node(&names.sink, NODE_WAIT_TIMEOUT)?;
|
||||
let sink_node = graph.wait_for_new_node(&names.sink, exclude_sink_ids, NODE_WAIT_TIMEOUT)?;
|
||||
let mix_node = graph.wait_for_node(&names.mix, NODE_WAIT_TIMEOUT)?;
|
||||
link_stage(graph, &names.mid, &names.mix, LINK_RETRY_TIMEOUT)?;
|
||||
Ok((sink_node.id, mix_node.id))
|
||||
|
||||
Reference in New Issue
Block a user