diff --git a/.gitignore b/.gitignore index 0289afe13..50f011359 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .DS_Store recorded.wav rls*.log +/.direnv diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe4cb9bd..41c78be07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - CoreAudio: Change `Device::supported_configs` to return a single element containing the available sample rate range when all elements have the same `mMinimum` and `mMaximum` values. - CoreAudio: Change default audio device detection to be lazy when building a stream, instead of during device enumeration. - CoreAudio: Add `i8`, `i32` and `I24` sample format support (24-bit samples stored in 4 bytes). +- CoreAudio: Add support for loopback recording (recording system audio output) on macOS. - iOS: Fix example by properly activating audio session. - WASAPI: Expose `IMMDevice` from WASAPI host Device. - WASAPI: Add `I24` and `U24` sample format support (24-bit samples stored in 4 bytes). diff --git a/Cargo.toml b/Cargo.toml index 3014e1562..52cffd9cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,10 @@ edition = "2021" rust-version = "1.70" [features] -asio = ["asio-sys", "num-traits"] # Only available on Windows. See README for setup instructions. +asio = [ + "asio-sys", + "num-traits", +] # Only available on Windows. See README for setup instructions. # Deprecated, the `oboe` backend has been removed oboe-shared-stdcxx = [] @@ -36,8 +39,8 @@ windows = { version = "0.54.0", features = [ "Win32_System_SystemServices", "Win32_System_Variant", "Win32_Media_Multimedia", - "Win32_UI_Shell_PropertiesSystem" -]} + "Win32_UI_Shell_PropertiesSystem", +] } audio_thread_priority = { version = "0.33.0", optional = true } asio-sys = { version = "0.2", path = "asio-sys", optional = true } num-traits = { version = "0.2.6", optional = true } @@ -60,6 +63,8 @@ objc2-core-audio = { version = "0.3.1", default-features = false, features = [ "std", "AudioHardware", "AudioHardwareDeprecated", + "objc2", + "objc2-foundation", ] } objc2-audio-toolbox = { version = "0.3.1", default-features = false, features = [ "std", @@ -70,20 +75,44 @@ objc2-core-audio-types = { version = "0.3.1", default-features = false, features "std", "CoreAudioBaseTypes", ] } +objc2-core-foundation = { version = "0.3.1" } +objc2-foundation = { version = "0.3.1" } +objc2 = { version = "0.6.2" } [target.'cfg(target_os = "emscripten")'.dependencies] wasm-bindgen = { version = "0.2.89" } wasm-bindgen-futures = "0.4.33" js-sys = { version = "0.3.35" } -web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] } +web-sys = { version = "0.3.35", features = [ + "AudioContext", + "AudioContextOptions", + "AudioBuffer", + "AudioBufferSourceNode", + "AudioNode", + "AudioDestinationNode", + "Window", + "AudioContextState", +] } [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] wasm-bindgen = { version = "0.2.58", optional = true } js-sys = { version = "0.3.35" } -web-sys = { version = "0.3.35", features = [ "AudioContext", "AudioContextOptions", "AudioBuffer", "AudioBufferSourceNode", "AudioNode", "AudioDestinationNode", "Window", "AudioContextState"] } +web-sys = { version = "0.3.35", features = [ + "AudioContext", + "AudioContextOptions", + "AudioBuffer", + "AudioBufferSourceNode", + "AudioNode", + "AudioDestinationNode", + "Window", + "AudioContextState", +] } [target.'cfg(target_os = "android")'.dependencies] -ndk = { version = "0.9", default-features = false, features = ["audio", "api-level-26"]} +ndk = { version = "0.9", default-features = false, features = [ + "audio", + "api-level-26", +] } ndk-context = "0.1" jni = "0.21" num-derive = "0.4" diff --git a/examples/record_wav.rs b/examples/record_wav.rs index b10f76216..ec7283774 100644 --- a/examples/record_wav.rs +++ b/examples/record_wav.rs @@ -12,10 +12,16 @@ use std::sync::{Arc, Mutex}; #[derive(Parser, Debug)] #[command(version, about = "CPAL record_wav example", long_about = None)] struct Opt { - /// The audio device to use + /// The audio device to use. + /// For the default microphone, use "default". + /// For recording system output, use "default-output". #[arg(short, long, default_value_t = String::from("default"))] device: String, + /// How long to record, in seconds + #[arg(long, default_value_t = 3)] + duration: u64, + /// Use the JACK host #[cfg(all( any( @@ -69,20 +75,24 @@ fn main() -> Result<(), anyhow::Error> { let host = cpal::default_host(); // Set up the input device and stream with the default input config. - let device = if opt.device == "default" { - host.default_input_device() - } else { - host.input_devices()? - .find(|x| x.name().map(|y| y == opt.device).unwrap_or(false)) + let device = match opt.device.as_str() { + "default" => host.default_input_device(), + "default-output" => host.default_output_device(), + name => host + .input_devices()? + .find(|x| x.name().map(|y| y == name).unwrap_or(false)), } .expect("failed to find input device"); println!("Input device: {}", device.name()?); - let config = device - .default_input_config() - .expect("Failed to get default input config"); - println!("Default input config: {config:?}"); + let config = if device.supports_input() { + device.default_input_config() + } else { + device.default_output_config() + } + .expect("Failed to get default input/output config"); + println!("Default input/output config: {config:?}"); // The WAV file we're recording to. const PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/recorded.wav"); @@ -135,7 +145,7 @@ fn main() -> Result<(), anyhow::Error> { stream.play()?; // Let recording go for roughly three seconds. - std::thread::sleep(std::time::Duration::from_secs(3)); + std::thread::sleep(std::time::Duration::from_secs(opt.duration)); drop(stream); writer.lock().unwrap().take().unwrap().finalize()?; println!("Recording {PATH} complete!"); diff --git a/src/host/coreaudio/macos/device.rs b/src/host/coreaudio/macos/device.rs new file mode 100644 index 000000000..177f82914 --- /dev/null +++ b/src/host/coreaudio/macos/device.rs @@ -0,0 +1,892 @@ +use super::OSStatus; +use super::Stream; +use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant}; +use crate::host::coreaudio::macos::loopback::LoopbackDevice; +use crate::host::coreaudio::macos::StreamInner; +use crate::traits::DeviceTrait; +use crate::{ + BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, + DefaultStreamConfigError, DeviceNameError, InputCallbackInfo, OutputCallbackInfo, SampleFormat, + SampleRate, StreamConfig, StreamError, SupportedBufferSize, SupportedStreamConfig, + SupportedStreamConfigRange, SupportedStreamConfigsError, +}; +use coreaudio::audio_unit::render_callback::{self, data}; +use coreaudio::audio_unit::{AudioUnit, Element, Scope}; +use objc2_audio_toolbox::{ + kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, + kAudioUnitProperty_StreamFormat, +}; +use objc2_core_audio::{ + kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyBufferFrameSize, + kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyDeviceIsAlive, + kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyStreamConfiguration, + kAudioDevicePropertyStreamFormat, kAudioObjectPropertyElementMaster, + kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, + kAudioObjectPropertyScopeOutput, AudioDeviceID, AudioObjectGetPropertyData, + AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, + AudioObjectPropertyScope, AudioObjectSetPropertyData, +}; +use objc2_core_audio_types::{ + AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, +}; + +pub use super::enumerate::{ + default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs, +}; +use std::fmt; +use std::mem::{self}; +use std::ptr::{null, NonNull}; +use std::rc::Rc; +use std::slice; +use std::sync::mpsc::{channel, RecvTimeoutError}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use super::property_listener::AudioObjectPropertyListener; +use coreaudio::audio_unit::macos_helpers::get_device_name; +/// Attempt to set the device sample rate to the provided rate. +/// Return an error if the requested sample rate is not supported by the device. +fn set_sample_rate( + audio_device_id: AudioObjectID, + target_sample_rate: SampleRate, +) -> Result<(), BuildStreamError> { + // Get the current sample rate. + let mut property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + let mut sample_rate: f64 = 0.0; + let data_size = mem::size_of::() as u32; + let status = unsafe { + AudioObjectGetPropertyData( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut sample_rate).cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + + // If the requested sample rate is different to the device sample rate, update the device. + if sample_rate as u32 != target_sample_rate.0 { + // Get available sample rate ranges. + property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + let data_size = 0u32; + let status = unsafe { + AudioObjectGetPropertyDataSize( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + ) + }; + coreaudio::Error::from_os_status(status)?; + let n_ranges = data_size as usize / mem::size_of::(); + let mut ranges: Vec = vec![]; + ranges.reserve_exact(data_size as usize); + let status = unsafe { + AudioObjectGetPropertyData( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; + let ranges: &[AudioValueRange] = unsafe { slice::from_raw_parts(ranges, n_ranges) }; + + // Now that we have the available ranges, pick the one matching the desired rate. + let sample_rate = target_sample_rate.0; + let maybe_index = ranges + .iter() + .position(|r| r.mMinimum as u32 == sample_rate && r.mMaximum as u32 == sample_rate); + let range_index = match maybe_index { + None => return Err(BuildStreamError::StreamConfigNotSupported), + Some(i) => i, + }; + + let (send, recv) = channel::>(); + let sample_rate_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyNominalSampleRate, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }; + // Send sample rate updates back on a channel. + let sample_rate_handler = move || { + let mut rate: f64 = 0.0; + let data_size = mem::size_of::() as u32; + + let result = unsafe { + AudioObjectGetPropertyData( + audio_device_id, + NonNull::from(&sample_rate_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut rate).cast(), + ) + }; + send.send(coreaudio::Error::from_os_status(result).map(|_| rate)) + .ok(); + }; + + let listener = AudioObjectPropertyListener::new( + audio_device_id, + sample_rate_address, + sample_rate_handler, + )?; + + // Finally, set the sample rate. + property_address.mSelector = kAudioDevicePropertyNominalSampleRate; + let status = unsafe { + AudioObjectSetPropertyData( + audio_device_id, + NonNull::from(&property_address), + 0, + null(), + data_size, + NonNull::from(&ranges[range_index]).cast(), + ) + }; + coreaudio::Error::from_os_status(status)?; + + // Wait for the reported_rate to change. + // + // This should not take longer than a few ms, but we timeout after 1 sec just in case. + // We loop over potentially several events from the channel to ensure + // that we catch the expected change in sample rate. + let mut timeout = Duration::from_secs(1); + let start = Instant::now(); + + loop { + match recv.recv_timeout(timeout) { + Err(err) => { + let description = match err { + RecvTimeoutError::Disconnected => { + "sample rate listener channel disconnected unexpectedly" + } + RecvTimeoutError::Timeout => { + "timeout waiting for sample rate update for device" + } + } + .to_string(); + return Err(BackendSpecificError { description }.into()); + } + Ok(Ok(reported_sample_rate)) => { + if reported_sample_rate == target_sample_rate.0 as f64 { + break; + } + } + Ok(Err(_)) => { + // TODO: should we consider collecting this error? + } + }; + timeout = timeout + .checked_sub(start.elapsed()) + .unwrap_or(Duration::ZERO); + } + listener.remove()?; + } + Ok(()) +} + +fn audio_unit_from_device(device: &Device, input: bool) -> Result { + let output_type = if is_default_device(device) && !input { + coreaudio::audio_unit::IOType::DefaultOutput + } else { + coreaudio::audio_unit::IOType::HalOutput + }; + let mut audio_unit = AudioUnit::new(output_type)?; + + if input { + // Enable input processing. + let enable_input = 1u32; + audio_unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Input, + Element::Input, + Some(&enable_input), + )?; + + // Disable output processing. + let disable_output = 0u32; + audio_unit.set_property( + kAudioOutputUnitProperty_EnableIO, + Scope::Output, + Element::Output, + Some(&disable_output), + )?; + } + + audio_unit.set_property( + kAudioOutputUnitProperty_CurrentDevice, + Scope::Global, + Element::Output, + Some(&device.audio_device_id), + )?; + + Ok(audio_unit) +} + +fn get_io_buffer_frame_size_range( + audio_unit: &AudioUnit, +) -> Result { + let buffer_size_range: AudioValueRange = audio_unit.get_property( + kAudioDevicePropertyBufferFrameSizeRange, + Scope::Global, + Element::Output, + )?; + + Ok(SupportedBufferSize::Range { + min: buffer_size_range.mMinimum as u32, + max: buffer_size_range.mMaximum as u32, + }) +} + +/// Register the on-disconnect callback. +/// This will both stop the stream and call the error callback with DeviceNotAvailable. +/// This function should only be called once per stream. +fn add_disconnect_listener( + stream: &Stream, + error_callback: Arc>, +) -> Result<(), BuildStreamError> +where + E: FnMut(StreamError) + Send + 'static, +{ + let stream_inner_weak = Rc::downgrade(&stream.inner); + let mut stream_inner = stream.inner.borrow_mut(); + stream_inner._disconnect_listener = Some(AudioObjectPropertyListener::new( + stream_inner.device_id, + AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceIsAlive, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMaster, + }, + move || { + if let Some(stream_inner_strong) = stream_inner_weak.upgrade() { + match stream_inner_strong.try_borrow_mut() { + Ok(mut stream_inner) => { + let _ = stream_inner.pause(); + } + Err(_) => { + // Could not acquire mutable borrow. This can occur if there are + // overlapping borrows, if the stream is already in use, or if a panic + // occurred during a previous borrow. Still notify about device + // disconnection even if we can't pause. + } + } + (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); + } + }, + )?); + Ok(()) +} + +impl DeviceTrait for Device { + type SupportedInputConfigs = SupportedInputConfigs; + type SupportedOutputConfigs = SupportedOutputConfigs; + type Stream = Stream; + + fn name(&self) -> Result { + Device::name(self) + } + + fn supported_input_configs( + &self, + ) -> Result { + Device::supported_input_configs(self) + } + + fn supported_output_configs( + &self, + ) -> Result { + Device::supported_output_configs(self) + } + + fn default_input_config(&self) -> Result { + Device::default_input_config(self) + } + + fn default_output_config(&self) -> Result { + Device::default_output_config(self) + } + + 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, + { + Device::build_input_stream_raw( + self, + config, + sample_format, + data_callback, + 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, + { + Device::build_output_stream_raw( + self, + config, + sample_format, + data_callback, + error_callback, + timeout, + ) + } +} + +#[derive(Clone, Eq, PartialEq)] +pub struct Device { + pub(crate) audio_device_id: AudioDeviceID, +} + +pub fn is_default_device(device: &Device) -> bool { + default_input_device() + .map(|d| d.audio_device_id == device.audio_device_id) + .unwrap_or(false) + || default_output_device() + .map(|d| d.audio_device_id == device.audio_device_id) + .unwrap_or(false) +} + +impl Device { + /// Construct a new device given its ID. + /// Useful for constructing hidden devices. + pub fn new(audio_device_id: AudioDeviceID) -> Self { + Self { audio_device_id } + } + + fn name(&self) -> Result { + get_device_name(self.audio_device_id).map_err(|err| DeviceNameError::BackendSpecific { + err: BackendSpecificError { + description: err.to_string(), + }, + }) + } + + // Logic re-used between `supported_input_configs` and `supported_output_configs`. + #[allow(clippy::cast_ptr_alignment)] + fn supported_configs( + &self, + scope: AudioObjectPropertyScope, + ) -> Result { + let mut property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamConfiguration, + mScope: scope, + mElement: kAudioObjectPropertyElementMaster, + }; + + unsafe { + // Retrieve the devices audio buffer list. + let data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + ); + check_os_status(status)?; + + let mut audio_buffer_list: Vec = vec![]; + audio_buffer_list.reserve_exact(data_size as usize); + let status = AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), + ); + check_os_status(status)?; + + let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList; + + // If there's no buffers, skip. + if (*audio_buffer_list).mNumberBuffers == 0 { + return Ok(vec![].into_iter()); + } + + // Count the number of channels as the sum of all channels in all output buffers. + let n_buffers = (*audio_buffer_list).mNumberBuffers as usize; + let first: *const AudioBuffer = (*audio_buffer_list).mBuffers.as_ptr(); + let buffers: &[AudioBuffer] = slice::from_raw_parts(first, n_buffers); + let mut n_channels = 0; + for buffer in buffers { + n_channels += buffer.mNumberChannels as usize; + } + + // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using + // I16 but just use F32 for now as it's the default anyway. + let sample_format = SampleFormat::F32; + + // Get available sample rate ranges. + // The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of + // minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear. + // This may cause issues when, for example, sorting the configs by the sample rates. + // We follows the implementation of RtAudio, which returns single element of config + // when all the pairs have the same values and returns multiple elements otherwise. + // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39 + + property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; + let data_size = 0u32; + let status = AudioObjectGetPropertyDataSize( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + ); + check_os_status(status)?; + + let n_ranges = data_size as usize / mem::size_of::(); + let mut ranges: Vec = vec![]; + ranges.reserve_exact(data_size as usize); + let status = AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), + ); + check_os_status(status)?; + + let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; + let ranges: &[AudioValueRange] = slice::from_raw_parts(ranges, n_ranges); + + #[allow(non_upper_case_globals)] + let input = match scope { + kAudioObjectPropertyScopeInput => Ok(true), + kAudioObjectPropertyScopeOutput => Ok(false), + _ => Err(BackendSpecificError { + description: format!("unexpected scope (neither input nor output): {scope:?}"), + }), + }?; + let audio_unit = audio_unit_from_device(self, input)?; + let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; + + // Collect the supported formats for the device. + + let contains_different_sample_rates = ranges.iter().any(|r| r.mMinimum != r.mMaximum); + if ranges.is_empty() { + Ok(vec![].into_iter()) + } else if contains_different_sample_rates { + let res = ranges.iter().map(|range| SupportedStreamConfigRange { + channels: n_channels as ChannelCount, + min_sample_rate: SampleRate(range.mMinimum as u32), + max_sample_rate: SampleRate(range.mMaximum as u32), + buffer_size, + sample_format, + }); + Ok(res.collect::>().into_iter()) + } else { + let fmt = SupportedStreamConfigRange { + channels: n_channels as ChannelCount, + min_sample_rate: SampleRate( + ranges + .iter() + .map(|v| v.mMinimum as u32) + .min() + .expect("the list must not be empty"), + ), + max_sample_rate: SampleRate( + ranges + .iter() + .map(|v| v.mMaximum as u32) + .max() + .expect("the list must not be empty"), + ), + buffer_size, + sample_format, + }; + + Ok(vec![fmt].into_iter()) + } + } + } + + fn supported_input_configs( + &self, + ) -> Result { + self.supported_configs(kAudioObjectPropertyScopeInput) + } + + fn supported_output_configs( + &self, + ) -> Result { + self.supported_configs(kAudioObjectPropertyScopeOutput) + } + + fn default_config( + &self, + scope: AudioObjectPropertyScope, + ) -> Result { + fn default_config_error_from_os_status( + status: OSStatus, + ) -> Result<(), DefaultStreamConfigError> { + let err = match coreaudio::Error::from_os_status(status) { + Err(err) => err, + Ok(_) => return Ok(()), + }; + match err { + coreaudio::Error::AudioUnit( + coreaudio::error::AudioUnitError::FormatNotSupported, + ) + | coreaudio::Error::AudioCodec(_) + | coreaudio::Error::AudioFormat(_) => { + Err(DefaultStreamConfigError::StreamTypeNotSupported) + } + coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => { + Err(DefaultStreamConfigError::DeviceNotAvailable) + } + err => { + let description = format!("{err}"); + let err = BackendSpecificError { description }; + Err(err.into()) + } + } + } + + let property_address = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyStreamFormat, + mScope: scope, + mElement: kAudioObjectPropertyElementMaster, + }; + + unsafe { + let mut asbd: AudioStreamBasicDescription = mem::zeroed(); + let data_size = mem::size_of::() as u32; + let status = AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property_address), + 0, + null(), + NonNull::from(&data_size), + NonNull::from(&mut asbd).cast(), + ); + default_config_error_from_os_status(status)?; + + let sample_format = { + let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag( + asbd.mFormatID, + Some(asbd.mFormatFlags), + ); + let flags = match audio_format { + Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags, + _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), + }; + let maybe_sample_format = + coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample( + flags, + asbd.mBitsPerChannel, + ); + match maybe_sample_format { + Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, + Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, + _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), + } + }; + + #[allow(non_upper_case_globals)] + let input = match scope { + kAudioObjectPropertyScopeInput => Ok(true), + kAudioObjectPropertyScopeOutput => Ok(false), + _ => Err(BackendSpecificError { + description: format!("unexpected scope (neither input nor output): {scope:?}"), + }), + }?; + let audio_unit = audio_unit_from_device(self, input)?; + let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; + + let config = SupportedStreamConfig { + sample_rate: SampleRate(asbd.mSampleRate as _), + channels: asbd.mChannelsPerFrame as _, + buffer_size, + sample_format, + }; + Ok(config) + } + } + + fn default_input_config(&self) -> Result { + self.default_config(kAudioObjectPropertyScopeInput) + } + + fn default_output_config(&self) -> Result { + self.default_config(kAudioObjectPropertyScopeOutput) + } + + /// Check if this device supports input (recording). + fn supports_input(&self) -> bool { + // Check if the device has input channels by trying to get its input configuration + self.supported_input_configs() + .map(|mut configs| configs.next().is_some()) + .unwrap_or(false) + } +} + +impl fmt::Debug for Device { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("Device") + .field("audio_device_id", &self.audio_device_id) + .field("name", &self.name()) + .finish() + } +} + +impl Device { + #[allow(clippy::cast_ptr_alignment)] + #[allow(clippy::while_immutable_condition)] + #[allow(clippy::float_cmp)] + fn build_input_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + // The scope and element for working with a device's input stream. + let scope = Scope::Output; + let element = Element::Input; + + // Potentially change the device sample rate to match the config. + set_sample_rate(self.audio_device_id, config.sample_rate)?; + + let mut loopback_aggregate: Option = None; + let mut audio_unit = if self.supports_input() { + audio_unit_from_device(self, true)? + } else { + loopback_aggregate.replace(LoopbackDevice::from_device(self)?); + audio_unit_from_device(&loopback_aggregate.as_ref().unwrap().aggregate_device, true)? + }; + + // Set the stream in interleaved mode. + let asbd = asbd_from_config(config, sample_format); + audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; + + // Set the buffersize + match config.buffer_size { + BufferSize::Fixed(v) => { + let buffer_size_range = get_io_buffer_frame_size_range(&audio_unit)?; + match buffer_size_range { + SupportedBufferSize::Range { min, max } => { + if v >= min && v <= max { + audio_unit.set_property( + kAudioDevicePropertyBufferFrameSize, + scope, + element, + Some(&v), + )? + } else { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + SupportedBufferSize::Unknown => (), + } + } + BufferSize::Default => (), + } + + let error_callback = Arc::new(Mutex::new(error_callback)); + let error_callback_disconnect = error_callback.clone(); + + // Register the callback that is being called by coreaudio whenever it needs data to be + // fed to the audio buffer. + let bytes_per_channel = sample_format.sample_size(); + let sample_rate = config.sample_rate; + type Args = render_callback::Args; + audio_unit.set_input_callback(move |args: Args| unsafe { + let ptr = (*args.data.data).mBuffers.as_ptr(); + let len = (*args.data.data).mNumberBuffers as usize; + let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len); + + // TODO: Perhaps loop over all buffers instead? + let AudioBuffer { + mNumberChannels: channels, + mDataByteSize: data_byte_size, + mData: data, + } = buffers[0]; + + let data = data as *mut (); + let len = data_byte_size as usize / bytes_per_channel; + let data = Data::from_parts(data, len, sample_format); + + // TODO: Need a better way to get delay, for now we assume a double-buffer offset. + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + (error_callback.lock().unwrap())(err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + let buffer_frames = len / channels as usize; + let delay = frames_to_duration(buffer_frames, sample_rate); + let capture = callback + .sub(delay) + .expect("`capture` occurs before origin of alsa `StreamInstant`"); + let timestamp = crate::InputStreamTimestamp { callback, capture }; + + let info = InputCallbackInfo { timestamp }; + data_callback(&data, &info); + Ok(()) + })?; + + let stream = Stream::new(StreamInner { + playing: true, + _disconnect_listener: None, + audio_unit, + device_id: self.audio_device_id, + _loopback_device: loopback_aggregate, + }); + + // If we didn't request the default device, stop the stream if the + // device disconnects. + if !is_default_device(self) { + add_disconnect_listener(&stream, error_callback_disconnect)?; + } + + stream.inner.borrow_mut().audio_unit.start()?; + + Ok(stream) + } + + fn build_output_stream_raw( + &self, + config: &StreamConfig, + sample_format: SampleFormat, + mut data_callback: D, + error_callback: E, + _timeout: Option, + ) -> Result + where + D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, + E: FnMut(StreamError) + Send + 'static, + { + let mut audio_unit = audio_unit_from_device(self, false)?; + + // The scope and element for working with a device's output stream. + let scope = Scope::Input; + let element = Element::Output; + + // Set the stream in interleaved mode. + let asbd = asbd_from_config(config, sample_format); + audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; + + // Set the buffersize + match config.buffer_size { + BufferSize::Fixed(v) => { + let buffer_size_range = get_io_buffer_frame_size_range(&audio_unit)?; + match buffer_size_range { + SupportedBufferSize::Range { min, max } => { + if v >= min && v <= max { + audio_unit.set_property( + kAudioDevicePropertyBufferFrameSize, + scope, + element, + Some(&v), + )? + } else { + return Err(BuildStreamError::StreamConfigNotSupported); + } + } + SupportedBufferSize::Unknown => (), + } + } + BufferSize::Default => (), + } + + let error_callback = Arc::new(Mutex::new(error_callback)); + let error_callback_disconnect = error_callback.clone(); + + // Register the callback that is being called by coreaudio whenever it needs data to be + // fed to the audio buffer. + let bytes_per_channel = sample_format.sample_size(); + let sample_rate = config.sample_rate; + type Args = render_callback::Args; + audio_unit.set_render_callback(move |args: Args| unsafe { + // If `run()` is currently running, then a callback will be available from this list. + // Otherwise, we just fill the buffer with zeroes and return. + + let AudioBuffer { + mNumberChannels: channels, + mDataByteSize: data_byte_size, + mData: data, + } = (*args.data.data).mBuffers[0]; + + let data = data as *mut (); + let len = data_byte_size as usize / bytes_per_channel; + let mut data = Data::from_parts(data, len, sample_format); + + let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { + Err(err) => { + (error_callback.lock().unwrap())(err.into()); + return Err(()); + } + Ok(cb) => cb, + }; + // TODO: Need a better way to get delay, for now we assume a double-buffer offset. + let buffer_frames = len / channels as usize; + let delay = frames_to_duration(buffer_frames, sample_rate); + let playback = callback + .add(delay) + .expect("`playback` occurs beyond representation supported by `StreamInstant`"); + let timestamp = crate::OutputStreamTimestamp { callback, playback }; + + let info = OutputCallbackInfo { timestamp }; + data_callback(&mut data, &info); + Ok(()) + })?; + + let stream = Stream::new(StreamInner { + playing: true, + _disconnect_listener: None, + audio_unit, + device_id: self.audio_device_id, + _loopback_device: None, + }); + + // If we didn't request the default device, stop the stream if the + // device disconnects. + if !is_default_device(self) { + add_disconnect_listener(&stream, error_callback_disconnect)?; + } + + stream.inner.borrow_mut().audio_unit.start()?; + + Ok(stream) + } +} diff --git a/src/host/coreaudio/macos/loopback.rs b/src/host/coreaudio/macos/loopback.rs new file mode 100644 index 000000000..34f6ca6ca --- /dev/null +++ b/src/host/coreaudio/macos/loopback.rs @@ -0,0 +1,250 @@ +//! Manages loopback recording (recording system audio output) + +use super::device::Device; +use crate::{ + host::coreaudio::check_os_status, traits::DeviceTrait, BackendSpecificError, BuildStreamError, +}; +use objc2::{rc::Retained, AnyThread}; +use objc2_core_audio::{ + kAudioAggregateDeviceNameKey, kAudioAggregateDeviceTapAutoStartKey, + kAudioAggregateDeviceTapListKey, kAudioAggregateDeviceUIDKey, kAudioDevicePropertyDeviceUID, + kAudioEndPointDeviceIsPrivateKey, kAudioObjectPropertyElementMain, + kAudioObjectPropertyScopeGlobal, kAudioSubTapDriftCompensationKey, kAudioSubTapUIDKey, + AudioHardwareCreateAggregateDevice, AudioHardwareCreateProcessTap, + AudioHardwareDestroyAggregateDevice, AudioHardwareDestroyProcessTap, + AudioObjectGetPropertyData, AudioObjectID, AudioObjectPropertyAddress, CATapDescription, + CATapMuteBehavior, +}; +use objc2_core_foundation::{ + kCFAllocatorDefault, kCFTypeArrayCallBacks, kCFTypeDictionaryKeyCallBacks, + kCFTypeDictionaryValueCallBacks, CFArray, CFDictionary, CFMutableDictionary, CFRetained, + CFString, CFStringCreateWithCString, +}; +use objc2_foundation::{ns_string, NSArray, NSNumber, NSString}; +use std::{ + ffi::{c_void, CStr}, + mem::MaybeUninit, + ptr::NonNull, +}; +type CFStringRef = *mut std::os::raw::c_void; + +impl Device { + fn uid(&self) -> Result, BackendSpecificError> { + let mut cfstring: CFStringRef = std::ptr::null_mut(); + let mut size = std::mem::size_of::() as u32; + + let property = AudioObjectPropertyAddress { + mSelector: kAudioDevicePropertyDeviceUID, + mScope: kAudioObjectPropertyScopeGlobal, + mElement: kAudioObjectPropertyElementMain, + }; + + let status = unsafe { + AudioObjectGetPropertyData( + self.audio_device_id, + NonNull::from(&property), + 0, + std::ptr::null(), + NonNull::from(&mut size), + NonNull::from(&mut cfstring).cast(), + ) + }; + check_os_status(status)?; + + if cfstring.is_null() { + return Err(BackendSpecificError { + description: "Device uid is null".to_string(), + }); + } + + let ns_string: Retained = unsafe { + // unwrap cause cfstring!=null as checked before + Retained::retain(cfstring as *mut NSString).unwrap() + }; + + Ok(ns_string) + } +} + +/// An aggregate device with tap for recording system output. +/// +/// Its main difference with [`Device`] is that it's destroyed when dropped. +/// +/// It also doesn't implement the [`DeviceTrait`] as users shouldn't be using it. Its +/// main purpose is to destroy the created aggregate device when loopback recording +/// is done. +#[derive(PartialEq, Eq)] +pub struct LoopbackDevice { + pub tap_id: AudioObjectID, + pub aggregate_device: Device, +} + +impl LoopbackDevice { + /// Create a [`LoopbackDevice`] that records the sound + /// output of `device`. + pub fn from_device(device: &Device) -> Result { + // 1 - Create tap + + // Empty list of processes as we want to record all processes + let processes = NSArray::new(); + let device_uid = device.uid()?; + let tap_desc = unsafe { + CATapDescription::initWithProcesses_andDeviceUID_withStream( + CATapDescription::alloc(), + &*processes, + device_uid.as_ref(), + 0, + ) + }; + unsafe { + tap_desc.setMuteBehavior(CATapMuteBehavior::Unmuted); // captured audio still goes to speakers + tap_desc.setName(ns_string!("cpal output recorder")); + tap_desc.setPrivate(true); // the Aggregate Device would be private + tap_desc.setExclusive(true); // the process list means exclude them + }; + + let mut tap_obj_id: MaybeUninit = MaybeUninit::uninit(); + let tap_obj_id = unsafe { + AudioHardwareCreateProcessTap(Some(tap_desc.as_ref()), tap_obj_id.as_mut_ptr()); + tap_obj_id.assume_init() + }; + let tap_uid = unsafe { tap_desc.UUID().UUIDString() }; + + // 2 - Create aggregate device + let aggregate_device_properties = create_audio_aggregate_device_properties(tap_uid); + let aggregate_device_id: AudioObjectID = 0; + let status = unsafe { + AudioHardwareCreateAggregateDevice( + aggregate_device_properties.as_ref(), + NonNull::from(&aggregate_device_id), + ) + }; + check_os_status(status)?; + + Ok(Self { + tap_id: tap_obj_id, + aggregate_device: Device::new(aggregate_device_id), + }) + } +} + +impl Drop for LoopbackDevice { + fn drop(&mut self) { + unsafe { + // We don't check status to avoid panic during `drop` + let _status = + AudioHardwareDestroyAggregateDevice(self.aggregate_device.audio_device_id); + let _status = AudioHardwareDestroyProcessTap(self.tap_id); + } + } +} + +fn to_cfstring(cstr: &'static CStr) -> CFRetained { + unsafe { + CFStringCreateWithCString( + kCFAllocatorDefault, + cstr.as_ptr(), + 0x08000100, /* UTF8 */ + ) + } + .unwrap() +} + +/// Rust reimplementation of the following: +/// ```c +/// tap_uid = [[tap_description UUID] UUIDString]; +/// taps = @[ +/// @{ +/// @kAudioSubTapUIDKey : (NSString*)tap_uid, +/// @kAudioSubTapDriftCompensationKey : @YES, +/// }, +/// ]; +/// +/// aggregate_device_properties = @{ +/// @kAudioAggregateDeviceNameKey : @"MiniMetersAggregateDevice", +/// @kAudioAggregateDeviceUIDKey : +/// @"com.josephlyncheski.MiniMetersAggregateDevice", +/// @kAudioAggregateDeviceTapListKey : taps, +/// @kAudioAggregateDeviceTapAutoStartKey : @NO, +/// // If we set this to NO then I believe we need to make the Tap public as +/// // well. +/// @kAudioAggregateDeviceIsPrivateKey : @YES, +/// }; +/// ``` +pub fn create_audio_aggregate_device_properties( + tap_uid: Retained, +) -> CFRetained { + let tap_inner = unsafe { + let dict = CFMutableDictionary::new( + kCFAllocatorDefault, + 2, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ) + .unwrap(); + + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioSubTapUIDKey) as *const _ as *const c_void, + &*tap_uid as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioSubTapDriftCompensationKey) as *const _ as *const c_void, + &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void, + ); + + dict + }; + let _taps_list = [tap_inner]; + let taps = unsafe { + CFArray::new( + kCFAllocatorDefault, + _taps_list.as_ptr() as *mut *const c_void, + _taps_list.len() as _, + &kCFTypeArrayCallBacks, + ) + .unwrap() + }; + let aggregate_dev_properties = unsafe { + let dict = CFMutableDictionary::new( + kCFAllocatorDefault, + 5, + &kCFTypeDictionaryKeyCallBacks, + &kCFTypeDictionaryValueCallBacks, + ) + .unwrap(); + + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceNameKey) as *const _ as *const c_void, + &*CFString::from_str("Cpal loopback record aggregate device") as *const _ + as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceUIDKey) as *const _ as *const c_void, + &*CFString::from_str("com.cpal.LoopbackRecordAggregateDevice") as *const _ + as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceTapListKey) as *const _ as *const c_void, + &*taps as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioAggregateDeviceTapAutoStartKey) as *const _ as *const c_void, + &*NSNumber::initWithBool(NSNumber::alloc(), false) as *const _ as *const c_void, + ); + CFMutableDictionary::set_value( + Some(dict.as_ref()), + &*to_cfstring(kAudioEndPointDeviceIsPrivateKey) as *const _ as *const c_void, + &*NSNumber::initWithBool(NSNumber::alloc(), true) as *const _ as *const c_void, + ); + + CFRetained::cast_unchecked::(dict) + }; + + aggregate_dev_properties +} diff --git a/src/host/coreaudio/macos/mod.rs b/src/host/coreaudio/macos/mod.rs index d83734d92..e29d05768 100644 --- a/src/host/coreaudio/macos/mod.rs +++ b/src/host/coreaudio/macos/mod.rs @@ -2,53 +2,23 @@ use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant}; use super::OSStatus; -use crate::traits::{DeviceTrait, HostTrait, StreamTrait}; -use crate::{ - BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, - DefaultStreamConfigError, DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, - PauseStreamError, PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, - SupportedBufferSize, SupportedStreamConfig, SupportedStreamConfigRange, - SupportedStreamConfigsError, -}; -use coreaudio::audio_unit::render_callback::{self, data}; -use coreaudio::audio_unit::{AudioUnit, Element, Scope}; -use objc2_audio_toolbox::{ - kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, - kAudioUnitProperty_StreamFormat, -}; -use objc2_core_audio::{ - kAudioDevicePropertyAvailableNominalSampleRates, kAudioDevicePropertyBufferFrameSize, - kAudioDevicePropertyBufferFrameSizeRange, kAudioDevicePropertyDeviceIsAlive, - kAudioDevicePropertyNominalSampleRate, kAudioDevicePropertyStreamConfiguration, - kAudioDevicePropertyStreamFormat, kAudioObjectPropertyElementMaster, - kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, - kAudioObjectPropertyScopeOutput, AudioDeviceID, AudioObjectGetPropertyData, - AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, - AudioObjectPropertyScope, AudioObjectSetPropertyData, -}; -use objc2_core_audio_types::{ - AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, -}; +use crate::host::coreaudio::macos::loopback::LoopbackDevice; +use crate::traits::{HostTrait, StreamTrait}; +use crate::{BackendSpecificError, DevicesError, PauseStreamError, PlayStreamError}; +use coreaudio::audio_unit::AudioUnit; +use objc2_core_audio::AudioDeviceID; use std::cell::RefCell; -use std::fmt; -use std::mem; -use std::ptr::{null, NonNull}; use std::rc::Rc; -use std::slice; -use std::sync::mpsc::{channel, RecvTimeoutError}; -use std::sync::{Arc, Mutex}; -use std::time::{Duration, Instant}; -pub use self::enumerate::{ - default_input_device, default_output_device, Devices, SupportedInputConfigs, - SupportedOutputConfigs, -}; +pub use self::enumerate::{default_input_device, default_output_device, Devices}; -use coreaudio::audio_unit::macos_helpers::get_device_name; use property_listener::AudioObjectPropertyListener; +mod device; pub mod enumerate; +mod loopback; mod property_listener; +pub use device::Device; /// Coreaudio host, the default host on macOS. #[derive(Debug)] @@ -82,375 +52,6 @@ impl HostTrait for Host { } } -impl DeviceTrait for Device { - type SupportedInputConfigs = SupportedInputConfigs; - type SupportedOutputConfigs = SupportedOutputConfigs; - type Stream = Stream; - - fn name(&self) -> Result { - Device::name(self) - } - - fn supported_input_configs( - &self, - ) -> Result { - Device::supported_input_configs(self) - } - - fn supported_output_configs( - &self, - ) -> Result { - Device::supported_output_configs(self) - } - - fn default_input_config(&self) -> Result { - Device::default_input_config(self) - } - - fn default_output_config(&self) -> Result { - Device::default_output_config(self) - } - - 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, - { - Device::build_input_stream_raw( - self, - config, - sample_format, - data_callback, - 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, - { - Device::build_output_stream_raw( - self, - config, - sample_format, - data_callback, - error_callback, - timeout, - ) - } -} - -#[derive(Clone, PartialEq, Eq)] -pub struct Device { - pub(crate) audio_device_id: AudioDeviceID, -} - -fn is_default_device(device: &Device) -> bool { - default_input_device() - .map(|d| d.audio_device_id == device.audio_device_id) - .unwrap_or(false) - || default_output_device() - .map(|d| d.audio_device_id == device.audio_device_id) - .unwrap_or(false) -} - -impl Device { - /// Construct a new device given its ID. - /// Useful for constructing hidden devices. - pub fn new(audio_device_id: AudioDeviceID) -> Self { - Self { audio_device_id } - } - - fn name(&self) -> Result { - get_device_name(self.audio_device_id).map_err(|err| DeviceNameError::BackendSpecific { - err: BackendSpecificError { - description: err.to_string(), - }, - }) - } - - // Logic re-used between `supported_input_configs` and `supported_output_configs`. - #[allow(clippy::cast_ptr_alignment)] - fn supported_configs( - &self, - scope: AudioObjectPropertyScope, - ) -> Result { - let mut property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyStreamConfiguration, - mScope: scope, - mElement: kAudioObjectPropertyElementMaster, - }; - - unsafe { - // Retrieve the devices audio buffer list. - let data_size = 0u32; - let status = AudioObjectGetPropertyDataSize( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - ); - check_os_status(status)?; - - let mut audio_buffer_list: Vec = vec![]; - audio_buffer_list.reserve_exact(data_size as usize); - let status = AudioObjectGetPropertyData( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), - ); - check_os_status(status)?; - - let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList; - - // If there's no buffers, skip. - if (*audio_buffer_list).mNumberBuffers == 0 { - return Ok(vec![].into_iter()); - } - - // Count the number of channels as the sum of all channels in all output buffers. - let n_buffers = (*audio_buffer_list).mNumberBuffers as usize; - let first: *const AudioBuffer = (*audio_buffer_list).mBuffers.as_ptr(); - let buffers: &'static [AudioBuffer] = slice::from_raw_parts(first, n_buffers); - let mut n_channels = 0; - for buffer in buffers { - n_channels += buffer.mNumberChannels as usize; - } - - // TODO: macOS should support I8, I16, I24, I32, F32 and F64. This should allow for - // using I16 but just use F32 for now as it's the default anyway. - let sample_format = SampleFormat::F32; - - // Get available sample rate ranges. - // The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of - // minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear. - // This may cause issues when, for example, sorting the configs by the sample rates. - // We follows the implementation of RtAudio, which returns single element of config - // when all the pairs have the same values and returns multiple elements otherwise. - // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39 - - property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; - let data_size = 0u32; - let status = AudioObjectGetPropertyDataSize( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - ); - check_os_status(status)?; - - let n_ranges = data_size as usize / mem::size_of::(); - let mut ranges: Vec = vec![]; - ranges.reserve_exact(data_size as usize); - let status = AudioObjectGetPropertyData( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), - ); - check_os_status(status)?; - - let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; - let ranges: &'static [AudioValueRange] = slice::from_raw_parts(ranges, n_ranges); - - #[allow(non_upper_case_globals)] - let input = match scope { - kAudioObjectPropertyScopeInput => Ok(true), - kAudioObjectPropertyScopeOutput => Ok(false), - _ => Err(BackendSpecificError { - description: format!("unexpected scope (neither input nor output): {scope:?}"), - }), - }?; - let audio_unit = audio_unit_from_device(self, input)?; - let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; - - // Collect the supported formats for the device. - - let contains_different_sample_rates = ranges.iter().any(|r| r.mMinimum != r.mMaximum); - if ranges.is_empty() { - Ok(vec![].into_iter()) - } else if contains_different_sample_rates { - let res = ranges.iter().map(|range| SupportedStreamConfigRange { - channels: n_channels as ChannelCount, - min_sample_rate: SampleRate(range.mMinimum as u32), - max_sample_rate: SampleRate(range.mMaximum as u32), - buffer_size, - sample_format, - }); - Ok(res.collect::>().into_iter()) - } else { - let fmt = SupportedStreamConfigRange { - channels: n_channels as ChannelCount, - min_sample_rate: SampleRate( - ranges - .iter() - .map(|v| v.mMinimum as u32) - .min() - .expect("the list must not be empty"), - ), - max_sample_rate: SampleRate( - ranges - .iter() - .map(|v| v.mMaximum as u32) - .max() - .expect("the list must not be empty"), - ), - buffer_size, - sample_format, - }; - - Ok(vec![fmt].into_iter()) - } - } - } - - fn supported_input_configs( - &self, - ) -> Result { - self.supported_configs(kAudioObjectPropertyScopeInput) - } - - fn supported_output_configs( - &self, - ) -> Result { - self.supported_configs(kAudioObjectPropertyScopeOutput) - } - - fn default_config( - &self, - scope: AudioObjectPropertyScope, - ) -> Result { - fn default_config_error_from_os_status( - status: OSStatus, - ) -> Result<(), DefaultStreamConfigError> { - let err = match coreaudio::Error::from_os_status(status) { - Err(err) => err, - Ok(_) => return Ok(()), - }; - match err { - coreaudio::Error::AudioUnit( - coreaudio::error::AudioUnitError::FormatNotSupported, - ) - | coreaudio::Error::AudioCodec(_) - | coreaudio::Error::AudioFormat(_) => { - Err(DefaultStreamConfigError::StreamTypeNotSupported) - } - coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => { - Err(DefaultStreamConfigError::DeviceNotAvailable) - } - err => { - let description = format!("{err}"); - let err = BackendSpecificError { description }; - Err(err.into()) - } - } - } - - let property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyStreamFormat, - mScope: scope, - mElement: kAudioObjectPropertyElementMaster, - }; - - unsafe { - let mut asbd: AudioStreamBasicDescription = mem::zeroed(); - let data_size = mem::size_of::() as u32; - let status = AudioObjectGetPropertyData( - self.audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - NonNull::from(&mut asbd).cast(), - ); - default_config_error_from_os_status(status)?; - - let sample_format = { - let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag( - asbd.mFormatID, - Some(asbd.mFormatFlags), - ); - let flags = match audio_format { - Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags, - _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), - }; - let maybe_sample_format = - coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample( - flags, - asbd.mBitsPerChannel, - ); - match maybe_sample_format { - Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, - Some(coreaudio::audio_unit::SampleFormat::I8) => SampleFormat::I8, - Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, - Some(coreaudio::audio_unit::SampleFormat::I24) => SampleFormat::I24, - Some(coreaudio::audio_unit::SampleFormat::I32) => SampleFormat::I32, - _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), - } - }; - - #[allow(non_upper_case_globals)] - let input = match scope { - kAudioObjectPropertyScopeInput => Ok(true), - kAudioObjectPropertyScopeOutput => Ok(false), - _ => Err(BackendSpecificError { - description: format!("unexpected scope (neither input nor output): {scope:?}"), - }), - }?; - let audio_unit = audio_unit_from_device(self, input)?; - let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; - - let config = SupportedStreamConfig { - sample_rate: SampleRate(asbd.mSampleRate as _), - channels: asbd.mChannelsPerFrame as _, - buffer_size, - sample_format, - }; - Ok(config) - } - } - - fn default_input_config(&self) -> Result { - self.default_config(kAudioObjectPropertyScopeInput) - } - - fn default_output_config(&self) -> Result { - self.default_config(kAudioObjectPropertyScopeOutput) - } -} - -impl fmt::Debug for Device { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.debug_struct("Device") - .field("audio_device_id", &self.audio_device_id) - .field("name", &self.name()) - .finish() - } -} - struct StreamInner { playing: bool, audio_unit: AudioUnit, @@ -462,6 +63,9 @@ struct StreamInner { // a stream associated with the device. #[allow(dead_code)] device_id: AudioDeviceID, + /// Manage the lifetime of the aggregate device used + /// for loopback recording + _loopback_device: Option, } impl StreamInner { @@ -490,456 +94,6 @@ impl StreamInner { } } -/// Register the on-disconnect callback. -/// This will both stop the stream and call the error callback with DeviceNotAvailable. -/// This function should only be called once per stream. -fn add_disconnect_listener( - stream: &Stream, - error_callback: Arc>, -) -> Result<(), BuildStreamError> -where - E: FnMut(StreamError) + Send + 'static, -{ - let stream_inner_weak = Rc::downgrade(&stream.inner); - let mut stream_inner = stream.inner.borrow_mut(); - stream_inner._disconnect_listener = Some(AudioObjectPropertyListener::new( - stream_inner.device_id, - AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyDeviceIsAlive, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMaster, - }, - move || { - if let Some(stream_inner_strong) = stream_inner_weak.upgrade() { - match stream_inner_strong.try_borrow_mut() { - Ok(mut stream_inner) => { - let _ = stream_inner.pause(); - } - Err(_) => { - // Could not acquire mutable borrow. This can occur if there are - // overlapping borrows, if the stream is already in use, or if a panic - // occurred during a previous borrow. Still notify about device - // disconnection even if we can't pause. - } - } - (error_callback.lock().unwrap())(StreamError::DeviceNotAvailable); - } - }, - )?); - Ok(()) -} - -fn audio_unit_from_device(device: &Device, input: bool) -> Result { - let output_type = if is_default_device(device) && !input { - coreaudio::audio_unit::IOType::DefaultOutput - } else { - coreaudio::audio_unit::IOType::HalOutput - }; - let mut audio_unit = AudioUnit::new(output_type)?; - - if input { - // Enable input processing. - let enable_input = 1u32; - audio_unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Input, - Element::Input, - Some(&enable_input), - )?; - - // Disable output processing. - let disable_output = 0u32; - audio_unit.set_property( - kAudioOutputUnitProperty_EnableIO, - Scope::Output, - Element::Output, - Some(&disable_output), - )?; - } - - audio_unit.set_property( - kAudioOutputUnitProperty_CurrentDevice, - Scope::Global, - Element::Output, - Some(&device.audio_device_id), - )?; - - Ok(audio_unit) -} - -impl Device { - #[allow(clippy::cast_ptr_alignment)] - #[allow(clippy::while_immutable_condition)] - #[allow(clippy::float_cmp)] - fn build_input_stream_raw( - &self, - config: &StreamConfig, - sample_format: SampleFormat, - mut data_callback: D, - error_callback: E, - _timeout: Option, - ) -> Result - where - D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, - { - // The scope and element for working with a device's input stream. - let scope = Scope::Output; - let element = Element::Input; - - // Potentially change the device sample rate to match the config. - set_sample_rate(self.audio_device_id, config.sample_rate)?; - - let mut audio_unit = audio_unit_from_device(self, true)?; - - // Set the stream in interleaved mode. - let asbd = asbd_from_config(config, sample_format); - audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; - - // Set the buffersize - match config.buffer_size { - BufferSize::Fixed(v) => { - let buffer_size_range = get_io_buffer_frame_size_range(&audio_unit)?; - match buffer_size_range { - SupportedBufferSize::Range { min, max } => { - if v >= min && v <= max { - audio_unit.set_property( - kAudioDevicePropertyBufferFrameSize, - scope, - element, - Some(&v), - )? - } else { - return Err(BuildStreamError::StreamConfigNotSupported); - } - } - SupportedBufferSize::Unknown => (), - } - } - BufferSize::Default => (), - } - - let error_callback = Arc::new(Mutex::new(error_callback)); - let error_callback_disconnect = error_callback.clone(); - - // Register the callback that is being called by coreaudio whenever it needs data to be - // fed to the audio buffer. - let bytes_per_channel = sample_format.sample_size(); - let sample_rate = config.sample_rate; - type Args = render_callback::Args; - audio_unit.set_input_callback(move |args: Args| unsafe { - let ptr = (*args.data.data).mBuffers.as_ptr(); - let len = (*args.data.data).mNumberBuffers as usize; - let buffers: &[AudioBuffer] = slice::from_raw_parts(ptr, len); - - // TODO: Perhaps loop over all buffers instead? - let AudioBuffer { - mNumberChannels: channels, - mDataByteSize: data_byte_size, - mData: data, - } = buffers[0]; - - let data = data as *mut (); - let len = data_byte_size as usize / bytes_per_channel; - let data = Data::from_parts(data, len, sample_format); - - // TODO: Need a better way to get delay, for now we assume a double-buffer offset. - let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { - Err(err) => { - (error_callback.lock().unwrap())(err.into()); - return Err(()); - } - Ok(cb) => cb, - }; - let buffer_frames = len / channels as usize; - let delay = frames_to_duration(buffer_frames, sample_rate); - let capture = callback - .sub(delay) - .expect("`capture` occurs before origin of alsa `StreamInstant`"); - let timestamp = crate::InputStreamTimestamp { callback, capture }; - - let info = InputCallbackInfo { timestamp }; - data_callback(&data, &info); - Ok(()) - })?; - - let stream = Stream::new(StreamInner { - playing: true, - _disconnect_listener: None, - audio_unit, - device_id: self.audio_device_id, - }); - - // If we didn't request the default device, stop the stream if the - // device disconnects. - if !is_default_device(self) { - add_disconnect_listener(&stream, error_callback_disconnect)?; - } - - stream.inner.borrow_mut().audio_unit.start()?; - - Ok(stream) - } - - fn build_output_stream_raw( - &self, - config: &StreamConfig, - sample_format: SampleFormat, - mut data_callback: D, - error_callback: E, - _timeout: Option, - ) -> Result - where - D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, - E: FnMut(StreamError) + Send + 'static, - { - let mut audio_unit = audio_unit_from_device(self, false)?; - - // The scope and element for working with a device's output stream. - let scope = Scope::Input; - let element = Element::Output; - - // Set the stream in interleaved mode. - let asbd = asbd_from_config(config, sample_format); - audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; - - // Set the buffersize - match config.buffer_size { - BufferSize::Fixed(v) => { - let buffer_size_range = get_io_buffer_frame_size_range(&audio_unit)?; - match buffer_size_range { - SupportedBufferSize::Range { min, max } => { - if v >= min && v <= max { - audio_unit.set_property( - kAudioDevicePropertyBufferFrameSize, - scope, - element, - Some(&v), - )? - } else { - return Err(BuildStreamError::StreamConfigNotSupported); - } - } - SupportedBufferSize::Unknown => (), - } - } - BufferSize::Default => (), - } - - let error_callback = Arc::new(Mutex::new(error_callback)); - let error_callback_disconnect = error_callback.clone(); - - // Register the callback that is being called by coreaudio whenever it needs data to be - // fed to the audio buffer. - let bytes_per_channel = sample_format.sample_size(); - let sample_rate = config.sample_rate; - type Args = render_callback::Args; - audio_unit.set_render_callback(move |args: Args| unsafe { - // If `run()` is currently running, then a callback will be available from this list. - // Otherwise, we just fill the buffer with zeroes and return. - - let AudioBuffer { - mNumberChannels: channels, - mDataByteSize: data_byte_size, - mData: data, - } = (*args.data.data).mBuffers[0]; - - let data = data as *mut (); - let len = data_byte_size as usize / bytes_per_channel; - let mut data = Data::from_parts(data, len, sample_format); - - let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { - Err(err) => { - (error_callback.lock().unwrap())(err.into()); - return Err(()); - } - Ok(cb) => cb, - }; - // TODO: Need a better way to get delay, for now we assume a double-buffer offset. - let buffer_frames = len / channels as usize; - let delay = frames_to_duration(buffer_frames, sample_rate); - let playback = callback - .add(delay) - .expect("`playback` occurs beyond representation supported by `StreamInstant`"); - let timestamp = crate::OutputStreamTimestamp { callback, playback }; - - let info = OutputCallbackInfo { timestamp }; - data_callback(&mut data, &info); - Ok(()) - })?; - - let stream = Stream::new(StreamInner { - playing: true, - _disconnect_listener: None, - audio_unit, - device_id: self.audio_device_id, - }); - - // If we didn't request the default device, stop the stream if the - // device disconnects. - if !is_default_device(self) { - add_disconnect_listener(&stream, error_callback_disconnect)?; - } - - stream.inner.borrow_mut().audio_unit.start()?; - - Ok(stream) - } -} - -/// Attempt to set the device sample rate to the provided rate. -/// Return an error if the requested sample rate is not supported by the device. -fn set_sample_rate( - audio_device_id: AudioObjectID, - target_sample_rate: SampleRate, -) -> Result<(), BuildStreamError> { - // Get the current sample rate. - let mut property_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyNominalSampleRate, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMaster, - }; - let mut sample_rate: f64 = 0.0; - let data_size = mem::size_of::() as u32; - let status = unsafe { - AudioObjectGetPropertyData( - audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - NonNull::from(&mut sample_rate).cast(), - ) - }; - coreaudio::Error::from_os_status(status)?; - - // If the requested sample rate is different to the device sample rate, update the device. - if sample_rate as u32 != target_sample_rate.0 { - // Get available sample rate ranges. - property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; - let data_size = 0u32; - let status = unsafe { - AudioObjectGetPropertyDataSize( - audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - ) - }; - coreaudio::Error::from_os_status(status)?; - let n_ranges = data_size as usize / mem::size_of::(); - let mut ranges: Vec = vec![]; - ranges.reserve_exact(data_size as usize); - let status = unsafe { - AudioObjectGetPropertyData( - audio_device_id, - NonNull::from(&property_address), - 0, - null(), - NonNull::from(&data_size), - NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), - ) - }; - coreaudio::Error::from_os_status(status)?; - let ranges: *mut AudioValueRange = ranges.as_mut_ptr() as *mut _; - let ranges: &'static [AudioValueRange] = unsafe { slice::from_raw_parts(ranges, n_ranges) }; - - // Now that we have the available ranges, pick the one matching the desired rate. - let sample_rate = target_sample_rate.0; - let maybe_index = ranges - .iter() - .position(|r| r.mMinimum as u32 == sample_rate && r.mMaximum as u32 == sample_rate); - let range_index = match maybe_index { - None => return Err(BuildStreamError::StreamConfigNotSupported), - Some(i) => i, - }; - - let (send, recv) = channel::>(); - let sample_rate_address = AudioObjectPropertyAddress { - mSelector: kAudioDevicePropertyNominalSampleRate, - mScope: kAudioObjectPropertyScopeGlobal, - mElement: kAudioObjectPropertyElementMaster, - }; - // Send sample rate updates back on a channel. - let sample_rate_handler = move || { - let mut rate: f64 = 0.0; - let data_size = mem::size_of::() as u32; - - let result = unsafe { - AudioObjectGetPropertyData( - audio_device_id, - NonNull::from(&sample_rate_address), - 0, - null(), - NonNull::from(&data_size), - NonNull::from(&mut rate).cast(), - ) - }; - send.send(coreaudio::Error::from_os_status(result).map(|_| rate)) - .ok(); - }; - - let listener = AudioObjectPropertyListener::new( - audio_device_id, - sample_rate_address, - sample_rate_handler, - )?; - - // Finally, set the sample rate. - property_address.mSelector = kAudioDevicePropertyNominalSampleRate; - let status = unsafe { - AudioObjectSetPropertyData( - audio_device_id, - NonNull::from(&property_address), - 0, - null(), - data_size, - NonNull::from(&ranges[range_index]).cast(), - ) - }; - coreaudio::Error::from_os_status(status)?; - - // Wait for the reported_rate to change. - // - // This should not take longer than a few ms, but we timeout after 1 sec just in case. - // We loop over potentially several events from the channel to ensure - // that we catch the expected change in sample rate. - let mut timeout = Duration::from_secs(1); - let start = Instant::now(); - - loop { - match recv.recv_timeout(timeout) { - Err(err) => { - let description = match err { - RecvTimeoutError::Disconnected => { - "sample rate listener channel disconnected unexpectedly" - } - RecvTimeoutError::Timeout => { - "timeout waiting for sample rate update for device" - } - } - .to_string(); - return Err(BackendSpecificError { description }.into()); - } - Ok(Ok(reported_sample_rate)) => { - if reported_sample_rate == target_sample_rate.0 as f64 { - break; - } - } - Ok(Err(_)) => { - // TODO: should we consider collecting this error? - } - }; - timeout = timeout - .checked_sub(start.elapsed()) - .unwrap_or(Duration::ZERO); - } - listener.remove()?; - } - Ok(()) -} - #[derive(Clone)] pub struct Stream { inner: Rc>, @@ -967,17 +121,106 @@ impl StreamTrait for Stream { } } -fn get_io_buffer_frame_size_range( - audio_unit: &AudioUnit, -) -> Result { - let buffer_size_range: AudioValueRange = audio_unit.get_property( - kAudioDevicePropertyBufferFrameSizeRange, - Scope::Global, - Element::Output, - )?; - - Ok(SupportedBufferSize::Range { - min: buffer_size_range.mMinimum as u32, - max: buffer_size_range.mMaximum as u32, - }) +#[cfg(test)] +mod test { + use crate::{ + default_host, + traits::{DeviceTrait, HostTrait, StreamTrait}, + Sample, + }; + + #[test] + fn test_play() { + let host = default_host(); + let device = host.default_output_device().unwrap(); + + let mut supported_configs_range = device.supported_output_configs().unwrap(); + let supported_config = supported_configs_range + .next() + .unwrap() + .with_max_sample_rate(); + let config = supported_config.config(); + + let stream = device + .build_output_stream( + &config, + write_silence::, + move |err| println!("Error: {err}"), + None, // None=blocking, Some(Duration)=timeout + ) + .unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + #[test] + fn test_record() { + let host = default_host(); + let device = host.default_input_device().unwrap(); + println!("Device: {:?}", device.name()); + + let mut supported_configs_range = device.supported_input_configs().unwrap(); + println!("Supported configs:"); + for config in supported_configs_range.clone() { + println!("{:?}", config) + } + let supported_config = supported_configs_range + .next() + .unwrap() + .with_max_sample_rate(); + let config = supported_config.config(); + + let stream = device + .build_input_stream( + &config, + move |data: &[f32], _: &crate::InputCallbackInfo| { + // react to stream events and read or write stream data here. + println!("Got data: {:?}", &data[..25]); + }, + move |err| println!("Error: {err}"), + None, // None=blocking, Some(Duration)=timeout + ) + .unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + #[test] + fn test_record_output() { + if std::env::var("CI").is_ok() { + println!("Skipping test_record_output in CI environment due to permissions"); + return; + } + + let host = default_host(); + let device = host.default_output_device().unwrap(); + + let mut supported_configs_range = device.supported_output_configs().unwrap(); + let supported_config = supported_configs_range + .next() + .unwrap() + .with_max_sample_rate(); + let config = supported_config.config(); + + println!("Building input stream"); + let stream = device + .build_input_stream( + &config, + move |data: &[f32], _: &crate::InputCallbackInfo| { + // react to stream events and read or write stream data here. + println!("Got data: {:?}", &data[..25]); + }, + move |err| println!("Error: {err}"), + None, // None=blocking, Some(Duration)=timeout + ) + .unwrap(); + stream.play().unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + } + + fn write_silence(data: &mut [T], _: &crate::OutputCallbackInfo) { + for sample in data.iter_mut() { + *sample = Sample::EQUILIBRIUM; + } + } }