| Lift freeq's AV media plane out of sleek 90f8b89 nandi 20d ago | 1 | //! CoreAudio backend implementation. |
| 2 | //! |
| 3 | //! Default backend on macOS and iOS. |
| 4 | |
| 5 | use objc2_core_audio_types::{ |
| 6 | kAudioFormatFlagIsFloat, kAudioFormatFlagIsPacked, kAudioFormatFlagIsSignedInteger, |
| 7 | kAudioFormatLinearPCM, AudioStreamBasicDescription, |
| 8 | }; |
| 9 | |
| 10 | use crate::DefaultStreamConfigError; |
| 11 | use crate::{BuildStreamError, SupportedStreamConfigsError}; |
| 12 | |
| 13 | use crate::{BackendSpecificError, SampleFormat, StreamConfig}; |
| 14 | |
| 15 | #[cfg(target_os = "ios")] |
| 16 | mod ios; |
| 17 | #[cfg(target_os = "macos")] |
| 18 | mod macos; |
| 19 | |
| 20 | #[cfg(target_os = "ios")] |
| 21 | #[allow(unused_imports)] |
| 22 | pub use self::ios::{ |
| 23 | enumerate::{Devices, SupportedInputConfigs, SupportedOutputConfigs}, |
| 24 | Device, Host, Stream, |
| 25 | }; |
| 26 | |
| 27 | #[cfg(target_os = "macos")] |
| 28 | pub use self::macos::{Host, Stream}; |
| 29 | |
| 30 | // Common helper methods used by both macOS and iOS |
| 31 | |
| 32 | fn check_os_status(os_status: OSStatus) -> Result<(), BackendSpecificError> { |
| 33 | match coreaudio::Error::from_os_status(os_status) { |
| 34 | Ok(()) => Ok(()), |
| 35 | Err(err) => { |
| 36 | let description = err.to_string(); |
| 37 | Err(BackendSpecificError { description }) |
| 38 | } |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | // Create a coreaudio AudioStreamBasicDescription from a CPAL Format. |
| 43 | fn asbd_from_config( |
| 44 | config: StreamConfig, |
| 45 | sample_format: SampleFormat, |
| 46 | ) -> AudioStreamBasicDescription { |
| 47 | let n_channels = config.channels as usize; |
| 48 | let sample_rate = config.sample_rate; |
| 49 | let bytes_per_channel = sample_format.sample_size(); |
| 50 | let bits_per_channel = bytes_per_channel * 8; |
| 51 | let bytes_per_frame = n_channels * bytes_per_channel; |
| 52 | let frames_per_packet = 1; |
| 53 | let bytes_per_packet = frames_per_packet * bytes_per_frame; |
| 54 | let format_flags = match sample_format { |
| 55 | SampleFormat::F32 | SampleFormat::F64 => kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked, |
| 56 | SampleFormat::I8 |
| 57 | | SampleFormat::I16 |
| 58 | | SampleFormat::I24 |
| 59 | | SampleFormat::I32 |
| 60 | | SampleFormat::I64 => kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked, |
| 61 | _ => kAudioFormatFlagIsPacked, |
| 62 | }; |
| 63 | AudioStreamBasicDescription { |
| 64 | mBitsPerChannel: bits_per_channel as _, |
| 65 | mBytesPerFrame: bytes_per_frame as _, |
| 66 | mChannelsPerFrame: n_channels as _, |
| 67 | mBytesPerPacket: bytes_per_packet as _, |
| 68 | mFramesPerPacket: frames_per_packet as _, |
| 69 | mFormatFlags: format_flags, |
| 70 | mFormatID: kAudioFormatLinearPCM, |
| 71 | mSampleRate: sample_rate as _, |
| 72 | mReserved: 0, |
| 73 | } |
| 74 | } |
| 75 | |
| 76 | #[inline] |
| 77 | fn host_time_to_stream_instant( |
| 78 | m_host_time: u64, |
| 79 | ) -> Result<crate::StreamInstant, BackendSpecificError> { |
| 80 | let mut info: mach2::mach_time::mach_timebase_info = Default::default(); |
| 81 | let res = unsafe { mach2::mach_time::mach_timebase_info(&mut info) }; |
| 82 | check_os_status(res)?; |
| 83 | let nanos = m_host_time as u128 * info.numer as u128 / info.denom as u128; |
| 84 | crate::StreamInstant::from_nanos_i128(nanos as i128).ok_or(BackendSpecificError { |
| 85 | description: "host time out of range of `StreamInstant` representation".to_string(), |
| 86 | }) |
| 87 | } |
| 88 | |
| 89 | // Convert the given duration in frames at the given sample rate to a `std::time::Duration`. |
| 90 | #[inline] |
| 91 | fn frames_to_duration(frames: usize, rate: crate::SampleRate) -> std::time::Duration { |
| 92 | let secsf = frames as f64 / rate as f64; |
| 93 | let secs = secsf as u64; |
| 94 | let nanos = ((secsf - secs as f64) * 1_000_000_000.0) as u32; |
| 95 | std::time::Duration::new(secs, nanos) |
| 96 | } |
| 97 | |
| 98 | // TODO need stronger error identification |
| 99 | impl From<coreaudio::Error> for BuildStreamError { |
| 100 | fn from(err: coreaudio::Error) -> BuildStreamError { |
| 101 | match err { |
| 102 | coreaudio::Error::RenderCallbackBufferFormatDoesNotMatchAudioUnitStreamFormat |
| 103 | | coreaudio::Error::NoKnownSubtype |
| 104 | | coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::FormatNotSupported) |
| 105 | | coreaudio::Error::AudioCodec(_) |
| 106 | | coreaudio::Error::AudioFormat(_) => BuildStreamError::StreamConfigNotSupported, |
| 107 | _ => BuildStreamError::DeviceNotAvailable, |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | impl From<coreaudio::Error> for SupportedStreamConfigsError { |
| 113 | fn from(err: coreaudio::Error) -> SupportedStreamConfigsError { |
| 114 | let description = format!("{err}"); |
| 115 | let err = BackendSpecificError { description }; |
| 116 | // Check for possible DeviceNotAvailable variant |
| 117 | SupportedStreamConfigsError::BackendSpecific { err } |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | impl From<coreaudio::Error> for DefaultStreamConfigError { |
| 122 | fn from(err: coreaudio::Error) -> DefaultStreamConfigError { |
| 123 | let description = format!("{err}"); |
| 124 | let err = BackendSpecificError { description }; |
| 125 | // Check for possible DeviceNotAvailable variant |
| 126 | DefaultStreamConfigError::BackendSpecific { err } |
| 127 | } |
| 128 | } |
| 129 | |
| 130 | pub(crate) type OSStatus = i32; |
| 131 | |
| 132 | // Compile-time assertion that Stream is Send and Sync |
| 133 | crate::assert_stream_send!(Stream); |
| 134 | crate::assert_stream_sync!(Stream); |