diff --git a/CHANGELOG.md b/CHANGELOG.md index bf88d2465..4eee0f7a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Add support for custom `Host`s, `Device`s, and `Stream`s - Add `Sample::bits_per_sample` method. - Update `audio_thread_priority` to 0.34. - AAudio: Configure buffer to ensure consistent callback buffer sizes. diff --git a/Cargo.toml b/Cargo.toml index 0ff38048c..830e45442 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ asio = [ "num-traits", ] # Only available on Windows. See README for setup instructions. +# Support for user-defined custom hosts, devices, and streams +# See examples/custom.rs for usage +custom = [] + [dependencies] dasp_sample = "0.11" diff --git a/examples/custom.rs b/examples/custom.rs new file mode 100644 index 000000000..eee3f1111 --- /dev/null +++ b/examples/custom.rs @@ -0,0 +1,345 @@ +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; + +use cpal::traits::{DeviceTrait, HostTrait, StreamTrait}; +use cpal::{FromSample, Sample}; + +#[allow(dead_code)] +#[derive(Clone)] // Clone, Send+Sync are required +struct MyHost; + +#[derive(Clone)] // Clone, Send+Sync are required +struct MyDevice; + +// Only Send+Sync is needed +struct MyStream { + controls: Arc, + // option is needed since joining a thread takes ownership, + // and we want to do that on drop (gives us &mut self, not self) + handle: Option>, +} + +struct StreamControls { + exit: AtomicBool, + pause: AtomicBool, +} + +impl HostTrait for MyHost { + type Device = MyDevice; + type Devices = std::iter::Once; + + fn is_available() -> bool { + true + } + + fn devices(&self) -> Result { + Ok(std::iter::once(MyDevice)) + } + + fn default_input_device(&self) -> Option { + None + } + + fn default_output_device(&self) -> Option { + Some(MyDevice) + } +} + +impl DeviceTrait for MyDevice { + type SupportedInputConfigs = std::iter::Empty; + type SupportedOutputConfigs = std::iter::Once; + type Stream = MyStream; + + fn name(&self) -> Result { + Ok(String::from("custom device")) + } + + fn supported_input_configs( + &self, + ) -> Result { + Ok(std::iter::empty()) + } + + fn supported_output_configs( + &self, + ) -> Result { + Ok(std::iter::once(cpal::SupportedStreamConfigRange::new( + 2, + cpal::SampleRate(44100), + cpal::SampleRate(44100), + cpal::SupportedBufferSize::Unknown, + cpal::SampleFormat::F32, + ))) + } + + fn default_input_config( + &self, + ) -> Result { + Err(cpal::DefaultStreamConfigError::StreamTypeNotSupported) + } + + fn default_output_config( + &self, + ) -> Result { + Ok(cpal::SupportedStreamConfig::new( + 2, + cpal::SampleRate(44100), + cpal::SupportedBufferSize::Unknown, + cpal::SampleFormat::I16, + )) + } + + fn build_input_stream_raw( + &self, + _: &cpal::StreamConfig, + _: cpal::SampleFormat, + _: D, + _: E, + _: Option, + ) -> Result + where + D: FnMut(&cpal::Data, &cpal::InputCallbackInfo) + Send + 'static, + E: FnMut(cpal::StreamError) + Send + 'static, + { + Err(cpal::BuildStreamError::StreamConfigNotSupported) + } + + // this is the meat of a custom device impl. + // you're expected to repeatedly call `data_callback` and provide it with a buffer of samples, + // as well as a stream timestamp. + // a proper impl would also check the stream config and sample format, as well as handle errors + fn build_output_stream_raw( + &self, + _: &cpal::StreamConfig, + _: cpal::SampleFormat, + mut data_callback: D, + _: E, + _: Option, + ) -> Result + where + D: FnMut(&mut cpal::Data, &cpal::OutputCallbackInfo) + Send + 'static, + E: FnMut(cpal::StreamError) + Send + 'static, + { + let controls = Arc::new(StreamControls { + exit: AtomicBool::new(false), + pause: AtomicBool::new(true), // streams are expected to start out paused by default + }); + + let thread_controls = controls.clone(); + let handle = std::thread::spawn(move || { + let start = std::time::Instant::now(); + let mut buffer = [0.0_f32; 4096]; + while !thread_controls.exit.load(Ordering::Relaxed) { + std::thread::sleep(std::time::Duration::from_secs_f32( + buffer.len() as f32 / 44100.0, + )); + // continue if paused + if thread_controls.pause.load(Ordering::Relaxed) { + continue; + } + + // data is cpal's way of having a type erased buffer. + // you're expected to provide a raw pointer, the amount of samples, and the sample format of the buffer + let mut data = unsafe { + cpal::Data::from_parts( + buffer.as_mut_ptr().cast(), + buffer.len(), + cpal::SampleFormat::F32, + ) + }; + + let duration = std::time::Instant::now().duration_since(start); + let secs = duration.as_nanos() / 1_000_000_000; + let subsec_nanos = duration.as_nanos() - secs * 1_000_000_000; + let stream_instant = cpal::StreamInstant::new(secs as _, subsec_nanos as _); + let timestamp = cpal::OutputStreamTimestamp { + callback: stream_instant, + playback: stream_instant, + }; + data_callback(&mut data, &cpal::OutputCallbackInfo::new(timestamp)); + + let avg = buffer.iter().sum::() / buffer.len() as f32; + println!("avg: {avg}"); + } + }); + + Ok(MyStream { + controls, + handle: Some(handle), + }) + } +} + +impl StreamTrait for MyStream { + fn play(&self) -> Result<(), cpal::PlayStreamError> { + self.controls.pause.store(false, Ordering::Relaxed); + Ok(()) + } + + fn pause(&self) -> Result<(), cpal::PauseStreamError> { + self.controls.pause.store(true, Ordering::Relaxed); + Ok(()) + } +} + +// streams are expected to stop when dropped +impl Drop for MyStream { + fn drop(&mut self) { + self.controls.exit.store(true, Ordering::Relaxed); + let _ = self.handle.take().unwrap().join(); + } +} + +#[cfg(feature = "custom")] +fn main() { + let custom_host = cpal::platform::CustomHost::from_host(MyHost); + // alternatively, use cpal::platform::CustomDevice and skip enumerating devices + let host = cpal::Host::from(custom_host); // this host can be passed to rodio or any other crate that uses cpal + + let device = host.default_output_device().unwrap(); + let config = device.default_output_config().unwrap(); + + let stream = make_stream(&device, &config.into()).unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_millis(4000)); +} + +#[cfg(not(feature = "custom"))] +fn main() { + panic!("please run with -F custom to try this example") +} + +// rest of this example is mostly based off of synth_tones.rs + +pub enum Waveform { + Sine, + Square, + Saw, + Triangle, +} + +pub struct Oscillator { + pub sample_rate: f32, + pub waveform: Waveform, + pub current_sample_index: f32, + pub frequency_hz: f32, +} + +impl Oscillator { + fn advance_sample(&mut self) { + self.current_sample_index = (self.current_sample_index + 1.0) % self.sample_rate; + } + + fn set_waveform(&mut self, waveform: Waveform) { + self.waveform = waveform; + } + + fn calculate_sine_output_from_freq(&self, freq: f32) -> f32 { + let two_pi = 2.0 * std::f32::consts::PI; + (self.current_sample_index * freq * two_pi / self.sample_rate).sin() + } + + fn is_multiple_of_freq_above_nyquist(&self, multiple: f32) -> bool { + self.frequency_hz * multiple > self.sample_rate / 2.0 + } + + fn sine_wave(&mut self) -> f32 { + self.advance_sample(); + self.calculate_sine_output_from_freq(self.frequency_hz) + } + + fn generative_waveform(&mut self, harmonic_index_increment: i32, gain_exponent: f32) -> f32 { + self.advance_sample(); + let mut output = 0.0; + let mut i = 1; + while !self.is_multiple_of_freq_above_nyquist(i as f32) { + let gain = 1.0 / (i as f32).powf(gain_exponent); + output += gain * self.calculate_sine_output_from_freq(self.frequency_hz * i as f32); + i += harmonic_index_increment; + } + output + } + + fn square_wave(&mut self) -> f32 { + self.generative_waveform(2, 1.0) + } + + fn saw_wave(&mut self) -> f32 { + self.generative_waveform(1, 1.0) + } + + fn triangle_wave(&mut self) -> f32 { + self.generative_waveform(2, 2.0) + } + + fn tick(&mut self) -> f32 { + match self.waveform { + Waveform::Sine => self.sine_wave(), + Waveform::Square => self.square_wave(), + Waveform::Saw => self.saw_wave(), + Waveform::Triangle => self.triangle_wave(), + } + } +} + +pub fn make_stream( + device: &cpal::Device, + config: &cpal::StreamConfig, +) -> Result { + let num_channels = config.channels as usize; + let mut oscillator = Oscillator { + waveform: Waveform::Sine, + sample_rate: config.sample_rate.0 as f32, + current_sample_index: 0.0, + frequency_hz: 440.0, + }; + let err_fn = |err| eprintln!("Error building output sound stream: {err}"); + + let time_at_start = std::time::Instant::now(); + println!("Time at start: {time_at_start:?}"); + + let stream = device.build_output_stream( + config, + move |output: &mut [f32], _: &cpal::OutputCallbackInfo| { + // for 0-1s play sine, 1-2s play square, 2-3s play saw, 3-4s play triangle_wave + let time_since_start = std::time::Instant::now() + .duration_since(time_at_start) + .as_secs_f32(); + if time_since_start < 1.0 { + oscillator.set_waveform(Waveform::Sine); + } else if time_since_start < 2.0 { + oscillator.set_waveform(Waveform::Triangle); + } else if time_since_start < 3.0 { + oscillator.set_waveform(Waveform::Square); + } else if time_since_start < 4.0 { + oscillator.set_waveform(Waveform::Saw); + } else { + oscillator.set_waveform(Waveform::Sine); + } + process_frame(output, &mut oscillator, num_channels) + }, + err_fn, + None, + )?; + + Ok(stream) +} + +fn process_frame( + output: &mut [SampleType], + oscillator: &mut Oscillator, + num_channels: usize, +) where + SampleType: Sample + FromSample, +{ + for frame in output.chunks_mut(num_channels) { + let value: SampleType = SampleType::from_sample(oscillator.tick()); + + // copy the same value to all channels + for sample in frame.iter_mut() { + *sample = value; + } + } +} diff --git a/src/host/custom/mod.rs b/src/host/custom/mod.rs new file mode 100644 index 000000000..7d37edd55 --- /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_output_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 {