diff --git a/Cargo.toml b/Cargo.toml index 0ff38048c..25ec54ad2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ asio = [ "num-traits", ] # Only available on Windows. See README for setup instructions. +custom = [] + +default = ["custom"] + [dependencies] dasp_sample = "0.11" diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs new file mode 100644 index 000000000..82e561050 --- /dev/null +++ b/src/host/custom/mod.rs @@ -0,0 +1,414 @@ +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; + +/// 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: +/// +/// ```ignore +/// 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 { + // 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 + Send + Sync + 'static, + T::Device: Send + Sync + Clone, + ::SupportedInputConfigs: Clone, + ::SupportedOutputConfigs: Clone, + ::Stream: Send + Sync, + { + Self(Box::new(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: +/// +/// ```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): +/// ```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"); +/// ``` +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 + Send + Sync + Clone + 'static, + T::SupportedInputConfigs: Clone, + T::SupportedOutputConfigs: Clone, + T::Stream: Send + Sync, + { + 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 + Send + Sync + 'static, + { + Self(Box::new(stream)) + } +} + +// dyn-compatible versions of DeviceTrait, HostTrait, and StreamTrait +// these only accept/return things via trait objects + +type Devices = Box>; +trait HostErased: Send + Sync { + fn devices(&self) -> Result; + fn default_input_device(&self) -> Option; + fn default_output_device(&self) -> Option; +} + +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; +} + +impl SupportedConfigsErased for T +where + T: Iterator + Clone + 'static, +{ + 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: Send + Sync { + 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; + // Required because `DeviceInner` is clone + fn clone(&self) -> Device; +} + +trait StreamErased: Send + Sync { + 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 + Send + Sync, + 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 + Send + Sync + 'static) -> Stream { + Stream(Box::new(s)) +} + +impl DeviceErased for T +where + T: DeviceTrait + Send + Sync + Clone + 'static, + T::SupportedInputConfigs: Clone + 'static, + T::SupportedOutputConfigs: Clone + 'static, + T::Stream: Send + Sync + '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 + Send + Sync, +{ + fn play(&self) -> Result<(), PlayStreamError> { + ::play(self) + } + + fn pause(&self) -> Result<(), PauseStreamError> { + ::pause(self) + } +} + +// implementations of HostTrait, DeviceTrait, and StreamTrait for custom versions + +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 1aecf93ab..28a6b9f1b 100644 --- a/src/host/mod.rs +++ b/src/host/mod.rs @@ -23,12 +23,15 @@ 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; +#[cfg(feature = "custom")] +pub(crate) mod custom; +pub(crate) mod null; + /// Compile-time assertion that a type implements Send. /// Use this macro in each host module to ensure Stream is Send. #[macro_export] 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..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 @@ -608,6 +611,7 @@ mod platform_impl { impl_platform_host!( #[cfg(feature = "jack")] Jack => JackHost, Alsa => AlsaHost, + #[cfg(feature = "custom")] Custom => super::CustomHost ); /// The default host for the current compilation target platform. @@ -621,7 +625,10 @@ 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); + impl_platform_host!( + CoreAudio => CoreAudioHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -634,7 +641,10 @@ mod platform_impl { #[cfg(target_os = "emscripten")] mod platform_impl { pub use crate::host::emscripten::Host as EmscriptenHost; - impl_platform_host!(Emscripten => EmscriptenHost); + impl_platform_host!( + Emscripten => EmscriptenHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -647,7 +657,10 @@ mod platform_impl { #[cfg(all(target_arch = "wasm32", feature = "wasm-bindgen"))] mod platform_impl { pub use crate::host::webaudio::Host as WebAudioHost; - impl_platform_host!(WebAudio => WebAudioHost); + impl_platform_host!( + WebAudio => WebAudioHost, + #[cfg(feature = "custom")] Custom => super::CustomHost + ); /// The default host for the current compilation target platform. pub fn default_host() -> Host { @@ -666,6 +679,7 @@ mod platform_impl { impl_platform_host!( #[cfg(feature = "asio")] Asio => AsioHost, Wasapi => WasapiHost, + #[cfg(feature = "custom")] Custom => super::CustomHost, ); /// The default host for the current compilation target platform. @@ -679,7 +693,10 @@ mod platform_impl { #[cfg(target_os = "android")] mod platform_impl { pub use crate::host::aaudio::Host as AAudioHost; - impl_platform_host!(AAudio => AAudioHost); + 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 { @@ -704,7 +721,10 @@ mod platform_impl { mod platform_impl { pub use crate::host::null::Host as NullHost; - impl_platform_host!(Null => NullHost); + 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 {