From cf6e2585482f695c5124c2f61969a4ce79cfe530 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 10 Jul 2026 22:36:43 +0200 Subject: [PATCH 1/2] fix(jack): allow more channels than physical system ports for patching --- CHANGELOG.md | 1 + src/host/jack/stream.rs | 54 +++++++++-------------------------------- 2 files changed, 13 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 217cefb25..0c1328402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Timestamps now stay monotonic across device and graph changes. - **ALSA**: A nonzero but sub-millisecond stream timeout is no longer treated as a non-blocking poll. +- **JACK**: Streams with more channels than physical system ports no longer fail to build. - **PipeWire**: Streams for a specific device no longer auto-reroute if it disappears. - **visionOS**: The CoreAudio backend now builds. - **WASAPI**: Default device changes no longer report `DeviceChanged`, which wrongly implied the stream had rerouted automatically. diff --git a/src/host/jack/stream.rs b/src/host/jack/stream.rs index fc271354a..af24d88f9 100644 --- a/src/host/jack/stream.rs +++ b/src/host/jack/stream.rs @@ -158,32 +158,17 @@ impl Stream { }) } - /// Connect stream output ports to the standard JACK system playback ports. - /// Must be called after the client is activated. + /// Connects the stream's output ports to as many system playback ports as are available; + /// must be called after the client is activated. A stream may have more output channels + /// than physical ports (e.g. feeding a downstream JACK client); the surplus is simply left + /// unconnected for manual patching. /// - /// # Returns - /// - /// - `Ok(())` if every stream channel was connected to a system playback port. - /// - `Err` if there are fewer system playback ports than stream channels, or if any - /// individual port-connection call fails. - /// - /// On error, connections that were made before the failure are rolled back on a best-effort - /// basis so the JACK graph is left unchanged. + /// Returns `Err` only if an individual port-connection call fails, rolling back any + /// connections already made so the JACK graph is left unchanged. pub fn connect_to_system_outputs(&mut self) -> Result<(), Error> { let client = self.async_client.as_client(); let system_ports = client.ports(Some("system:playback_.*"), None, jack::PortFlags::empty()); - let n_our = self.output_port_names.len(); - let n_sys = system_ports.len(); - if n_sys < n_our { - return Err(Error::with_message( - ErrorKind::UnsupportedConfig, - format!( - "Only {n_sys} system playback port(s) available, but the stream has {n_our} output channel(s)" - ), - )); - } - // Connect outputs from this client to the system playback inputs. for (i, (our_port, system_port)) in self.output_port_names.iter().zip(&system_ports).enumerate() @@ -204,32 +189,17 @@ impl Stream { Ok(()) } - /// Connect stream input ports to the standard JACK system capture ports. - /// Must be called after the client is activated. + /// Connects the stream's input ports to as many system capture ports as are available; must + /// be called after the client is activated. A stream may have more input channels than + /// physical ports (e.g. sourced from an upstream JACK client); the surplus is simply left + /// unconnected for manual patching. /// - /// # Returns - /// - /// - `Ok(())` if every stream channel was connected to a system capture port. - /// - `Err` if there are fewer system capture ports than stream channels, or if any individual - /// port-connection call fails. - /// - /// On error, connections that were made before the failure are rolled back on a best-effort - /// basis so the JACK graph is left unchanged. + /// Returns `Err` only if an individual port-connection call fails, rolling back any + /// connections already made so the JACK graph is left unchanged. pub fn connect_to_system_inputs(&mut self) -> Result<(), Error> { let client = self.async_client.as_client(); let system_ports = client.ports(Some("system:capture_.*"), None, jack::PortFlags::empty()); - let n_our = self.input_port_names.len(); - let n_sys = system_ports.len(); - if n_sys < n_our { - return Err(Error::with_message( - ErrorKind::UnsupportedConfig, - format!( - "Only {n_sys} system capture port(s) available, but the stream has {n_our} input channel(s)" - ), - )); - } - // Connect inputs from system capture ports to this client. for (i, (system_port, our_port)) in system_ports.iter().zip(&self.input_port_names).enumerate() From 92f9ed6636bae67e39ca138d42400ab4bc5d22c7 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Jul 2026 13:48:02 +0200 Subject: [PATCH 2/2] fix(jack): no longer cap channel enumeration at physical port count --- CHANGELOG.md | 1 + src/host/jack/device.rs | 29 ++++++++++------------------- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c1328402..22c93b41c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Timestamps now stay monotonic across device and graph changes. - **ALSA**: A nonzero but sub-millisecond stream timeout is no longer treated as a non-blocking poll. - **JACK**: Streams with more channels than physical system ports no longer fail to build. +- **JACK**: Channel enumeration is no longer capped at the physical system port count. - **PipeWire**: Streams for a specific device no longer auto-reroute if it disappears. - **visionOS**: The CoreAudio backend now builds. - **WASAPI**: Default device changes no longer report `DeviceChanged`, which wrongly implied the stream had rerouted automatically. diff --git a/src/host/jack/device.rs b/src/host/jack/device.rs index 9052c7d42..d471b3497 100644 --- a/src/host/jack/device.rs +++ b/src/host/jack/device.rs @@ -15,12 +15,14 @@ use crate::{ const DEFAULT_NUM_CHANNELS: ChannelCount = 2; +// 64 = AES10 (MADI) maximum; JACK itself imposes no per-client channel limit. +const MAX_CHANNELS: ChannelCount = 64; + #[derive(Clone, Debug)] pub struct Device { name: String, sample_rate: SampleRate, buffer_size: SupportedBufferSize, - max_channels: ChannelCount, direction: DeviceDirection, start_server_automatically: bool, connect_ports_automatically: bool, @@ -40,22 +42,12 @@ impl Device { // making the stream. This is a hack due to the fact that the Client must be moved to // create the AsyncClient. let client = super::get_client(&name, client_options)?; - let port_pattern = match direction { - DeviceDirection::Input => "system:capture_.*", - DeviceDirection::Output => "system:playback_.*", - _ => { - return Err(Error::with_message( - ErrorKind::UnsupportedOperation, - format!("JACK does not support {direction:?} direction"), - )) - } - }; - let max_channels = client - .ports(Some(port_pattern), None, jack::PortFlags::empty()) - .len() - .try_into() - .unwrap_or(DEFAULT_NUM_CHANNELS) - .max(DEFAULT_NUM_CHANNELS); + if !matches!(direction, DeviceDirection::Input | DeviceDirection::Output) { + return Err(Error::with_message( + ErrorKind::UnsupportedOperation, + format!("JACK does not support {direction:?} direction"), + )); + } Ok(Self { // The name given to the client by JACK, could potentially be different from the name // supplied e.g. if there is a name collision @@ -65,7 +57,6 @@ impl Device { min: client.buffer_size(), max: client.buffer_size(), }, - max_channels, direction, start_server_automatically, connect_ports_automatically, @@ -126,7 +117,7 @@ impl Device { Ok(f) => f, }; - (1..=self.max_channels) + (1..=MAX_CHANNELS) .map(|channels| SupportedStreamConfigRange { channels, min_sample_rate: f.sample_rate,