From d7035b3923f5b2b986a8aae164ea3ecea9525a11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Laitl?= Date: Wed, 19 Nov 2025 22:46:04 +0100 Subject: [PATCH 1/2] Cope with "UNK" audio.channel I could not get pipeswitch to match some ports, and I was getting these errors in logs: ``` Nov 20 06:28:56 hostname pipeswitchd[21760]: 2025-11-20T06:28:56+09:00 - ERROR - channel not valid: UNK ``` It turned out the port of our application had `audio.channel` set to `UNK` for some reason: ```json { "id": 156, "type": "PipeWire:Interface:Port", "version": 3, "permissions": [ "r", "x", "m" ], "info": { "direction": "output", "change-mask": [ "props", "params" ], "props": { "audio.channel": "UNK", "format.dsp": "32 bit float mono audio", "node.id": 146, "object.id": 156, "object.path": "tonari-main-playback:output_3", "object.serial": 1450, "port.alias": "tonari-main-playback:output_4", "port.direction": "out", "port.group": "stream.0", "port.id": 3, "port.name": "output_4" }, "params": { "EnumFormat": [ { "mediaType": "audio", "mediaSubtype": "dsp", "format": "F32P" } ], "Meta": [ { "type": "Header", "size": 32 } ], "IO": [ { "id": "Buffers", "size": 8 }, { "id": "AsyncBuffers", "size": 8 } ], "Format": [ ], "Buffers": [ ], "Latency": [ { "direction": "Input", "minQuantum": 0.000000, "maxQuantum": 0.000000, "minRate": 0, "maxRate": 0, "minNs": 0, "maxNs": 0 }, { "direction": "Output", "minQuantum": 0.000000, "maxQuantum": 0.000000, "minRate": 0, "maxRate": 0, "minNs": 0, "maxNs": 0 } ], "Tag": [ ] } } } ``` Instead of returning Err, return Ok(None), which activates the fallback one level higher, which does the right thing. --- pipeswitch-lib/src/pw/types.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/pipeswitch-lib/src/pw/types.rs b/pipeswitch-lib/src/pw/types.rs index b8aea0c..f1f340b 100644 --- a/pipeswitch-lib/src/pw/types.rs +++ b/pipeswitch-lib/src/pw/types.rs @@ -47,20 +47,22 @@ impl Channel { fn from_channel>(input: Option) -> Result, PipewireError> { if let Some(input) = input { let input = input.into(); - Ok(Some(match input.as_str() { - "FL" => Channel::Left, - "FR" => Channel::Right, - "MONO" => Channel::Mono, + Ok(match input.as_str() { + "FL" => Some(Channel::Left), + "FR" => Some(Channel::Right), + "MONO" => Some(Channel::Mono), + // Unknown, produce empty but successful result for fallback to port.id + "UNK" => None, i if i.starts_with("AUX") => { let num = i.split_at("AUX".len()).1.parse()?; - match num { + Some(match num { 0 => Channel::Left, 1 => Channel::Right, _ => Channel::Aux(num), - } + }) } _ => Err(PipewireError::InvalidChannel(input))?, - })) + }) } else { Ok(None) } From e54b7d2ea8e80cff676bd59a24d8236a6e017593 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Laitl?= Date: Thu, 20 Nov 2025 10:45:56 +0100 Subject: [PATCH 2/2] Make Channel a wrapper around String, remove fallibility Per https://github.com/Teascade/pipeswitch/pull/17#issuecomment-3556898256 --- pipeswitch-lib/src/pw/mod.rs | 2 -- pipeswitch-lib/src/pw/types.rs | 52 +++++++++++----------------------- 2 files changed, 17 insertions(+), 37 deletions(-) diff --git a/pipeswitch-lib/src/pw/mod.rs b/pipeswitch-lib/src/pw/mod.rs index ceed6a5..41a1f02 100644 --- a/pipeswitch-lib/src/pw/mod.rs +++ b/pipeswitch-lib/src/pw/mod.rs @@ -24,8 +24,6 @@ pub enum PipewireError { MissingProps(u32, ObjectType, HashMap), #[error("direction not valid: {0}")] InvalidDirection(String), - #[error("channel not valid: {0}")] - InvalidChannel(String), #[error("error with core pipewire interface: {0}")] PipewireInterfaceError(#[from] pipewire::Error), #[error("tried to delete a global object that was not yet registered: {0}")] diff --git a/pipeswitch-lib/src/pw/types.rs b/pipeswitch-lib/src/pw/types.rs index f1f340b..8670d4a 100644 --- a/pipeswitch-lib/src/pw/types.rs +++ b/pipeswitch-lib/src/pw/types.rs @@ -36,44 +36,26 @@ impl Direction { } #[derive(Debug, Clone, PartialEq, Eq)] -pub enum Channel { - Left, - Right, - Mono, - Aux(u32), +pub struct Channel { + short_name: String, } impl Channel { - fn from_channel>(input: Option) -> Result, PipewireError> { - if let Some(input) = input { - let input = input.into(); - Ok(match input.as_str() { - "FL" => Some(Channel::Left), - "FR" => Some(Channel::Right), - "MONO" => Some(Channel::Mono), - // Unknown, produce empty but successful result for fallback to port.id - "UNK" => None, - i if i.starts_with("AUX") => { - let num = i.split_at("AUX".len()).1.parse()?; - Some(match num { - 0 => Channel::Left, - 1 => Channel::Right, - _ => Channel::Aux(num), - }) - } - _ => Err(PipewireError::InvalidChannel(input))?, - }) - } else { - Ok(None) - } + fn from_channel>(input: Option) -> Option { + Some(Self { + short_name: input?.into(), + }) } - fn from_portid(input: u32) -> Result { - Ok(match input { - 0 => Channel::Left, - 1 => Channel::Right, - _ => Channel::Aux(input), - }) + fn from_portid(input: u32) -> Self { + Self { + // TODO(strohel): wouldn't it be better to name them all AUX{n}? + short_name: match input { + 0 => "FL".to_string(), + 1 => "FR".to_string(), + _ => format!("AUX{input}"), + }, + } } } @@ -115,8 +97,8 @@ impl Port { path: get_prop(*OBJECT_PATH), node_id: get_prop_or(*NODE_ID)?.parse()?, dsp: get_prop(*FORMAT_DSP), - channel: Channel::from_channel(get_prop(*AUDIO_CHANNEL))? - .unwrap_or(Channel::from_portid(local_port_id)?), + channel: Channel::from_channel(get_prop(*AUDIO_CHANNEL)) + .unwrap_or(Channel::from_portid(local_port_id)), name: get_prop_or(*PORT_NAME)?, direction: Direction::from(get_prop_or(*PORT_DIRECTION)?)?, alias: get_prop_or(*PORT_ALIAS)?,