From f226abef3f4bdad38f355bc1d22c04771f504df7 Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Wed, 15 Jul 2026 22:35:38 +1200 Subject: [PATCH 1/7] refactor(hostid): Make all variants available on all platforms. + Added `host_id` sub-module, which contains the `HostId` non-exhaustive enum, and all associated implementations and definitions. + Added internal implementations in `platform::impl_platform_host`. + Added implementation of `TryFrom for Host`. * Fixed links broken by moving `host_from_id` and `available_hosts`. * Added additional error kind to `host_from_id`. - Removed all definitions and impls of `HostId` from `platform`. - Removed `__cpal_select_host_name` internal macro. - Removed `default_host` from all `platform_impl` modules. --- src/platform/host_id.rs | 195 +++++++++++++++++++++++++++++++ src/platform/mod.rs | 248 ++++++++++++---------------------------- 2 files changed, 269 insertions(+), 174 deletions(-) create mode 100644 src/platform/host_id.rs diff --git a/src/platform/host_id.rs b/src/platform/host_id.rs new file mode 100644 index 000000000..9b0550e88 --- /dev/null +++ b/src/platform/host_id.rs @@ -0,0 +1,195 @@ +use crate::platform; + +/// A unique identifier for each host supported by CPAL. +/// +/// Not all hosts in this enum are available at runtime, or are even supported +/// by the current platform. This can be checked with `is_available` or +/// `is_supported` respectively. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[non_exhaustive] +pub enum HostId { + AAudio, + Alsa, + Asio, + AudioWorklet, + CoreAudio, + Custom, + Jack, + Null, + PipeWire, + PulseAudio, + Wasapi, + WebAudio, +} + +impl HostId { + /// All hosts supported by CPAL on this platform. + pub const SUPPORTED_HOSTS: &[HostId] = { + // This is a hack to prevent rustdoc from referencing the + // implementation const in its output. + let _ = 1 + 2; + + super::SUPPORTED_HOSTS + }; + + /// Returns the human-readable host name. + pub const fn name(&self) -> &'static str { + match self { + HostId::AAudio => "AAudio", + HostId::Alsa => "ALSA", + HostId::Asio => "ASIO", + HostId::AudioWorklet => "AudioWorklet", + HostId::CoreAudio => "CoreAudio", + HostId::Custom => "Custom", + HostId::Jack => "JACK", + HostId::Null => "Null", + HostId::PipeWire => "PipeWire", + HostId::PulseAudio => "PulseAudio", + HostId::Wasapi => "WASAPI", + HostId::WebAudio => "WebAudio", + } + } + + /// Checks if the given `HostId` is supported on this platform. + pub const fn is_supported(&self) -> bool { + super::is_supported_impl(*self) + } + + /// Checks if the given `HostId` is currently available. + pub fn is_available(&self) -> bool { + super::is_available_impl(*self) + } + + /// Iterates over all the `HostId`s currently available on this platform. + /// + /// The availability check is performed when `next` is called, not when + /// this function is called. + pub fn available_hosts() -> AvailableHostsIter { + AvailableHostsIter(Self::SUPPORTED_HOSTS.iter()) + } +} + +impl Default for HostId { + fn default() -> Self { + super::default_host_id() + } +} + +impl std::fmt::Display for HostId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.name().to_ascii_lowercase()) + } +} + +impl std::str::FromStr for HostId { + type Err = crate::Error; + + /// Parse a host identifier from its string representation (e.g. `"alsa"`, + /// `"coreaudio"`). This conversion is case-insensitive. + /// + /// # Errors + /// + /// - [`ErrorKind::UnsupportedOperation`] if the string does not name a + /// valid `HostId. + /// + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + fn from_str(s: &str) -> Result { + macro_rules! match_str_case_insensitive { + ( + $s:expr => { + $( $l:literal => $e:expr, )* + _ => $f:expr $(,)? + } + ) => { + { + let s = $s; + + if false { unreachable!() } + + $( + else if $l.eq_ignore_ascii_case(s) { $e } + )* + + else { $f } + } + }; + } + + match_str_case_insensitive! { + s => { + "AAudio" => Ok(HostId::AAudio), + "ALSA" => Ok(HostId::Alsa), + "ASIO" => Ok(HostId::Asio), + "AudioWorklet" => Ok(HostId::AudioWorklet), + "CoreAudio" => Ok(HostId::CoreAudio), + "Custom" => Ok(HostId::Custom), + "JACK" => Ok(HostId::Jack), + "Null" => Ok(HostId::Null), + "PipeWire" => Ok(HostId::PipeWire), + "PulseAudio" => Ok(HostId::PulseAudio), + "WASAPI" => Ok(HostId::Wasapi), + "WebAudio" => Ok(HostId::WebAudio), + + _ => Err(crate::Error::with_message( + crate::ErrorKind::UnsupportedOperation, + format!("unknown host \"{s}\"") + )), + } + } + } +} + +impl TryFrom for platform::Host { + type Error = crate::Error; + + fn try_from(value: HostId) -> Result { + host_from_id(value) + } +} + +pub struct AvailableHostsIter(std::slice::Iter<'static, HostId>); + +impl Iterator for AvailableHostsIter { + type Item = HostId; + + fn next(&mut self) -> Option { + loop { + let host_id = self.0.next()?; + + if host_id.is_supported() { + return Some(*host_id); + } else { + continue; + } + } + } +} + +/// Produces a list of hosts that are currently available on the system. +pub fn available_hosts() -> Vec { + HostId::available_hosts().collect() +} + +/// The default host for the current compilation target platform. +pub fn default_host() -> crate::Host { + HostId::default() + .try_into() + .expect("the default host should always be available") +} + +/// Given a unique host identifier, initialise and produce the host if it is available. +/// +/// # Errors +/// +/// - [`ErrorKind::HostUnavailable`] if the host identified by `id` is not currently +/// reachable (e.g. the audio daemon is not running). +/// - [`ErrorKind::UnsupportedOperation`] if the host identified by `id` is not +/// supported by this configuration of CPAL. +/// - [`ErrorKind::BackendError`] for unclassifiable initialization failures. +/// +/// [`ErrorKind::HostUnavailable`]: crate::ErrorKind::HostUnavailable +/// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation +/// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError +pub fn host_from_id(id: HostId) -> Result { + crate::platform::host_from_id_impl(id) +} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 5b2d5aeb6..f34613e20 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -4,6 +4,10 @@ //! type and its associated [`Device`], [`Stream`] and other associated types. These //! types are useful in the case that users require switching between audio host APIs at runtime. +mod host_id; + +pub use host_id::{AvailableHostsIter, HostId, available_hosts, default_host, host_from_id}; + pub use self::platform_impl::*; #[cfg(all( @@ -70,6 +74,35 @@ macro_rules! impl_platform_host { )* ]; + pub(crate) const SUPPORTED_HOSTS: &[HostId] = &[ + $( + $(#[cfg($feat)])? + HostId::$HostVariant, + )* + ]; + + pub(crate) const fn is_supported_impl(host_id: HostId) -> bool { + match host_id { + $( + $(#[cfg($feat)])? + HostId::$HostVariant => true, + )* + + _ => false, + } + } + + pub(crate) fn is_available_impl(host_id: HostId) -> bool { + match host_id { + $( + $(#[cfg($feat)])? + HostId::$HostVariant => <$Host as crate::traits::HostTrait>::is_available(), + )* + + _ => false, + } + } + /// The platform's dynamically dispatched `Host` type. /// /// An instance of this `Host` type may represent one of the `Host`s available @@ -79,6 +112,9 @@ macro_rules! impl_platform_host { /// /// This type may be constructed via the [`host_from_id`] function. [`HostId`]s may /// be acquired via the [`ALL_HOSTS`] const, and the [`available_hosts`] function. + /// + /// [`host_from_id`]: super::host_from_id + /// [`available_hosts`]: super::available_hosts pub struct Host(HostInner); /// The `Device` implementation associated with the platform's dynamically dispatched @@ -105,60 +141,6 @@ macro_rules! impl_platform_host { #[derive(Clone)] pub struct SupportedOutputConfigs(SupportedOutputConfigsInner); - /// Unique identifier for available hosts on the platform. - /// - /// Only the hosts supported by the current platform are available as enum variants. - /// For cross-platform code that needs to handle hosts from other platforms, - /// use the string representation via [`std::fmt::Display`]/[`std::str::FromStr`]. - /// - /// # Available Host Strings - /// - /// For cross-platform matching, these host strings are available: - /// - /// - `"aaudio"` - Android Audio - /// - `"alsa"` - Advanced Linux Sound Architecture - /// - `"asio"` - ASIO - /// - `"audioworklet"` - Audio Worklet - /// - `"coreaudio"` - CoreAudio - /// - `"custom"` - Custom host (requires `custom` feature) - /// - `"jack"` - JACK Audio Connection Kit - /// - `"null"` - Null host - /// - `"wasapi"` - Windows Audio Session API - /// - `"webaudio"` - Web Audio API - /// - /// # Cross-Platform Example - /// - /// ``` - /// use cpal::HostId; - /// use std::str::FromStr; - /// - /// fn handle_host_string(host_string: &str) { - /// // String matching works on all platforms - /// match host_string { - /// "alsa" => println!("ALSA host"), - /// "coreaudio" => println!("CoreAudio host"), - /// "jack" => println!("JACK host"), - /// "wasapi" => println!("WASAPI host"), - /// "asio" => println!("ASIO host"), - /// "aaudio" => println!("AAudio host"), - /// _ => println!("Other host"), - /// } - /// - /// // Parse host string (may fail if host is not available on this platform) - /// if let Ok(host_id) = HostId::from_str(host_string) { - /// println!("Successfully parsed: {}", host_id); - /// } - /// } - /// ``` - #[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] - pub enum HostId { - $( - $(#[cfg($feat)])? - $(#[cfg_attr(docsrs, doc(cfg($feat)))])? - $HostVariant, - )* - } - /// Contains a platform-specific [`Device`] implementation. #[doc(hidden)] #[derive(Clone)] @@ -213,54 +195,6 @@ macro_rules! impl_platform_host { )* } - impl HostId { - /// Returns the human-readable host name. - pub fn name(&self) -> &'static str { - match self { - $( - $(#[cfg($feat)])? - HostId::$HostVariant => __cpal_select_host_name!($HostVariant, $($HostName)?), - )* - } - } - } - - impl std::fmt::Display for HostId { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.name().to_ascii_lowercase()) - } - } - - impl std::str::FromStr for HostId { - type Err = crate::Error; - - /// Parse a host identifier from its string representation (e.g. `"alsa"`, - /// `"coreaudio"`). - /// - /// The comparison is case-insensitive. Only hosts compiled in for the current platform - /// are recognized; a host string that is valid on another platform is still an error - /// here. - /// - /// # Errors - /// - /// - [`ErrorKind::UnsupportedOperation`] if the string does not name a host available - /// on this platform. - /// - /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation - fn from_str(s: &str) -> Result { - $( - $(#[cfg($feat)])? - if HostId::$HostVariant.name().eq_ignore_ascii_case(s) { - return Ok(HostId::$HostVariant); - } - )* - Err(crate::Error::with_message( - crate::ErrorKind::UnsupportedOperation, - format!("host \"{s}\" is not supported on this platform"), - )) - } - } - impl Devices { /// Returns a reference to the underlying platform-specific [`DevicesInner`]. pub fn as_inner(&self) -> &DevicesInner { &self.0 } @@ -808,29 +742,20 @@ macro_rules! impl_platform_host { } )* - /// Produces a list of hosts that are currently available on the system. - pub fn available_hosts() -> Vec { - let mut host_ids = vec![]; - $( - $(#[cfg($feat)])? - if <$Host as crate::traits::HostTrait>::is_available() { - host_ids.push(HostId::$HostVariant); - } - )* - host_ids - } - /// Given a unique host identifier, initialise and produce the host if it is available. /// /// # Errors /// /// - [`ErrorKind::HostUnavailable`] if the host identified by `id` is not currently /// reachable (e.g. the audio daemon is not running). + /// - [`ErrorKind::UnsupportedOperation`] if the host identified by `id` is not + /// supported by this configuration of CPAL. /// - [`ErrorKind::BackendError`] for unclassifiable initialization failures. /// /// [`ErrorKind::HostUnavailable`]: crate::ErrorKind::HostUnavailable + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation /// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError - pub fn host_from_id(id: HostId) -> Result { + pub(crate) fn host_from_id_impl(id: HostId) -> Result { match id { $( $(#[cfg($feat)])? @@ -840,26 +765,19 @@ macro_rules! impl_platform_host { .map(Host::from) } )* + + _ => Err(crate::Error::new(crate::ErrorKind::UnsupportedOperation)), } } impl Default for Host { fn default() -> Host { - default_host() + super::default_host() } } }; } -macro_rules! __cpal_select_host_name { - ($variant:ident, $name:literal) => { - $name - }; - ($variant:ident,) => { - stringify!($variant) - }; -} - #[cfg(any( target_os = "linux", target_os = "dragonfly", @@ -874,6 +792,9 @@ mod platform_impl { use crate::host::pipewire::Host as PipeWireHost; #[cfg(feature = "pulseaudio")] use crate::host::pulseaudio::Host as PulseAudioHost; + + use crate::platform::HostId; + impl_platform_host!( #[cfg(feature = "pipewire")] PipeWire => PipeWireHost, #[cfg(feature = "pulseaudio")] PulseAudio => PulseAudioHost, @@ -882,23 +803,14 @@ mod platform_impl { #[cfg(feature = "custom")] Custom => super::CustomHost, ); - /// The default host for the current compilation target platform. - pub fn default_host() -> Host { - #[cfg(feature = "pipewire")] - if ::is_available() { - if let Ok(host) = PipeWireHost::new() { - return host.into(); - } - } - #[cfg(feature = "pulseaudio")] - if ::is_available() { - if let Ok(host) = PulseAudioHost::new() { - return host.into(); - } + pub(crate) fn default_host_id() -> HostId { + if HostId::PipeWire.is_available() { + HostId::PipeWire + } else if HostId::PulseAudio.is_available() { + HostId::PulseAudio + } else { + HostId::Alsa } - AlsaHost::new() - .expect("the default host should always be available") - .into() } } @@ -908,17 +820,16 @@ mod platform_impl { use super::JackHost; use crate::host::coreaudio::Host as CoreAudioHost; + use crate::platform::HostId; + impl_platform_host!( CoreAudio => CoreAudioHost, #[cfg(all(feature = "jack", target_os = "macos"))] Jack "JACK" => JackHost, #[cfg(feature = "custom")] Custom => super::CustomHost ); - /// The default host for the current compilation target platform. - pub fn default_host() -> Host { - CoreAudioHost::new() - .expect("the default host should always be available") - .into() + pub(crate) fn default_host_id() -> HostId { + HostId::CoreAudio } } @@ -933,25 +844,16 @@ mod platform_impl { use crate::host::webaudio::Host as WebAudioHost; use crate::traits::HostTrait as _; + use crate::platform::HostId; + impl_platform_host!( WebAudio => WebAudioHost, #[cfg(all(feature = "audioworklet", target_feature = "atomics"))] AudioWorklet => AudioWorkletHost, #[cfg(feature = "custom")] Custom => super::CustomHost ); - /// The default host for the current compilation target platform. - /// - /// # Panics - /// - /// Panics if called outside a Window context (e.g. from a Web Worker or Service Worker), - /// where `AudioContext` is unavailable. - pub fn default_host() -> Host { - assert!( - WebAudioHost::is_available(), - "WebAudio is not available in this context; \ - AudioContext requires a Window (not a Worker or Service Worker)" - ); - WebAudioHost::new().unwrap().into() + pub(crate) fn default_host_id() -> HostId { + HostId::WebAudio } } @@ -963,6 +865,8 @@ mod platform_impl { use crate::host::asio::Host as AsioHost; use crate::host::wasapi::Host as WasapiHost; + use crate::platform::HostId; + impl_platform_host!( #[cfg(feature = "asio")] Asio "ASIO" => AsioHost, Wasapi "WASAPI" => WasapiHost, @@ -970,27 +874,24 @@ mod platform_impl { #[cfg(feature = "custom")] Custom => super::CustomHost, ); - /// The default host for the current compilation target platform. - pub fn default_host() -> Host { - WasapiHost::new() - .expect("the default host should always be available") - .into() + pub(crate) fn default_host_id() -> HostId { + HostId::Wasapi } } #[cfg(target_os = "android")] mod platform_impl { use crate::host::aaudio::Host as AAudioHost; + + use crate::platform::HostId; + impl_platform_host!( AAudio => AAudioHost, #[cfg(feature = "custom")] Custom => super::CustomHost ); - /// The default host for the current compilation target platform. - pub fn default_host() -> Host { - AAudioHost::new() - .expect("the default host should always be available") - .into() + pub(crate) fn default_host_id() -> HostId { + HostId::AAudio } } @@ -1011,15 +912,14 @@ mod platform_impl { mod platform_impl { use crate::host::null::Host as NullHost; + use crate::platform::HostId; + impl_platform_host!( Null => NullHost, #[cfg(feature = "custom")] Custom => super::CustomHost, ); - /// The default host for the current compilation target platform. - pub fn default_host() -> Host { - NullHost::new() - .expect("the default host should always be available") - .into() + pub(crate) fn default_host_id() -> HostId { + HostId::Null } } From af4a0107ca7be5c6d15c35d4b3501e92f90959b2 Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Wed, 15 Jul 2026 22:56:35 +1200 Subject: [PATCH 2/7] fix(hostid): Fixed docs and extraneous import. * Fixed hanging indentation on doc comment. - Removed import of `HostTrait` in `platform_impl` of `wasm-bindgen` target. --- src/platform/host_id.rs | 2 +- src/platform/mod.rs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform/host_id.rs b/src/platform/host_id.rs index 9b0550e88..507f61927 100644 --- a/src/platform/host_id.rs +++ b/src/platform/host_id.rs @@ -90,7 +90,7 @@ impl std::str::FromStr for HostId { /// # Errors /// /// - [`ErrorKind::UnsupportedOperation`] if the string does not name a - /// valid `HostId. + /// valid `HostId. /// /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation fn from_str(s: &str) -> Result { diff --git a/src/platform/mod.rs b/src/platform/mod.rs index f34613e20..2da7c10fa 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -842,7 +842,6 @@ mod platform_impl { #[cfg(all(feature = "audioworklet", target_feature = "atomics"))] use crate::host::audioworklet::Host as AudioWorkletHost; use crate::host::webaudio::Host as WebAudioHost; - use crate::traits::HostTrait as _; use crate::platform::HostId; From 8e2d1ce4c460490636c4475b668ab9a9169b333b Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Fri, 7 Aug 2026 01:56:57 +1200 Subject: [PATCH 3/7] fix(examples): No longer referencing to-be removed items. --- examples/beep.rs | 6 +++--- examples/enumerate.rs | 6 +++--- examples/feedback.rs | 4 ++-- examples/record_wav.rs | 6 +++--- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/examples/beep.rs b/examples/beep.rs index 2547ace65..b20000331 100644 --- a/examples/beep.rs +++ b/examples/beep.rs @@ -75,15 +75,15 @@ fn main() -> anyhow::Result<()> { // cargo run --release --example beep --features jack -- --jack let host = if opt.jack { jack_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features jack` is specified, and the platform is supported") } else if opt.pulseaudio { pulseaudio_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features pulseaudio` is specified, and the platform is supported") } else if opt.pipewire { pipewire_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features pipewire` is specified, and the platform is supported") } else { cpal::default_host() diff --git a/examples/enumerate.rs b/examples/enumerate.rs index 6e2680961..1efa8ab78 100644 --- a/examples/enumerate.rs +++ b/examples/enumerate.rs @@ -19,13 +19,13 @@ fn main() -> Result<(), anyhow::Error> { #[cfg(target_os = "linux")] let _silence_alsa_errors = alsa::Output::local_error_handler()?; - println!("Supported hosts:\n {:?}", cpal::ALL_HOSTS); - let available_hosts = cpal::available_hosts(); + println!("Supported hosts:\n {:?}", cpal::HostId::SUPPORTED_HOSTS); + let available_hosts = cpal::HostId::available_hosts().collect::>(); println!("Available hosts:\n {available_hosts:?}"); for host_id in available_hosts { println!("{}", host_id.name()); - let host = cpal::host_from_id(host_id)?; + let host = cpal::Host::try_from(host_id)?; let default_in = host .default_input_device() diff --git a/examples/feedback.rs b/examples/feedback.rs index e4f83ad5e..edd747212 100644 --- a/examples/feedback.rs +++ b/examples/feedback.rs @@ -72,11 +72,11 @@ fn main() -> anyhow::Result<()> { // cargo run --release --example beep --features jack -- --jack let host = if opt.jack { jack_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features jack` is specified, and the platform is supported") } else if opt.pulseaudio { pulseaudio_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features pulseaudio` is specified, and the platform is supported") } else { cpal::default_host() diff --git a/examples/record_wav.rs b/examples/record_wav.rs index 6946eb321..952f3adda 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -76,15 +76,15 @@ fn main() -> Result<(), anyhow::Error> { // cargo run --release --example record_wav --features jack -- --jack let host = if opt.jack { jack_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features jack` is specified, and the platform is supported") } else if opt.pulseaudio { pulseaudio_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features pulseaudio` is specified, and the platform is supported") } else if opt.pipewire { pipewire_host_id - .and_then(cpal::host_from_id) + .and_then(TryFrom::try_from) .expect("make sure `--features pipewire` is specified, and the platform is supported") } else { cpal::default_host() From 7c1da68ff5baddca60a0c0142b84ef375bd94b60 Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Fri, 7 Aug 2026 02:08:09 +1200 Subject: [PATCH 4/7] refactor: Removed `ALL_HOSTS`, `available_hosts`, and `host_from_id`. + Added migration path for removed items to `UPGRADING.md`. - Removed `ALL_HOSTS`, `available_hosts`, and `host_from_id`. --- UPGRADING.md | 2 ++ src/lib.rs | 4 ++-- src/platform/host_id.rs | 5 ----- src/platform/mod.rs | 9 +-------- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/UPGRADING.md b/UPGRADING.md index e9ff1f3f9..9b51316c7 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -11,6 +11,8 @@ This guide covers breaking changes requiring code updates. See [CHANGELOG.md](CH - [ ] Replace `InputCallbackInfo`/`OutputCallbackInfo` with `CallbackInfo`. - [ ] Replace `InputStreamTimestamp`/`OutputStreamTimestamp` with `StreamTimestamp`; `capture`/`playback` is now `device`. - [ ] Remove `ErrorKind::Xrun` match arms; read `CallbackInfo::xrun()` instead. +- [ ] Replace `available_hosts`, `host_from_id`, and `ALL_HOSTS` with `HostId::iter_available`, `Host::try_from`, and + `HostId::SUPPORTED_HOSTS` respectively. ## 1. `DeviceTrait` and `StreamTrait` require `Send + Sync` diff --git a/src/lib.rs b/src/lib.rs index 469e4e338..8085420a6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -213,8 +213,8 @@ pub use device_description::{ }; pub use error::*; pub use platform::{ - ALL_HOSTS, Device, Devices, Host, HostId, Stream, SupportedInputConfigs, - SupportedOutputConfigs, available_hosts, default_host, host_from_id, + Device, Devices, Host, HostId, Stream, SupportedInputConfigs, SupportedOutputConfigs, + default_host, }; pub use sample_format::{FromSample, I24, Sample, SampleFormat, SizedSample, U24}; #[cfg(all( diff --git a/src/platform/host_id.rs b/src/platform/host_id.rs index 507f61927..40097ff1f 100644 --- a/src/platform/host_id.rs +++ b/src/platform/host_id.rs @@ -165,11 +165,6 @@ impl Iterator for AvailableHostsIter { } } -/// Produces a list of hosts that are currently available on the system. -pub fn available_hosts() -> Vec { - HostId::available_hosts().collect() -} - /// The default host for the current compilation target platform. pub fn default_host() -> crate::Host { HostId::default() diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 2da7c10fa..a6e93cf2c 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -6,7 +6,7 @@ mod host_id; -pub use host_id::{AvailableHostsIter, HostId, available_hosts, default_host, host_from_id}; +pub use host_id::{AvailableHostsIter, HostId, default_host, host_from_id}; pub use self::platform_impl::*; @@ -67,13 +67,6 @@ pub use crate::host::custom::{Device as CustomDevice, Host as CustomHost, Stream macro_rules! impl_platform_host { ($($(#[cfg($feat: meta)])? $HostVariant:ident $($HostName:literal)? => $Host:ty),* $(,)?) => { /// All hosts supported by CPAL on this platform. - pub const ALL_HOSTS: &'static [HostId] = &[ - $( - $(#[cfg($feat)])? - HostId::$HostVariant, - )* - ]; - pub(crate) const SUPPORTED_HOSTS: &[HostId] = &[ $( $(#[cfg($feat)])? From 81a19a9980a82be5028aa618460ea1a84c06a646 Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Fri, 7 Aug 2026 02:18:41 +1200 Subject: [PATCH 5/7] fix: `HostId` now no longer lowercases its `Display` output. * `HostId::display` no longer outputs as lowercased. * `HostId::parse` now uses the mixed-case host id forms in the documentation. The error message is also slightly clarified. * `TryFrom for Host` is now the implementation of `host_from_id`, as opposed to vice-versa. --- src/platform/host_id.rs | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/platform/host_id.rs b/src/platform/host_id.rs index 40097ff1f..b719048b5 100644 --- a/src/platform/host_id.rs +++ b/src/platform/host_id.rs @@ -77,15 +77,15 @@ impl Default for HostId { impl std::fmt::Display for HostId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_str(&self.name().to_ascii_lowercase()) + f.write_str(self.name()) } } impl std::str::FromStr for HostId { type Err = crate::Error; - /// Parse a host identifier from its string representation (e.g. `"alsa"`, - /// `"coreaudio"`). This conversion is case-insensitive. + /// Parse a host identifier from its string representation (e.g. `"ALSA"`, + /// `"CoreAudio"`). This conversion is case-insensitive. /// /// # Errors /// @@ -132,7 +132,7 @@ impl std::str::FromStr for HostId { _ => Err(crate::Error::with_message( crate::ErrorKind::UnsupportedOperation, - format!("unknown host \"{s}\"") + format!("unknown host string \"{s}\"") )), } } @@ -142,8 +142,21 @@ impl std::str::FromStr for HostId { impl TryFrom for platform::Host { type Error = crate::Error; + /// Given a unique host identifier, initialise and produce the host if it is available. + /// + /// # Errors + /// + /// - [`ErrorKind::HostUnavailable`] if the host identified by `id` is not currently + /// reachable (e.g. the audio daemon is not running). + /// - [`ErrorKind::UnsupportedOperation`] if the host identified by `id` is not + /// supported by this configuration of CPAL. + /// - [`ErrorKind::BackendError`] for unclassifiable initialization failures. + /// + /// [`ErrorKind::HostUnavailable`]: crate::ErrorKind::HostUnavailable + /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation + /// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError fn try_from(value: HostId) -> Result { - host_from_id(value) + crate::platform::host_from_id_impl(value) } } @@ -156,7 +169,7 @@ impl Iterator for AvailableHostsIter { loop { let host_id = self.0.next()?; - if host_id.is_supported() { + if host_id.is_available() { return Some(*host_id); } else { continue; @@ -186,5 +199,5 @@ pub fn default_host() -> crate::Host { /// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation /// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError pub fn host_from_id(id: HostId) -> Result { - crate::platform::host_from_id_impl(id) + crate::Host::try_from(id) } From 64d35ed05cebeb821dd2aba1c608948dd6c1b85b Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Fri, 7 Aug 2026 03:56:36 +1200 Subject: [PATCH 6/7] fix: Actually remove `host_from_id`. + Added documentation to `HostId` pointing to the `TryFrom` implementation to get a `Host`. - Removed `host_from_id`. --- src/platform/host_id.rs | 27 ++++++++++----------------- src/platform/mod.rs | 2 +- 2 files changed, 11 insertions(+), 18 deletions(-) diff --git a/src/platform/host_id.rs b/src/platform/host_id.rs index b719048b5..030353f9c 100644 --- a/src/platform/host_id.rs +++ b/src/platform/host_id.rs @@ -5,6 +5,16 @@ use crate::platform; /// Not all hosts in this enum are available at runtime, or are even supported /// by the current platform. This can be checked with `is_available` or /// `is_supported` respectively. +/// +/// # Getting a Host +/// If you have a `HostId` that you would like to turn into an instance of a [`Host`], +/// you can use the `TryFrom` implementation of [`Host`]. +/// +/// ```ignore +/// let host = Host::try_from(HostId::Jack); +/// ``` +/// +/// [`Host`]: crate::Host #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum HostId { @@ -184,20 +194,3 @@ pub fn default_host() -> crate::Host { .try_into() .expect("the default host should always be available") } - -/// Given a unique host identifier, initialise and produce the host if it is available. -/// -/// # Errors -/// -/// - [`ErrorKind::HostUnavailable`] if the host identified by `id` is not currently -/// reachable (e.g. the audio daemon is not running). -/// - [`ErrorKind::UnsupportedOperation`] if the host identified by `id` is not -/// supported by this configuration of CPAL. -/// - [`ErrorKind::BackendError`] for unclassifiable initialization failures. -/// -/// [`ErrorKind::HostUnavailable`]: crate::ErrorKind::HostUnavailable -/// [`ErrorKind::UnsupportedOperation`]: crate::ErrorKind::UnsupportedOperation -/// [`ErrorKind::BackendError`]: crate::ErrorKind::BackendError -pub fn host_from_id(id: HostId) -> Result { - crate::Host::try_from(id) -} diff --git a/src/platform/mod.rs b/src/platform/mod.rs index a6e93cf2c..9dfe45526 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -6,7 +6,7 @@ mod host_id; -pub use host_id::{AvailableHostsIter, HostId, default_host, host_from_id}; +pub use host_id::{AvailableHostsIter, HostId, default_host}; pub use self::platform_impl::*; From 833fd71e368f917779acc34ba741ba85315750c7 Mon Sep 17 00:00:00 2001 From: Cameron Brownsey Date: Fri, 7 Aug 2026 04:06:06 +1200 Subject: [PATCH 7/7] test: HostId roundtrips through Display and FromStr. --- src/platform/host_id.rs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/platform/host_id.rs b/src/platform/host_id.rs index 030353f9c..4b39bfe49 100644 --- a/src/platform/host_id.rs +++ b/src/platform/host_id.rs @@ -194,3 +194,32 @@ pub fn default_host() -> crate::Host { .try_into() .expect("the default host should always be available") } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + const EVERY_HOSTID: &[HostId] = &[ + HostId::AAudio, + HostId::Alsa, + HostId::Asio, + HostId::AudioWorklet, + HostId::CoreAudio, + HostId::Custom, + HostId::Jack, + HostId::Null, + HostId::PipeWire, + HostId::PulseAudio, + HostId::Wasapi, + HostId::WebAudio, + ]; + + #[test] + fn host_id_display_roundtrips() { + for &hostid in EVERY_HOSTID { + assert_eq!(HostId::from_str(&hostid.to_string()), Ok(hostid)); + } + } +}