From d71a2f811b3ce5b82d67792da753255147731021 Mon Sep 17 00:00:00 2001 From: melody-rs Date: Sun, 12 Oct 2025 17:37:06 -0700 Subject: [PATCH 1/7] Initial custom host implementation --- src/host/custom/erased.rs | 174 ++++++++++++++++++ src/host/custom/mod.rs | 358 ++++++++++++++++++++++++++++++++++++++ src/host/mod.rs | 4 +- src/lib.rs | 24 ++- src/platform/mod.rs | 34 +++- 5 files changed, 574 insertions(+), 20 deletions(-) create mode 100644 src/host/custom/erased.rs create mode 100644 src/host/custom/mod.rs diff --git a/src/host/custom/erased.rs b/src/host/custom/erased.rs new file mode 100644 index 000000000..d56faf814 --- /dev/null +++ b/src/host/custom/erased.rs @@ -0,0 +1,174 @@ +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, + StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, +}; +use core::time::Duration; + +pub(crate) type Devices = Box>>; + +pub(crate) trait HostErased { + fn devices(&self) -> Result; + fn default_input_device(&self) -> Option>; + fn default_output_device(&self) -> Option>; +} + +pub(crate) type SupportedConfigs = Box>; +pub(crate) type ErrorCallback = Box; +pub(crate) type InputCallback = Box; +pub(crate) type OutputCallback = Box; + +pub(crate) trait DeviceErased { + fn name(&self) -> Result; + fn supports_input(&self) -> bool; + fn supports_output(&self) -> bool; + fn supported_input_configs(&self) -> Result; + fn supported_output_configs(&self) -> Result; + fn default_input_config(&self) -> Result; + fn default_output_config(&self) -> Result; + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: InputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result, BuildStreamError>; + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: OutputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result, BuildStreamError>; +} + +pub(crate) trait StreamErased { + fn play(&self) -> Result<(), PlayStreamError>; + fn pause(&self) -> Result<(), PauseStreamError>; +} + +fn device_to_erased(d: impl DeviceTrait + 'static) -> Box { + Box::new(d) +} + +impl HostErased for T +where + T: HostTrait, + T::Devices: 'static, + T::Device: 'static, +{ + fn devices(&self) -> Result { + let iter = ::devices(self)?; + let erased = Box::new(iter.map(device_to_erased)); + Ok(erased) + } + + fn default_input_device(&self) -> Option> { + ::default_input_device(self).map(device_to_erased) + } + + fn default_output_device(&self) -> Option> { + ::default_output_device(self).map(device_to_erased) + } +} + +fn supported_configs_to_erased( + i: impl Iterator + 'static, +) -> SupportedConfigs { + Box::new(i) +} + +fn stream_to_erased(s: impl StreamTrait + 'static) -> Box { + Box::new(s) +} + +impl DeviceErased for T +where + T: DeviceTrait, + T::SupportedInputConfigs: 'static, + T::SupportedOutputConfigs: 'static, + T::Stream: 'static, +{ + fn name(&self) -> Result { + ::name(self) + } + + fn supports_input(&self) -> bool { + ::supports_input(self) + } + + fn supports_output(&self) -> bool { + ::supports_output(self) + } + + fn supported_input_configs(&self) -> Result { + ::supported_input_configs(self).map(supported_configs_to_erased) + } + + fn supported_output_configs(&self) -> Result { + ::supported_input_configs(self).map(supported_configs_to_erased) + } + + fn default_input_config(&self) -> Result { + ::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + ::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: InputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result, BuildStreamError> { + ::build_input_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(stream_to_erased) + } + + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: OutputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result, BuildStreamError> { + ::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(stream_to_erased) + } +} + +impl StreamErased for T +where + T: StreamTrait, +{ + fn play(&self) -> Result<(), PlayStreamError> { + ::play(self) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + ::pause(self) + } +} diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs new file mode 100644 index 000000000..b857ec187 --- /dev/null +++ b/src/host/custom/mod.rs @@ -0,0 +1,358 @@ +use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; +use crate::{ + BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, + InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, + StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, + SupportedStreamConfigsError, +}; +use core::time::Duration; + +pub struct Host(Box); + +impl Host { + pub(crate) fn new() -> Result { + Err(crate::HostUnavailable) + } + + pub fn from_host(host: T) -> Self + where + T: HostTrait + 'static, + T::Device: Clone, + ::SupportedInputConfigs: Clone, + ::SupportedOutputConfigs: Clone, + { + Self(Box::new(host)) + } +} + +pub struct Device(Box); + +impl Clone for Device { + fn clone(&self) -> Self { + self.0.clone() + } +} + +pub struct Stream(Box); + +// ----- + +type Devices = Box>; +trait HostErased { + fn devices(&self) -> Result; + fn default_input_device(&self) -> Option; + fn default_output_device(&self) -> Option; +} + +pub struct SupportedConfigs(Box); + +trait SupportedConfigsErased { + fn next(&mut self) -> Option; + + fn clone(&self) -> SupportedConfigs; +} + +impl SupportedConfigsErased for T +where + T: Iterator + Clone + 'static, +{ + fn next(&mut self) -> Option { + ::next(self) + } + + fn clone(&self) -> SupportedConfigs { + SupportedConfigs(Box::new(Clone::clone(self))) + } +} + +impl Iterator for SupportedConfigs { + type Item = SupportedStreamConfigRange; + + fn next(&mut self) -> Option { + self.0.next() + } +} + +impl Clone for SupportedConfigs { + fn clone(&self) -> Self { + self.0.clone() + } +} + +type ErrorCallback = Box; +type InputCallback = Box; +type OutputCallback = Box; + +trait DeviceErased { + fn name(&self) -> Result; + fn supports_input(&self) -> bool; + fn supports_output(&self) -> bool; + fn supported_input_configs(&self) -> Result; + fn supported_output_configs(&self) -> Result; + fn default_input_config(&self) -> Result; + fn default_output_config(&self) -> Result; + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: InputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result; + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: OutputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result; + + fn clone(&self) -> Device; +} + +trait StreamErased { + fn play(&self) -> Result<(), PlayStreamError>; + fn pause(&self) -> Result<(), PauseStreamError>; +} + +fn device_to_erased(d: impl DeviceErased + 'static) -> Device { + Device(Box::new(d)) +} + +impl HostErased for T +where + T: HostTrait, + T::Devices: 'static, + T::Device: DeviceErased + 'static, +{ + fn devices(&self) -> Result { + let iter = ::devices(self)?; + let erased = Box::new(iter.map(device_to_erased)); + Ok(erased) + } + + fn default_input_device(&self) -> Option { + ::default_input_device(self).map(device_to_erased) + } + + fn default_output_device(&self) -> Option { + ::default_output_device(self).map(device_to_erased) + } +} + +fn supported_configs_to_erased( + i: impl Iterator + Clone + 'static, +) -> SupportedConfigs { + SupportedConfigs(Box::new(i)) +} + +fn stream_to_erased(s: impl StreamTrait + 'static) -> Stream { + Stream(Box::new(s)) +} + +impl DeviceErased for T +where + T: DeviceTrait + Clone + 'static, + T::SupportedInputConfigs: Clone + 'static, + T::SupportedOutputConfigs: Clone + 'static, + T::Stream: 'static, +{ + fn name(&self) -> Result { + ::name(self) + } + + fn supports_input(&self) -> bool { + ::supports_input(self) + } + + fn supports_output(&self) -> bool { + ::supports_output(self) + } + + fn supported_input_configs(&self) -> Result { + ::supported_input_configs(self).map(supported_configs_to_erased) + } + + fn supported_output_configs(&self) -> Result { + ::supported_input_configs(self).map(supported_configs_to_erased) + } + + fn default_input_config(&self) -> Result { + ::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + ::default_output_config(self) + } + + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: InputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result { + ::build_input_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(stream_to_erased) + } + + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: OutputCallback, + error_callback: ErrorCallback, + timeout: Option, + ) -> Result { + ::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + .map(stream_to_erased) + } + + fn clone(&self) -> Device { + device_to_erased(Clone::clone(self)) + } +} + +impl StreamErased for T +where + T: StreamTrait, +{ + fn play(&self) -> Result<(), PlayStreamError> { + ::play(self) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + ::pause(self) + } +} + +// ----- + +impl HostTrait for Host { + type Devices = Devices; + type Device = Device; + + fn is_available() -> bool { + false + } + + fn devices(&self) -> Result { + self.0.devices() + } + + fn default_input_device(&self) -> Option { + self.0.default_input_device() + } + + fn default_output_device(&self) -> Option { + self.0.default_output_device() + } +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedConfigs; + + type SupportedOutputConfigs = SupportedConfigs; + + type Stream = Stream; + + fn name(&self) -> Result { + self.0.name() + } + + fn supports_input(&self) -> bool { + self.0.supports_input() + } + + fn supports_output(&self) -> bool { + self.0.supports_output() + } + + fn supported_input_configs( + &self, + ) -> Result { + self.0.supported_input_configs() + } + + fn supported_output_configs( + &self, + ) -> Result { + self.0.supported_output_configs() + } + + fn default_input_config(&self) -> Result { + self.0.default_input_config() + } + + fn default_output_config(&self) -> Result { + self.0.default_output_config() + } + + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.0.build_input_stream_raw( + config, + sample_format, + Box::new(data_callback), + Box::new(error_callback), + timeout, + ) + } + + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + data_callback: D, + error_callback: E, + timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + self.0.build_output_stream_raw( + config, + sample_format, + Box::new(data_callback), + Box::new(error_callback), + timeout, + ) + } +} + +impl StreamTrait for Stream { + fn play(&self) -> Result<(), PlayStreamError> { + self.0.play() + } + + fn pause(&self) -> Result<(), PauseStreamError> { + self.0.pause() + } +} diff --git a/src/host/mod.rs b/src/host/mod.rs index 0c61a5910..7b0f15cba 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -23,8 +23,10 @@ pub(crate) mod emscripten; feature = "jack" ))] pub(crate) mod jack; -pub(crate) mod null; #[cfg(windows)] pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] pub(crate) mod webaudio; + +pub(crate) mod custom; +pub(crate) mod null; diff --git a/src/lib.rs b/src/lib.rs index 5acd3b951..40f9f334e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -534,20 +534,16 @@ impl OutputCallbackInfo { #[allow(clippy::len_without_is_empty)] impl Data { - // Internal constructor for host implementations to use. - // - // The following requirements must be met in order for the safety of `Data`'s public API. - // - // - The `data` pointer must point to the first sample in the slice containing all samples. - // - The `len` must describe the length of the buffer as a number of samples in the expected - // format specified via the `sample_format` argument. - // - The `sample_format` must correctly represent the underlying sample data delivered/expected - // by the stream. - pub(crate) unsafe fn from_parts( - data: *mut (), - len: usize, - sample_format: SampleFormat, - ) -> Self { + /// Constructor for host implementations to use. + /// + /// # Safety + /// The following requirements must be met in order for the safety of `Data`'s API. + /// - The `data` pointer must point to the first sample in the slice containing all samples. + /// - The `len` must describe the length of the buffer as a number of samples in the expected + /// format specified via the `sample_format` argument. + /// - The `sample_format` must correctly represent the underlying sample data delivered/expected + /// by the stream. + pub unsafe fn from_parts(data: *mut (), len: usize, sample_format: SampleFormat) -> Self { Data { data, len, diff --git a/src/platform/mod.rs b/src/platform/mod.rs index fd12eaaac..0d08f54f9 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -602,12 +602,14 @@ macro_rules! impl_platform_host { ))] mod platform_impl { pub use crate::host::alsa::Host as AlsaHost; + pub use crate::host::custom::Host as CustomHost; #[cfg(feature = "jack")] pub use crate::host::jack::Host as JackHost; impl_platform_host!( #[cfg(feature = "jack")] Jack => JackHost, Alsa => AlsaHost, + Custom => CustomHost ); /// The default host for the current compilation target platform. @@ -621,7 +623,11 @@ mod platform_impl { #[cfg(any(target_os = "macos", target_os = "ios"))] mod platform_impl { pub use crate::host::coreaudio::Host as CoreAudioHost; - impl_platform_host!(CoreAudio => CoreAudioHost); + pub use crate::host::custom::Host as CustomHost; + impl_platform_host!( + CoreAudio => CoreAudioHost, + Custom => CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -633,8 +639,12 @@ mod platform_impl { #[cfg(target_os = "emscripten")] mod platform_impl { + pub use crate::host::custom::Host as CustomHost; pub use crate::host::emscripten::Host as EmscriptenHost; - impl_platform_host!(Emscripten => EmscriptenHost); + impl_platform_host!( + Emscripten => EmscriptenHost, + Custom => CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -646,8 +656,12 @@ mod platform_impl { #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] mod platform_impl { + pub use crate::host::custom::Host as CustomHost; pub use crate::host::webaudio::Host as WebAudioHost; - impl_platform_host!(WebAudio => WebAudioHost); + impl_platform_host!( + WebAudio => WebAudioHost, + Custom => CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -661,11 +675,13 @@ mod platform_impl { mod platform_impl { #[cfg(feature = "asio")] pub use crate::host::asio::Host as AsioHost; + pub use crate::host::custom::Host as CustomHost; pub use crate::host::wasapi::Host as WasapiHost; impl_platform_host!( #[cfg(feature = "asio")] Asio => AsioHost, Wasapi => WasapiHost, + Custom => CustomHost, ); /// The default host for the current compilation target platform. @@ -679,7 +695,11 @@ mod platform_impl { #[cfg(target_os = "android")] mod platform_impl { pub use crate::host::aaudio::Host as AAudioHost; - impl_platform_host!(AAudio => AAudioHost); + pub use crate::host::custom::Host as CustomHost; + impl_platform_host!( + AAudio => AAudioHost, + Custom => CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -702,9 +722,13 @@ mod platform_impl { all(target_arch = "wasm32", feature = "wasm-bindgen"), )))] mod platform_impl { + pub use crate::host::custom::Host as CustomHost; pub use crate::host::null::Host as NullHost; - impl_platform_host!(Null => NullHost); + impl_platform_host!( + Null => NullHost, + Custom => CustomHost, + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { From 97281060669ac363bc479de4e647255092044080 Mon Sep 17 00:00:00 2001 From: melody-rs Date: Sun, 12 Oct 2025 17:57:21 -0700 Subject: [PATCH 2/7] document custom hosts --- src/host/custom/mod.rs | 55 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index b857ec187..19a2d3cbe 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -7,13 +7,26 @@ use crate::{ }; use core::time::Duration; +/// A host that can be used to write custom [`HostTrait`] implementations. +/// +/// # Usage +/// +/// A [`CustomHost`](Host) can be used on its own, but most crates that depend on `cpal` use a [`cpal::Host`](crate::Host) instead. +/// You can turn a `CustomHost` into a `Host` fairly easily: +/// +/// ```no_run +/// let custom = cpal::platform::CustomHost::from_host(/* ... */); +/// let host = cpal::Host::from(custom); +/// ``` pub struct Host(Box); impl Host { + // this only exists for impl_platform_host, which requires it pub(crate) fn new() -> Result { Err(crate::HostUnavailable) } + /// Construct a custom host from an arbitrary [`HostTrait`] implementation. pub fn from_host(host: T) -> Self where T: HostTrait + 'static, @@ -25,16 +38,58 @@ impl Host { } } +/// A device that can be used to write custom [`DeviceTrait`] implementations. +/// +/// # Usage +/// +/// A [`CustomDevice`](Device) can be used on its own, but most crates that depend on `cpal` use a [`cpal::Device`](crate::Device) instead. +/// You can turn a `Device` into a `Device` fairly easily: +/// +/// ```no_run +/// let custom = cpal::platform::Device::from_device(/* ... */); +/// let device = cpal::Device::from(custom); +/// ``` +/// +/// `rodio`, for example, lets you build an `OutputStream` with a [`cpal::Device`](crate::Device): +/// ```no_run +/// let custom = cpal::platform::Device::from_device(/* ... */); +/// let device = cpal::Device::from(custom); +/// +/// let stream_builder = rodio::OutputStreamBuilder::from_device(device).expect("failed to build stream"); +/// ``` pub struct Device(Box); +impl Device { + /// Construct a custom device from an arbitrary [`DeviceTrait`] implementation. + pub fn from_device(device: T) -> Self + where + T: DeviceTrait + Clone + 'static, + T::SupportedInputConfigs: Clone, + T::SupportedOutputConfigs: Clone, + { + Self(Box::new(device)) + } +} + impl Clone for Device { fn clone(&self) -> Self { self.0.clone() } } +/// A stream that can be used with custom [`StreamTrait`] implementations. pub struct Stream(Box); +impl Stream { + /// Construct a custom stream from an arbitrary [`StreamTrait`] implementation. + pub fn from_stream(stream: T) -> Self + where + T: StreamTrait + 'static, + { + Self(Box::new(stream)) + } +} + // ----- type Devices = Box>; From 4801e7b8b86465c79cbaa4fcaca5f2b02916df1b Mon Sep 17 00:00:00 2001 From: melody-rs Date: Sun, 12 Oct 2025 18:01:43 -0700 Subject: [PATCH 3/7] make custom hosts optional --- Cargo.toml | 4 ++++ src/host/custom/mod.rs | 2 ++ src/host/mod.rs | 1 + src/platform/mod.rs | 21 ++++++++++++++------- 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 271600ca6..0c93d9994 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,10 @@ asio = [ # Deprecated, the `oboe` backend has been removed oboe-shared-stdcxx = [] +# Enable the creation of custom hosts. +custom = [] + +default = ["custom"] [dependencies] dasp_sample = "0.11" diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 19a2d3cbe..bf73433a3 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -18,6 +18,8 @@ use core::time::Duration; /// let custom = cpal::platform::CustomHost::from_host(/* ... */); /// let host = cpal::Host::from(custom); /// ``` +/// +/// Custom hosts are marked as unavailable and will not appear in [`cpal::available_hosts`](crate::available_hosts). pub struct Host(Box); impl Host { diff --git a/src/host/mod.rs b/src/host/mod.rs index 7b0f15cba..6990bad29 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -28,5 +28,6 @@ pub(crate) mod wasapi; #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] pub(crate) mod webaudio; +#[cfg(feature = "custom")] pub(crate) mod custom; pub(crate) mod null; diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 0d08f54f9..603a9ec00 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -602,6 +602,7 @@ macro_rules! impl_platform_host { ))] mod platform_impl { pub use crate::host::alsa::Host as AlsaHost; + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; #[cfg(feature = "jack")] pub use crate::host::jack::Host as JackHost; @@ -609,7 +610,7 @@ mod platform_impl { impl_platform_host!( #[cfg(feature = "jack")] Jack => JackHost, Alsa => AlsaHost, - Custom => CustomHost + #[cfg(feature = "custom")] Custom => CustomHost ); /// The default host for the current compilation target platform. @@ -623,10 +624,11 @@ mod platform_impl { #[cfg(any(target_os = "macos", target_os = "ios"))] mod platform_impl { pub use crate::host::coreaudio::Host as CoreAudioHost; + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; impl_platform_host!( CoreAudio => CoreAudioHost, - Custom => CustomHost + #[cfg(feature = "custom")] Custom => CustomHost ); /// The default host for the current compilation target platform. @@ -639,11 +641,12 @@ mod platform_impl { #[cfg(target_os = "emscripten")] mod platform_impl { + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; pub use crate::host::emscripten::Host as EmscriptenHost; impl_platform_host!( Emscripten => EmscriptenHost, - Custom => CustomHost + #[cfg(feature = "custom")] Custom => CustomHost ); /// The default host for the current compilation target platform. @@ -656,11 +659,12 @@ mod platform_impl { #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] mod platform_impl { + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; pub use crate::host::webaudio::Host as WebAudioHost; impl_platform_host!( WebAudio => WebAudioHost, - Custom => CustomHost + #[cfg(feature = "custom")] Custom => CustomHost ); /// The default host for the current compilation target platform. @@ -675,13 +679,14 @@ mod platform_impl { mod platform_impl { #[cfg(feature = "asio")] pub use crate::host::asio::Host as AsioHost; + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; pub use crate::host::wasapi::Host as WasapiHost; impl_platform_host!( #[cfg(feature = "asio")] Asio => AsioHost, Wasapi => WasapiHost, - Custom => CustomHost, + #[cfg(feature = "custom")] Custom => CustomHost, ); /// The default host for the current compilation target platform. @@ -695,10 +700,11 @@ mod platform_impl { #[cfg(target_os = "android")] mod platform_impl { pub use crate::host::aaudio::Host as AAudioHost; + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; impl_platform_host!( AAudio => AAudioHost, - Custom => CustomHost + #[cfg(feature = "custom")] Custom => CustomHost ); /// The default host for the current compilation target platform. @@ -722,12 +728,13 @@ mod platform_impl { all(target_arch = "wasm32", feature = "wasm-bindgen"), )))] mod platform_impl { + #[cfg(feature = "custom")] pub use crate::host::custom::Host as CustomHost; pub use crate::host::null::Host as NullHost; impl_platform_host!( Null => NullHost, - Custom => CustomHost, + #[cfg(feature = "custom")] Custom => CustomHost, ); /// The default host for the current compilation target platform. From ea1df9019609dc076b407bf8c83728e1aa01d2ca Mon Sep 17 00:00:00 2001 From: melody-rs Date: Sun, 12 Oct 2025 18:14:20 -0700 Subject: [PATCH 4/7] make SupportedConfigsErased a supertrait of Iterator --- src/host/custom/mod.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index bf73433a3..daa15b195 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -103,9 +103,7 @@ trait HostErased { pub struct SupportedConfigs(Box); -trait SupportedConfigsErased { - fn next(&mut self) -> Option; - +trait SupportedConfigsErased: Iterator { fn clone(&self) -> SupportedConfigs; } @@ -113,10 +111,6 @@ impl SupportedConfigsErased for T where T: Iterator + Clone + 'static, { - fn next(&mut self) -> Option { - ::next(self) - } - fn clone(&self) -> SupportedConfigs { SupportedConfigs(Box::new(Clone::clone(self))) } From 697cebe83d0c98f0320c2ac0ac6711742e84f76c Mon Sep 17 00:00:00 2001 From: melody-rs Date: Sun, 12 Oct 2025 18:25:49 -0700 Subject: [PATCH 5/7] document code design a bit more --- src/host/custom/mod.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index daa15b195..63647a15b 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -92,7 +92,8 @@ impl Stream { } } -// ----- +// dyn-compatible versions of DeviceTrait, HostTrait, and StreamTrait +// these only accept/return things via trait objects type Devices = Box>; trait HostErased { @@ -103,6 +104,8 @@ trait HostErased { pub struct SupportedConfigs(Box); +// A trait for supported configs. This only adds a dyn compatible clone function +// This is required because `SupportedInputConfigsInner` & `SupportedOutputConfigsInner` are `Clone` trait SupportedConfigsErased: Iterator { fn clone(&self) -> SupportedConfigs; } @@ -158,7 +161,7 @@ trait DeviceErased { error_callback: ErrorCallback, timeout: Option, ) -> Result; - + // Required because `DeviceInner` is clone fn clone(&self) -> Device; } @@ -293,7 +296,7 @@ where } } -// ----- +// implementations of HostTrait, DeviceTrait, and StreamTrait for custom versions impl HostTrait for Host { type Devices = Devices; From 77720239184b31b0f36f6d00bf66233b59ec79c3 Mon Sep 17 00:00:00 2001 From: melody-rs Date: Sun, 12 Oct 2025 18:33:45 -0700 Subject: [PATCH 6/7] disable doctests, clean up platform/mod.rs --- src/host/custom/mod.rs | 10 +++++----- src/platform/mod.rs | 31 ++++++++++--------------------- 2 files changed, 15 insertions(+), 26 deletions(-) diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 63647a15b..3c4e2645c 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -14,7 +14,7 @@ use core::time::Duration; /// A [`CustomHost`](Host) can be used on its own, but most crates that depend on `cpal` use a [`cpal::Host`](crate::Host) instead. /// You can turn a `CustomHost` into a `Host` fairly easily: /// -/// ```no_run +/// ```ignore /// let custom = cpal::platform::CustomHost::from_host(/* ... */); /// let host = cpal::Host::from(custom); /// ``` @@ -47,14 +47,14 @@ impl Host { /// A [`CustomDevice`](Device) can be used on its own, but most crates that depend on `cpal` use a [`cpal::Device`](crate::Device) instead. /// You can turn a `Device` into a `Device` fairly easily: /// -/// ```no_run -/// let custom = cpal::platform::Device::from_device(/* ... */); +/// ```ignore +/// let custom = cpal::platform::CustomDevice::from_device(/* ... */); /// let device = cpal::Device::from(custom); /// ``` /// /// `rodio`, for example, lets you build an `OutputStream` with a [`cpal::Device`](crate::Device): -/// ```no_run -/// let custom = cpal::platform::Device::from_device(/* ... */); +/// ```ignore +/// let custom = cpal::platform::CustomDevice::from_device(/* ... */); /// let device = cpal::Device::from(custom); /// /// let stream_builder = rodio::OutputStreamBuilder::from_device(device).expect("failed to build stream"); diff --git a/src/platform/mod.rs b/src/platform/mod.rs index 603a9ec00..53ab43239 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -7,6 +7,9 @@ #[doc(inline)] pub use self::platform_impl::*; +#[cfg(feature = "custom")] +pub use crate::host::custom::{Device as CustomDevice, Host as CustomHost, Stream as CustomStream}; + /// A macro to assist with implementing a platform's dynamically dispatched [`Host`] type. /// /// These dynamically dispatched types are necessary to allow for users to switch between hosts at @@ -602,15 +605,13 @@ macro_rules! impl_platform_host { ))] mod platform_impl { pub use crate::host::alsa::Host as AlsaHost; - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; #[cfg(feature = "jack")] pub use crate::host::jack::Host as JackHost; impl_platform_host!( #[cfg(feature = "jack")] Jack => JackHost, Alsa => AlsaHost, - #[cfg(feature = "custom")] Custom => CustomHost + #[cfg(feature = "custom")] Custom => super::CustomHost ); /// The default host for the current compilation target platform. @@ -624,11 +625,9 @@ mod platform_impl { #[cfg(any(target_os = "macos", target_os = "ios"))] mod platform_impl { pub use crate::host::coreaudio::Host as CoreAudioHost; - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; impl_platform_host!( CoreAudio => CoreAudioHost, - #[cfg(feature = "custom")] Custom => CustomHost + #[cfg(feature = "custom")] Custom => super::CustomHost ); /// The default host for the current compilation target platform. @@ -641,12 +640,10 @@ mod platform_impl { #[cfg(target_os = "emscripten")] mod platform_impl { - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; pub use crate::host::emscripten::Host as EmscriptenHost; impl_platform_host!( Emscripten => EmscriptenHost, - #[cfg(feature = "custom")] Custom => CustomHost + #[cfg(feature = "custom")] Custom => super::CustomHost ); /// The default host for the current compilation target platform. @@ -659,12 +656,10 @@ mod platform_impl { #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] mod platform_impl { - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; pub use crate::host::webaudio::Host as WebAudioHost; impl_platform_host!( WebAudio => WebAudioHost, - #[cfg(feature = "custom")] Custom => CustomHost + #[cfg(feature = "custom")] Custom => super::CustomHost ); /// The default host for the current compilation target platform. @@ -679,14 +674,12 @@ mod platform_impl { mod platform_impl { #[cfg(feature = "asio")] pub use crate::host::asio::Host as AsioHost; - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; pub use crate::host::wasapi::Host as WasapiHost; impl_platform_host!( #[cfg(feature = "asio")] Asio => AsioHost, Wasapi => WasapiHost, - #[cfg(feature = "custom")] Custom => CustomHost, + #[cfg(feature = "custom")] Custom => super::CustomHost, ); /// The default host for the current compilation target platform. @@ -700,11 +693,9 @@ mod platform_impl { #[cfg(target_os = "android")] mod platform_impl { pub use crate::host::aaudio::Host as AAudioHost; - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; impl_platform_host!( AAudio => AAudioHost, - #[cfg(feature = "custom")] Custom => CustomHost + #[cfg(feature = "custom")] Custom => super::CustomHost ); /// The default host for the current compilation target platform. @@ -728,13 +719,11 @@ mod platform_impl { all(target_arch = "wasm32", feature = "wasm-bindgen"), )))] mod platform_impl { - #[cfg(feature = "custom")] - pub use crate::host::custom::Host as CustomHost; pub use crate::host::null::Host as NullHost; impl_platform_host!( Null => NullHost, - #[cfg(feature = "custom")] Custom => CustomHost, + #[cfg(feature = "custom")] Custom => super::CustomHost, ); /// The default host for the current compilation target platform. From f2649ac0377b652ed3ec96f39685b33a2c46eddd Mon Sep 17 00:00:00 2001 From: melody-rs Date: Mon, 13 Oct 2025 14:39:41 -0700 Subject: [PATCH 7/7] require custom hosts/devices/streams to impl Send + Sync --- src/host/custom/erased.rs | 174 -------------------------------------- src/host/custom/mod.rs | 26 +++--- 2 files changed, 14 insertions(+), 186 deletions(-) delete mode 100644 src/host/custom/erased.rs diff --git a/src/host/custom/erased.rs b/src/host/custom/erased.rs deleted file mode 100644 index d56faf814..000000000 --- a/src/host/custom/erased.rs +++ /dev/null @@ -1,174 +0,0 @@ -use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; -use crate::{ - BuildStreamError, Data, DefaultStreamConfigError, DeviceNameError, DevicesError, - InputCallbackInfo, OutputCallbackInfo, PauseStreamError, PlayStreamError, SampleFormat, - StreamConfig, StreamError, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, -}; -use core::time::Duration; - -pub(crate) type Devices = Box>>; - -pub(crate) trait HostErased { - fn devices(&self) -> Result; - fn default_input_device(&self) -> Option>; - fn default_output_device(&self) -> Option>; -} - -pub(crate) type SupportedConfigs = Box>; -pub(crate) type ErrorCallback = Box; -pub(crate) type InputCallback = Box; -pub(crate) type OutputCallback = Box; - -pub(crate) trait DeviceErased { - fn name(&self) -> Result; - fn supports_input(&self) -> bool; - fn supports_output(&self) -> bool; - fn supported_input_configs(&self) -> Result; - fn supported_output_configs(&self) -> Result; - fn default_input_config(&self) -> Result; - fn default_output_config(&self) -> Result; - fn build_input_stream_raw( - &self, - config: &StreamConfig, - sample_format: SampleFormat, - data_callback: InputCallback, - error_callback: ErrorCallback, - timeout: Option, - ) -> Result, BuildStreamError>; - fn build_output_stream_raw( - &self, - config: &StreamConfig, - sample_format: SampleFormat, - data_callback: OutputCallback, - error_callback: ErrorCallback, - timeout: Option, - ) -> Result, BuildStreamError>; -} - -pub(crate) trait StreamErased { - fn play(&self) -> Result<(), PlayStreamError>; - fn pause(&self) -> Result<(), PauseStreamError>; -} - -fn device_to_erased(d: impl DeviceTrait + 'static) -> Box { - Box::new(d) -} - -impl HostErased for T -where - T: HostTrait, - T::Devices: 'static, - T::Device: 'static, -{ - fn devices(&self) -> Result { - let iter = ::devices(self)?; - let erased = Box::new(iter.map(device_to_erased)); - Ok(erased) - } - - fn default_input_device(&self) -> Option> { - ::default_input_device(self).map(device_to_erased) - } - - fn default_output_device(&self) -> Option> { - ::default_output_device(self).map(device_to_erased) - } -} - -fn supported_configs_to_erased( - i: impl Iterator + 'static, -) -> SupportedConfigs { - Box::new(i) -} - -fn stream_to_erased(s: impl StreamTrait + 'static) -> Box { - Box::new(s) -} - -impl DeviceErased for T -where - T: DeviceTrait, - T::SupportedInputConfigs: 'static, - T::SupportedOutputConfigs: 'static, - T::Stream: 'static, -{ - fn name(&self) -> Result { - ::name(self) - } - - fn supports_input(&self) -> bool { - ::supports_input(self) - } - - fn supports_output(&self) -> bool { - ::supports_output(self) - } - - fn supported_input_configs(&self) -> Result { - ::supported_input_configs(self).map(supported_configs_to_erased) - } - - fn supported_output_configs(&self) -> Result { - ::supported_input_configs(self).map(supported_configs_to_erased) - } - - fn default_input_config(&self) -> Result { - ::default_input_config(self) - } - - fn default_output_config(&self) -> Result { - ::default_output_config(self) - } - - fn build_input_stream_raw( - &self, - config: &StreamConfig, - sample_format: SampleFormat, - data_callback: InputCallback, - error_callback: ErrorCallback, - timeout: Option, - ) -> Result, BuildStreamError> { - ::build_input_stream_raw( - self, - config, - sample_format, - data_callback, - error_callback, - timeout, - ) - .map(stream_to_erased) - } - - fn build_output_stream_raw( - &self, - config: &StreamConfig, - sample_format: SampleFormat, - data_callback: OutputCallback, - error_callback: ErrorCallback, - timeout: Option, - ) -> Result, BuildStreamError> { - ::build_output_stream_raw( - self, - config, - sample_format, - data_callback, - error_callback, - timeout, - ) - .map(stream_to_erased) - } -} - -impl StreamErased for T -where - T: StreamTrait, -{ - fn play(&self) -> Result<(), PlayStreamError> { - ::play(self) - } - - fn pause(&self) -> Result<(), PauseStreamError> { - ::pause(self) - } -} diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs index 3c4e2645c..82e561050 100644 --- a/src/host/custom/mod.rs +++ b/src/host/custom/mod.rs @@ -31,10 +31,11 @@ impl Host { /// Construct a custom host from an arbitrary [`HostTrait`] implementation. pub fn from_host(host: T) -> Self where - T: HostTrait + 'static, - T::Device: Clone, + T: HostTrait + Send + Sync + 'static, + T::Device: Send + Sync + Clone, ::SupportedInputConfigs: Clone, ::SupportedOutputConfigs: Clone, + ::Stream: Send + Sync, { Self(Box::new(host)) } @@ -65,9 +66,10 @@ impl Device { /// Construct a custom device from an arbitrary [`DeviceTrait`] implementation. pub fn from_device(device: T) -> Self where - T: DeviceTrait + Clone + 'static, + T: DeviceTrait + Send + Sync + Clone + 'static, T::SupportedInputConfigs: Clone, T::SupportedOutputConfigs: Clone, + T::Stream: Send + Sync, { Self(Box::new(device)) } @@ -86,7 +88,7 @@ impl Stream { /// Construct a custom stream from an arbitrary [`StreamTrait`] implementation. pub fn from_stream(stream: T) -> Self where - T: StreamTrait + 'static, + T: StreamTrait + Send + Sync + 'static, { Self(Box::new(stream)) } @@ -96,7 +98,7 @@ impl Stream { // these only accept/return things via trait objects type Devices = Box>; -trait HostErased { +trait HostErased: Send + Sync { fn devices(&self) -> Result; fn default_input_device(&self) -> Option; fn default_output_device(&self) -> Option; @@ -137,7 +139,7 @@ type ErrorCallback = Box; type InputCallback = Box; type OutputCallback = Box; -trait DeviceErased { +trait DeviceErased: Send + Sync { fn name(&self) -> Result; fn supports_input(&self) -> bool; fn supports_output(&self) -> bool; @@ -165,7 +167,7 @@ trait DeviceErased { fn clone(&self) -> Device; } -trait StreamErased { +trait StreamErased: Send + Sync { fn play(&self) -> Result<(), PlayStreamError>; fn pause(&self) -> Result<(), PauseStreamError>; } @@ -176,7 +178,7 @@ fn device_to_erased(d: impl DeviceErased + 'static) -> Device { impl HostErased for T where - T: HostTrait, + T: HostTrait + Send + Sync, T::Devices: 'static, T::Device: DeviceErased + 'static, { @@ -201,16 +203,16 @@ fn supported_configs_to_erased( SupportedConfigs(Box::new(i)) } -fn stream_to_erased(s: impl StreamTrait + 'static) -> Stream { +fn stream_to_erased(s: impl StreamTrait + Send + Sync + 'static) -> Stream { Stream(Box::new(s)) } impl DeviceErased for T where - T: DeviceTrait + Clone + 'static, + T: DeviceTrait + Send + Sync + Clone + 'static, T::SupportedInputConfigs: Clone + 'static, T::SupportedOutputConfigs: Clone + 'static, - T::Stream: 'static, + T::Stream: Send + Sync + 'static, { fn name(&self) -> Result { ::name(self) @@ -285,7 +287,7 @@ where impl StreamErased for T where - T: StreamTrait, + T: StreamTrait + Send + Sync, { fn play(&self) -> Result<(), PlayStreamError> { ::play(self)