| Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago | 1 | use super::OSStatus; |
| 2 | use super::Stream; |
| 3 | use super::{asbd_from_config, check_os_status, frames_to_duration, host_time_to_stream_instant}; |
| 4 | use crate::host::coreaudio::macos::loopback::LoopbackDevice; |
| 5 | use crate::host::coreaudio::macos::StreamInner; |
| 6 | use crate::traits::DeviceTrait; |
| 7 | use crate::{ |
| 8 | BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data, |
| 9 | DefaultStreamConfigError, DeviceId, DeviceIdError, DeviceNameError, InputCallbackInfo, |
| 10 | OutputCallbackInfo, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize, |
| 11 | SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError, |
| 12 | }; |
| 13 | use coreaudio::audio_unit::render_callback::{self, data}; |
| 14 | use coreaudio::audio_unit::{AudioUnit, Element, Scope}; |
| 15 | use objc2_audio_toolbox::{ |
| 16 | kAudioOutputUnitProperty_CurrentDevice, kAudioOutputUnitProperty_EnableIO, |
| 17 | kAudioUnitProperty_StreamFormat, |
| 18 | }; |
| 19 | use objc2_core_audio::kAudioDevicePropertyDeviceUID; |
| 20 | use objc2_core_audio::kAudioObjectPropertyElementMain; |
| 21 | use objc2_core_audio::{ |
| 22 | kAudioAggregateDeviceClassID, kAudioDevicePropertyAvailableNominalSampleRates, |
| 23 | kAudioDevicePropertyBufferFrameSize, kAudioDevicePropertyBufferFrameSizeRange, |
| 24 | kAudioDevicePropertyLatency, kAudioDevicePropertyNominalSampleRate, |
| 25 | kAudioDevicePropertySafetyOffset, kAudioDevicePropertyStreamConfiguration, |
| 26 | kAudioDevicePropertyStreamFormat, kAudioObjectPropertyClass, kAudioObjectPropertyElementMaster, |
| 27 | kAudioObjectPropertyScopeGlobal, kAudioObjectPropertyScopeInput, |
| 28 | kAudioObjectPropertyScopeOutput, AudioClassID, AudioDeviceID, AudioObjectGetPropertyData, |
| 29 | AudioObjectGetPropertyDataSize, AudioObjectID, AudioObjectPropertyAddress, |
| 30 | AudioObjectPropertyScope, AudioObjectSetPropertyData, |
| 31 | }; |
| 32 | use objc2_core_audio_types::{ |
| 33 | AudioBuffer, AudioBufferList, AudioStreamBasicDescription, AudioValueRange, |
| 34 | }; |
| 35 | use objc2_core_foundation::CFString; |
| 36 | use objc2_core_foundation::Type; |
| 37 | |
| 38 | pub use super::enumerate::{ |
| 39 | default_input_device, default_output_device, SupportedInputConfigs, SupportedOutputConfigs, |
| 40 | }; |
| 41 | use std::fmt; |
| 42 | use std::mem::{self, size_of}; |
| 43 | use std::ptr::{null, NonNull}; |
| 44 | use std::sync::mpsc::{channel, RecvTimeoutError}; |
| 45 | use std::sync::{Arc, Mutex}; |
| 46 | use std::time::{Duration, Instant}; |
| 47 | |
| 48 | use super::invoke_error_callback; |
| 49 | use super::property_listener::AudioObjectPropertyListener; |
| 50 | use coreaudio::audio_unit::macos_helpers::get_device_name; |
| 51 | |
| 52 | /// Attempt to set the device sample rate to the provided rate. |
| 53 | /// Return an error if the requested sample rate is not supported by the device. |
| 54 | fn set_sample_rate( |
| 55 | audio_device_id: AudioObjectID, |
| 56 | target_sample_rate: SampleRate, |
| 57 | ) -> Result<(), BuildStreamError> { |
| 58 | // Get the current sample rate. |
| 59 | let mut property_address = AudioObjectPropertyAddress { |
| 60 | mSelector: kAudioDevicePropertyNominalSampleRate, |
| 61 | mScope: kAudioObjectPropertyScopeGlobal, |
| 62 | mElement: kAudioObjectPropertyElementMaster, |
| 63 | }; |
| 64 | let mut sample_rate: f64 = 0.0; |
| 65 | let mut data_size = mem::size_of::<f64>() as u32; |
| 66 | let status = unsafe { |
| 67 | AudioObjectGetPropertyData( |
| 68 | audio_device_id, |
| 69 | NonNull::from(&property_address), |
| 70 | 0, |
| 71 | null(), |
| 72 | NonNull::from(&mut data_size), |
| 73 | NonNull::from(&mut sample_rate).cast(), |
| 74 | ) |
| 75 | }; |
| 76 | coreaudio::Error::from_os_status(status)?; |
| 77 | |
| 78 | // If the requested sample rate is different to the device sample rate, update the device. |
| 79 | if sample_rate as u32 != target_sample_rate { |
| 80 | // Get available sample rate ranges. |
| 81 | property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; |
| 82 | let mut data_size = 0u32; |
| 83 | let status = unsafe { |
| 84 | AudioObjectGetPropertyDataSize( |
| 85 | audio_device_id, |
| 86 | NonNull::from(&property_address), |
| 87 | 0, |
| 88 | null(), |
| 89 | NonNull::from(&mut data_size), |
| 90 | ) |
| 91 | }; |
| 92 | coreaudio::Error::from_os_status(status)?; |
| 93 | let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>(); |
| 94 | let mut ranges: Vec<AudioValueRange> = Vec::with_capacity(n_ranges); |
| 95 | let status = unsafe { |
| 96 | AudioObjectGetPropertyData( |
| 97 | audio_device_id, |
| 98 | NonNull::from(&property_address), |
| 99 | 0, |
| 100 | null(), |
| 101 | NonNull::from(&mut data_size), |
| 102 | NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), |
| 103 | ) |
| 104 | }; |
| 105 | coreaudio::Error::from_os_status(status)?; |
| 106 | unsafe { |
| 107 | ranges.set_len(n_ranges); |
| 108 | } |
| 109 | |
| 110 | // Now that we have the available ranges, pick the one matching the desired rate. |
| 111 | let sample_rate = target_sample_rate; |
| 112 | if !ranges |
| 113 | .iter() |
| 114 | .any(|r| sample_rate as f64 >= r.mMinimum && sample_rate as f64 <= r.mMaximum) |
| 115 | { |
| 116 | return Err(BuildStreamError::StreamConfigNotSupported); |
| 117 | } |
| 118 | |
| 119 | let (send, recv) = channel::<Result<f64, coreaudio::Error>>(); |
| 120 | let sample_rate_address = AudioObjectPropertyAddress { |
| 121 | mSelector: kAudioDevicePropertyNominalSampleRate, |
| 122 | mScope: kAudioObjectPropertyScopeGlobal, |
| 123 | mElement: kAudioObjectPropertyElementMaster, |
| 124 | }; |
| 125 | // Send sample rate updates back on a channel. |
| 126 | let sample_rate_handler = move || { |
| 127 | let mut rate: f64 = 0.0; |
| 128 | let mut data_size = mem::size_of::<f64>() as u32; |
| 129 | |
| 130 | let result = unsafe { |
| 131 | AudioObjectGetPropertyData( |
| 132 | audio_device_id, |
| 133 | NonNull::from(&sample_rate_address), |
| 134 | 0, |
| 135 | null(), |
| 136 | NonNull::from(&mut data_size), |
| 137 | NonNull::from(&mut rate).cast(), |
| 138 | ) |
| 139 | }; |
| 140 | send.send(coreaudio::Error::from_os_status(result).map(|_| rate)) |
| 141 | .ok(); |
| 142 | }; |
| 143 | |
| 144 | let listener = AudioObjectPropertyListener::new( |
| 145 | audio_device_id, |
| 146 | sample_rate_address, |
| 147 | sample_rate_handler, |
| 148 | )?; |
| 149 | |
| 150 | // Finally, set the sample rate. |
| 151 | property_address.mSelector = kAudioDevicePropertyNominalSampleRate; |
| 152 | // Set the nominal sample rate using a single f64 as required by CoreAudio. |
| 153 | let rate = sample_rate as f64; |
| 154 | let data_size = mem::size_of::<f64>() as u32; |
| 155 | let status = unsafe { |
| 156 | AudioObjectSetPropertyData( |
| 157 | audio_device_id, |
| 158 | NonNull::from(&property_address), |
| 159 | 0, |
| 160 | null(), |
| 161 | data_size, |
| 162 | NonNull::from(&rate).cast(), |
| 163 | ) |
| 164 | }; |
| 165 | coreaudio::Error::from_os_status(status)?; |
| 166 | |
| 167 | // Wait for the reported_rate to change. |
| 168 | // |
| 169 | // This should not take longer than a few ms, but we timeout after 1 sec just in case. |
| 170 | // We loop over potentially several events from the channel to ensure |
| 171 | // that we catch the expected change in sample rate. |
| 172 | let mut timeout = Duration::from_secs(1); |
| 173 | let start = Instant::now(); |
| 174 | |
| 175 | loop { |
| 176 | match recv.recv_timeout(timeout) { |
| 177 | Err(err) => { |
| 178 | let description = match err { |
| 179 | RecvTimeoutError::Disconnected => { |
| 180 | "sample rate listener channel disconnected unexpectedly" |
| 181 | } |
| 182 | RecvTimeoutError::Timeout => { |
| 183 | "timeout waiting for sample rate update for device" |
| 184 | } |
| 185 | } |
| 186 | .to_string(); |
| 187 | return Err(BackendSpecificError { description }.into()); |
| 188 | } |
| 189 | Ok(Ok(reported_sample_rate)) => { |
| 190 | if reported_sample_rate == target_sample_rate as f64 { |
| 191 | break; |
| 192 | } |
| 193 | } |
| 194 | Ok(Err(_)) => { |
| 195 | // TODO: should we consider collecting this error? |
| 196 | } |
| 197 | }; |
| 198 | timeout = timeout |
| 199 | .checked_sub(start.elapsed()) |
| 200 | .unwrap_or(Duration::ZERO); |
| 201 | } |
| 202 | listener.remove()?; |
| 203 | } |
| 204 | Ok(()) |
| 205 | } |
| 206 | |
| 207 | fn audio_unit_from_device(device: &Device, input: bool) -> Result<AudioUnit, coreaudio::Error> { |
| 208 | let output_type = if !input && is_default_output_device(device) { |
| 209 | coreaudio::audio_unit::IOType::DefaultOutput |
| 210 | } else { |
| 211 | coreaudio::audio_unit::IOType::HalOutput |
| 212 | }; |
| 213 | let mut audio_unit = AudioUnit::new(output_type)?; |
| 214 | |
| 215 | if input { |
| 216 | // Enable input processing. |
| 217 | let enable_input = 1u32; |
| 218 | audio_unit.set_property( |
| 219 | kAudioOutputUnitProperty_EnableIO, |
| 220 | Scope::Input, |
| 221 | Element::Input, |
| 222 | Some(&enable_input), |
| 223 | )?; |
| 224 | |
| 225 | // Disable output processing. |
| 226 | let disable_output = 0u32; |
| 227 | audio_unit.set_property( |
| 228 | kAudioOutputUnitProperty_EnableIO, |
| 229 | Scope::Output, |
| 230 | Element::Output, |
| 231 | Some(&disable_output), |
| 232 | )?; |
| 233 | } |
| 234 | |
| 235 | // Device selection is a device-level property: always use Scope::Global + Element::Output |
| 236 | audio_unit.set_property( |
| 237 | kAudioOutputUnitProperty_CurrentDevice, |
| 238 | Scope::Global, |
| 239 | Element::Output, |
| 240 | Some(&device.audio_device_id), |
| 241 | )?; |
| 242 | |
| 243 | Ok(audio_unit) |
| 244 | } |
| 245 | |
| 246 | fn get_io_buffer_frame_size_range( |
| 247 | audio_unit: &AudioUnit, |
| 248 | ) -> Result<SupportedBufferSize, coreaudio::Error> { |
| 249 | // Device-level property: always use Scope::Global + Element::Output |
| 250 | // regardless of whether this audio unit is configured for input or output |
| 251 | let buffer_size_range: AudioValueRange = audio_unit.get_property( |
| 252 | kAudioDevicePropertyBufferFrameSizeRange, |
| 253 | Scope::Global, |
| 254 | Element::Output, |
| 255 | )?; |
| 256 | |
| 257 | Ok(SupportedBufferSize::Range { |
| 258 | min: buffer_size_range.mMinimum as u32, |
| 259 | max: buffer_size_range.mMaximum as u32, |
| 260 | }) |
| 261 | } |
| 262 | |
| 263 | impl DeviceTrait for Device { |
| 264 | type SupportedInputConfigs = SupportedInputConfigs; |
| 265 | type SupportedOutputConfigs = SupportedOutputConfigs; |
| 266 | type Stream = Stream; |
| 267 | |
| 268 | fn description(&self) -> Result<crate::DeviceDescription, DeviceNameError> { |
| 269 | Device::description(self) |
| 270 | } |
| 271 | |
| 272 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 273 | Device::id(self) |
| 274 | } |
| 275 | |
| 276 | fn supported_input_configs( |
| 277 | &self, |
| 278 | ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> { |
| 279 | Device::supported_input_configs(self) |
| 280 | } |
| 281 | |
| 282 | fn supported_output_configs( |
| 283 | &self, |
| 284 | ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 285 | Device::supported_output_configs(self) |
| 286 | } |
| 287 | |
| 288 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 289 | Device::default_input_config(self) |
| 290 | } |
| 291 | |
| 292 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 293 | Device::default_output_config(self) |
| 294 | } |
| 295 | |
| 296 | fn build_input_stream_raw<D, E>( |
| 297 | &self, |
| 298 | config: StreamConfig, |
| 299 | sample_format: SampleFormat, |
| 300 | data_callback: D, |
| 301 | error_callback: E, |
| 302 | timeout: Option<Duration>, |
| 303 | ) -> Result<Self::Stream, BuildStreamError> |
| 304 | where |
| 305 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, |
| 306 | E: FnMut(StreamError) + Send + 'static, |
| 307 | { |
| 308 | Device::build_input_stream_raw( |
| 309 | self, |
| 310 | config, |
| 311 | sample_format, |
| 312 | data_callback, |
| 313 | error_callback, |
| 314 | timeout, |
| 315 | ) |
| 316 | } |
| 317 | |
| 318 | fn build_output_stream_raw<D, E>( |
| 319 | &self, |
| 320 | config: StreamConfig, |
| 321 | sample_format: SampleFormat, |
| 322 | data_callback: D, |
| 323 | error_callback: E, |
| 324 | timeout: Option<Duration>, |
| 325 | ) -> Result<Self::Stream, BuildStreamError> |
| 326 | where |
| 327 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 328 | E: FnMut(StreamError) + Send + 'static, |
| 329 | { |
| 330 | Device::build_output_stream_raw( |
| 331 | self, |
| 332 | config, |
| 333 | sample_format, |
| 334 | data_callback, |
| 335 | error_callback, |
| 336 | timeout, |
| 337 | ) |
| 338 | } |
| 339 | } |
| 340 | |
| 341 | #[derive(Clone, Eq, Hash, PartialEq)] |
| 342 | pub struct Device { |
| 343 | pub(crate) audio_device_id: AudioDeviceID, |
| 344 | } |
| 345 | |
| 346 | fn is_default_input_device(device: &Device) -> bool { |
| 347 | default_input_device().is_some_and(|d| d.audio_device_id == device.audio_device_id) |
| 348 | } |
| 349 | |
| 350 | fn is_default_output_device(device: &Device) -> bool { |
| 351 | default_output_device().is_some_and(|d| d.audio_device_id == device.audio_device_id) |
| 352 | } |
| 353 | |
| 354 | impl Device { |
| 355 | /// Construct a new device given its ID. |
| 356 | /// Useful for constructing hidden devices. |
| 357 | pub fn new(audio_device_id: AudioDeviceID) -> Self { |
| 358 | Self { audio_device_id } |
| 359 | } |
| 360 | |
| 361 | /// Checks if this device is an aggregate device. |
| 362 | /// |
| 363 | /// Aggregate devices combine multiple physical devices into a single logical device. |
| 364 | fn is_aggregate_device(&self) -> bool { |
| 365 | let property_address = AudioObjectPropertyAddress { |
| 366 | mSelector: kAudioObjectPropertyClass, |
| 367 | mScope: kAudioObjectPropertyScopeGlobal, |
| 368 | mElement: kAudioObjectPropertyElementMain, |
| 369 | }; |
| 370 | |
| 371 | let mut class_id: AudioClassID = 0; |
| 372 | let data_size = size_of::<AudioClassID>() as u32; |
| 373 | |
| 374 | // SAFETY: AudioObjectGetPropertyData is documented to write an AudioClassID |
| 375 | // for kAudioObjectPropertyClass. We check the status before using the value. |
| 376 | let status = unsafe { |
| 377 | AudioObjectGetPropertyData( |
| 378 | self.audio_device_id, |
| 379 | NonNull::from(&property_address), |
| 380 | 0, |
| 381 | null(), |
| 382 | NonNull::from(&data_size), |
| 383 | NonNull::from(&mut class_id).cast(), |
| 384 | ) |
| 385 | }; |
| 386 | |
| 387 | // If successful, check if it's an aggregate device |
| 388 | status == 0 && class_id == kAudioAggregateDeviceClassID |
| 389 | } |
| 390 | |
| 391 | fn description(&self) -> Result<crate::DeviceDescription, DeviceNameError> { |
| 392 | let name = get_device_name(self.audio_device_id).map_err(|err| { |
| 393 | DeviceNameError::BackendSpecific { |
| 394 | err: BackendSpecificError { |
| 395 | description: err.to_string(), |
| 396 | }, |
| 397 | } |
| 398 | })?; |
| 399 | |
| 400 | let input_configs = self |
| 401 | .supported_input_configs() |
| 402 | .map(|configs| configs.count() as ChannelCount) |
| 403 | .ok(); |
| 404 | let output_configs = self |
| 405 | .supported_output_configs() |
| 406 | .map(|configs| configs.count() as ChannelCount) |
| 407 | .ok(); |
| 408 | |
| 409 | let direction = |
| 410 | crate::device_description::direction_from_counts(input_configs, output_configs); |
| 411 | |
| 412 | let mut builder = crate::DeviceDescriptionBuilder::new(name).direction(direction); |
| 413 | |
| 414 | // Check if this is an aggregate device |
| 415 | if self.is_aggregate_device() { |
| 416 | builder = builder.interface_type(crate::InterfaceType::Aggregate); |
| 417 | } |
| 418 | |
| 419 | Ok(builder.build()) |
| 420 | } |
| 421 | |
| 422 | fn id(&self) -> Result<DeviceId, DeviceIdError> { |
| 423 | let property_address = AudioObjectPropertyAddress { |
| 424 | mSelector: kAudioDevicePropertyDeviceUID, |
| 425 | mScope: kAudioObjectPropertyScopeGlobal, |
| 426 | mElement: kAudioObjectPropertyElementMain, |
| 427 | }; |
| 428 | |
| 429 | // CFString is copied from the audio object, use wrap_under_create_rule |
| 430 | let mut uid: *mut CFString = std::ptr::null_mut(); |
| 431 | let mut data_size = size_of::<*mut CFString>() as u32; |
| 432 | |
| 433 | // SAFETY: AudioObjectGetPropertyData is documented to write a CFString pointer |
| 434 | // for kAudioDevicePropertyDeviceUID. We check the status code before use. |
| 435 | let status = unsafe { |
| 436 | AudioObjectGetPropertyData( |
| 437 | self.audio_device_id, |
| 438 | NonNull::from(&property_address), |
| 439 | 0, |
| 440 | null(), |
| 441 | NonNull::from(&mut data_size), |
| 442 | NonNull::from(&mut uid).cast(), |
| 443 | ) |
| 444 | }; |
| 445 | check_os_status(status)?; |
| 446 | |
| 447 | // SAFETY: Status was successful, meaning the API call succeeded. |
| 448 | // We now check if the returned uid is non-null before use. |
| 449 | if !uid.is_null() { |
| 450 | let uid_string = unsafe { CFString::wrap_under_create_rule(uid).to_string() }; |
| 451 | Ok(DeviceId(crate::platform::HostId::CoreAudio, uid_string)) |
| 452 | } else { |
| 453 | Err(DeviceIdError::BackendSpecific { |
| 454 | err: BackendSpecificError { |
| 455 | description: "Device UID is null".to_string(), |
| 456 | }, |
| 457 | }) |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | // Logic re-used between `supported_input_configs` and `supported_output_configs`. |
| 462 | #[allow(clippy::cast_ptr_alignment)] |
| 463 | fn supported_configs( |
| 464 | &self, |
| 465 | scope: AudioObjectPropertyScope, |
| 466 | ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 467 | let mut property_address = AudioObjectPropertyAddress { |
| 468 | mSelector: kAudioDevicePropertyStreamConfiguration, |
| 469 | mScope: scope, |
| 470 | mElement: kAudioObjectPropertyElementMaster, |
| 471 | }; |
| 472 | |
| 473 | unsafe { |
| 474 | // Retrieve the devices audio buffer list. |
| 475 | let mut data_size = 0u32; |
| 476 | let status = AudioObjectGetPropertyDataSize( |
| 477 | self.audio_device_id, |
| 478 | NonNull::from(&property_address), |
| 479 | 0, |
| 480 | null(), |
| 481 | NonNull::from(&mut data_size), |
| 482 | ); |
| 483 | check_os_status(status)?; |
| 484 | |
| 485 | let mut audio_buffer_list: Vec<u8> = vec![]; |
| 486 | audio_buffer_list.reserve_exact(data_size as usize); |
| 487 | let status = AudioObjectGetPropertyData( |
| 488 | self.audio_device_id, |
| 489 | NonNull::from(&property_address), |
| 490 | 0, |
| 491 | null(), |
| 492 | NonNull::from(&mut data_size), |
| 493 | NonNull::new(audio_buffer_list.as_mut_ptr()).unwrap().cast(), |
| 494 | ); |
| 495 | check_os_status(status)?; |
| 496 | |
| 497 | let audio_buffer_list = audio_buffer_list.as_mut_ptr() as *mut AudioBufferList; |
| 498 | |
| 499 | // Read the number of buffers without assuming alignment (avoid UB). |
| 500 | let nb_ptr = core::ptr::addr_of!((*audio_buffer_list).mNumberBuffers); |
| 501 | let n_buffers = core::ptr::read_unaligned(nb_ptr) as usize; |
| 502 | // If there are no buffers, skip. |
| 503 | if n_buffers == 0 { |
| 504 | return Ok(vec![].into_iter()); |
| 505 | } |
| 506 | |
| 507 | // Count the number of channels as the sum of all channels in all output buffers. |
| 508 | let first_buf_ptr = |
| 509 | core::ptr::addr_of!((*audio_buffer_list).mBuffers) as *const AudioBuffer; |
| 510 | let mut n_channels = 0usize; |
| 511 | for i in 0..n_buffers { |
| 512 | let buf_ptr = first_buf_ptr.add(i); |
| 513 | // Read potentially unaligned |
| 514 | let buf: AudioBuffer = core::ptr::read_unaligned(buf_ptr); |
| 515 | n_channels += buf.mNumberChannels as usize; |
| 516 | } |
| 517 | |
| 518 | // TODO: macOS should support U8, I16, I32, F32 and F64. This should allow for using |
| 519 | // I16 but just use F32 for now as it's the default anyway. |
| 520 | let sample_format = SampleFormat::F32; |
| 521 | |
| 522 | // Get available sample rate ranges. |
| 523 | // The property "kAudioDevicePropertyAvailableNominalSampleRates" returns a list of pairs of |
| 524 | // minimum and maximum sample rates but most of the devices returns pairs of same values though the underlying mechanism is unclear. |
| 525 | // This may cause issues when, for example, sorting the configs by the sample rates. |
| 526 | // We follows the implementation of RtAudio, which returns single element of config |
| 527 | // when all the pairs have the same values and returns multiple elements otherwise. |
| 528 | // See https://github.com/thestk/rtaudio/blob/master/RtAudio.cpp#L1369C1-L1375C39 |
| 529 | |
| 530 | property_address.mSelector = kAudioDevicePropertyAvailableNominalSampleRates; |
| 531 | let mut data_size = 0u32; |
| 532 | let status = AudioObjectGetPropertyDataSize( |
| 533 | self.audio_device_id, |
| 534 | NonNull::from(&property_address), |
| 535 | 0, |
| 536 | null(), |
| 537 | NonNull::from(&mut data_size), |
| 538 | ); |
| 539 | check_os_status(status)?; |
| 540 | |
| 541 | let n_ranges = data_size as usize / mem::size_of::<AudioValueRange>(); |
| 542 | let mut ranges: Vec<AudioValueRange> = Vec::with_capacity(n_ranges); |
| 543 | let status = AudioObjectGetPropertyData( |
| 544 | self.audio_device_id, |
| 545 | NonNull::from(&property_address), |
| 546 | 0, |
| 547 | null(), |
| 548 | NonNull::from(&mut data_size), |
| 549 | NonNull::new(ranges.as_mut_ptr()).unwrap().cast(), |
| 550 | ); |
| 551 | check_os_status(status)?; |
| 552 | |
| 553 | ranges.set_len(n_ranges); |
| 554 | |
| 555 | #[allow(non_upper_case_globals)] |
| 556 | let input = match scope { |
| 557 | kAudioObjectPropertyScopeInput => Ok(true), |
| 558 | kAudioObjectPropertyScopeOutput => Ok(false), |
| 559 | _ => Err(BackendSpecificError { |
| 560 | description: format!("unexpected scope (neither input nor output): {scope:?}"), |
| 561 | }), |
| 562 | }?; |
| 563 | let audio_unit = audio_unit_from_device(self, input)?; |
| 564 | let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; |
| 565 | |
| 566 | // Collect the supported formats for the device. |
| 567 | |
| 568 | let contains_different_sample_rates = ranges.iter().any(|r| r.mMinimum != r.mMaximum); |
| 569 | if ranges.is_empty() { |
| 570 | Ok(vec![].into_iter()) |
| 571 | } else if contains_different_sample_rates { |
| 572 | let res = ranges.iter().map(|range| SupportedStreamConfigRange { |
| 573 | channels: n_channels as ChannelCount, |
| 574 | min_sample_rate: range.mMinimum as u32, |
| 575 | max_sample_rate: range.mMaximum as u32, |
| 576 | buffer_size, |
| 577 | sample_format, |
| 578 | }); |
| 579 | Ok(res.collect::<Vec<_>>().into_iter()) |
| 580 | } else { |
| 581 | let fmt = SupportedStreamConfigRange { |
| 582 | channels: n_channels as ChannelCount, |
| 583 | min_sample_rate: ranges |
| 584 | .iter() |
| 585 | .map(|v| v.mMinimum as u32) |
| 586 | .min() |
| 587 | .expect("the list must not be empty"), |
| 588 | max_sample_rate: ranges |
| 589 | .iter() |
| 590 | .map(|v| v.mMaximum as u32) |
| 591 | .max() |
| 592 | .expect("the list must not be empty"), |
| 593 | buffer_size, |
| 594 | sample_format, |
| 595 | }; |
| 596 | |
| 597 | Ok(vec![fmt].into_iter()) |
| 598 | } |
| 599 | } |
| 600 | } |
| 601 | |
| 602 | fn supported_input_configs( |
| 603 | &self, |
| 604 | ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 605 | self.supported_configs(kAudioObjectPropertyScopeInput) |
| 606 | } |
| 607 | |
| 608 | fn supported_output_configs( |
| 609 | &self, |
| 610 | ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> { |
| 611 | self.supported_configs(kAudioObjectPropertyScopeOutput) |
| 612 | } |
| 613 | |
| 614 | fn default_config( |
| 615 | &self, |
| 616 | scope: AudioObjectPropertyScope, |
| 617 | ) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 618 | fn default_config_error_from_os_status( |
| 619 | status: OSStatus, |
| 620 | ) -> Result<(), DefaultStreamConfigError> { |
| 621 | let err = match coreaudio::Error::from_os_status(status) { |
| 622 | Err(err) => err, |
| 623 | Ok(_) => return Ok(()), |
| 624 | }; |
| 625 | match err { |
| 626 | coreaudio::Error::AudioUnit( |
| 627 | coreaudio::error::AudioUnitError::FormatNotSupported, |
| 628 | ) |
| 629 | | coreaudio::Error::AudioCodec(_) |
| 630 | | coreaudio::Error::AudioFormat(_) => { |
| 631 | Err(DefaultStreamConfigError::StreamTypeNotSupported) |
| 632 | } |
| 633 | coreaudio::Error::AudioUnit(coreaudio::error::AudioUnitError::NoConnection) => { |
| 634 | Err(DefaultStreamConfigError::DeviceNotAvailable) |
| 635 | } |
| 636 | err => { |
| 637 | let description = format!("{err}"); |
| 638 | let err = BackendSpecificError { description }; |
| 639 | Err(err.into()) |
| 640 | } |
| 641 | } |
| 642 | } |
| 643 | |
| 644 | let property_address = AudioObjectPropertyAddress { |
| 645 | mSelector: kAudioDevicePropertyStreamFormat, |
| 646 | mScope: scope, |
| 647 | mElement: kAudioObjectPropertyElementMaster, |
| 648 | }; |
| 649 | |
| 650 | unsafe { |
| 651 | let mut asbd: AudioStreamBasicDescription = mem::zeroed(); |
| 652 | let mut data_size = mem::size_of::<AudioStreamBasicDescription>() as u32; |
| 653 | let status = AudioObjectGetPropertyData( |
| 654 | self.audio_device_id, |
| 655 | NonNull::from(&property_address), |
| 656 | 0, |
| 657 | null(), |
| 658 | NonNull::from(&mut data_size), |
| 659 | NonNull::from(&mut asbd).cast(), |
| 660 | ); |
| 661 | default_config_error_from_os_status(status)?; |
| 662 | |
| 663 | let sample_format = { |
| 664 | let audio_format = coreaudio::audio_unit::AudioFormat::from_format_and_flag( |
| 665 | asbd.mFormatID, |
| 666 | Some(asbd.mFormatFlags), |
| 667 | ); |
| 668 | let flags = match audio_format { |
| 669 | Some(coreaudio::audio_unit::AudioFormat::LinearPCM(flags)) => flags, |
| 670 | _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), |
| 671 | }; |
| 672 | let maybe_sample_format = |
| 673 | coreaudio::audio_unit::SampleFormat::from_flags_and_bits_per_sample( |
| 674 | flags, |
| 675 | asbd.mBitsPerChannel, |
| 676 | ); |
| 677 | match maybe_sample_format { |
| 678 | Some(coreaudio::audio_unit::SampleFormat::F32) => SampleFormat::F32, |
| 679 | Some(coreaudio::audio_unit::SampleFormat::I16) => SampleFormat::I16, |
| 680 | _ => return Err(DefaultStreamConfigError::StreamTypeNotSupported), |
| 681 | } |
| 682 | }; |
| 683 | |
| 684 | #[allow(non_upper_case_globals)] |
| 685 | let input = match scope { |
| 686 | kAudioObjectPropertyScopeInput => Ok(true), |
| 687 | kAudioObjectPropertyScopeOutput => Ok(false), |
| 688 | _ => Err(BackendSpecificError { |
| 689 | description: format!("unexpected scope (neither input nor output): {scope:?}"), |
| 690 | }), |
| 691 | }?; |
| 692 | let audio_unit = audio_unit_from_device(self, input)?; |
| 693 | let buffer_size = get_io_buffer_frame_size_range(&audio_unit)?; |
| 694 | |
| 695 | let config = SupportedStreamConfig { |
| 696 | sample_rate: asbd.mSampleRate as _, |
| 697 | channels: asbd.mChannelsPerFrame as _, |
| 698 | buffer_size, |
| 699 | sample_format, |
| 700 | }; |
| 701 | Ok(config) |
| 702 | } |
| 703 | } |
| 704 | |
| 705 | fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 706 | self.default_config(kAudioObjectPropertyScopeInput) |
| 707 | } |
| 708 | |
| 709 | fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> { |
| 710 | self.default_config(kAudioObjectPropertyScopeOutput) |
| 711 | } |
| 712 | |
| 713 | /// Check if this device supports input (recording). |
| 714 | fn supports_input(&self) -> bool { |
| 715 | // Check if the device has input channels by trying to get its input configuration |
| 716 | self.supported_input_configs() |
| 717 | .map(|mut configs| configs.next().is_some()) |
| 718 | .unwrap_or(false) |
| 719 | } |
| 720 | } |
| 721 | |
| 722 | impl fmt::Debug for Device { |
| 723 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 724 | f.debug_struct("Device") |
| 725 | .field("audio_device_id", &self.audio_device_id) |
| 726 | .field("name", &self.name()) |
| 727 | .finish() |
| 728 | } |
| 729 | } |
| 730 | |
| 731 | impl Device { |
| 732 | #[allow(clippy::cast_ptr_alignment)] |
| 733 | #[allow(clippy::while_immutable_condition)] |
| 734 | #[allow(clippy::float_cmp)] |
| 735 | fn build_input_stream_raw<D, E>( |
| 736 | &self, |
| 737 | config: StreamConfig, |
| 738 | sample_format: SampleFormat, |
| 739 | mut data_callback: D, |
| 740 | error_callback: E, |
| 741 | _timeout: Option<Duration>, |
| 742 | ) -> Result<Stream, BuildStreamError> |
| 743 | where |
| 744 | D: FnMut(&Data, &InputCallbackInfo) + Send + 'static, |
| 745 | E: FnMut(StreamError) + Send + 'static, |
| 746 | { |
| 747 | // The scope and element for working with a device's input stream. |
| 748 | let scope = Scope::Output; |
| 749 | let element = Element::Input; |
| 750 | |
| 751 | // Potentially change the device sample rate to match the config. |
| 752 | set_sample_rate(self.audio_device_id, config.sample_rate)?; |
| 753 | |
| 754 | let mut loopback_aggregate: Option<LoopbackDevice> = None; |
| 755 | let mut audio_unit = if self.supports_input() { |
| 756 | audio_unit_from_device(self, true)? |
| 757 | } else { |
| 758 | loopback_aggregate.replace(LoopbackDevice::from_device(self)?); |
| 759 | audio_unit_from_device(&loopback_aggregate.as_ref().unwrap().aggregate_device, true)? |
| 760 | }; |
| 761 | |
| 762 | // Configure stream format and buffer size for predictable callback behavior. |
| 763 | configure_stream_format_and_buffer(&mut audio_unit, config, sample_format, scope, element)?; |
| 764 | |
| 765 | let error_callback = Arc::new(Mutex::new(error_callback)); |
| 766 | let error_callback_disconnect = error_callback.clone(); |
| 767 | |
| 768 | // Register the callback that is being called by coreaudio whenever it needs data to be |
| 769 | // fed to the audio buffer. |
| 770 | let (bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames) = |
| 771 | setup_callback_vars(&audio_unit, config, sample_format, Scope::Input); |
| 772 | |
| 773 | type Args = render_callback::Args<data::Raw>; |
| 774 | audio_unit.set_input_callback(move |args: Args| unsafe { |
| 775 | // SAFETY: We configure the stream format as interleaved (via asbd_from_config which |
| 776 | // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has |
| 777 | // exactly one buffer containing all channels, so mBuffers[0] is always valid. |
| 778 | let AudioBuffer { |
| 779 | mNumberChannels: channels, |
| 780 | mDataByteSize: data_byte_size, |
| 781 | mData: data, |
| 782 | } = (*args.data.data).mBuffers[0]; |
| 783 | |
| 784 | let data = data as *mut (); |
| 785 | let len = data_byte_size as usize / bytes_per_channel; |
| 786 | let data = Data::from_parts(data, len, sample_format); |
| 787 | |
| 788 | let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { |
| 789 | Err(err) => { |
| 790 | invoke_error_callback(&error_callback, err.into()); |
| 791 | return Err(()); |
| 792 | } |
| 793 | Ok(cb) => cb, |
| 794 | }; |
| 795 | let buffer_frames = len / channels as usize; |
| 796 | let latency_frames = |
| 797 | device_buffer_frames.unwrap_or(buffer_frames) + extra_latency_frames; |
| 798 | let delay = frames_to_duration(latency_frames, sample_rate); |
| 799 | let capture = callback |
| 800 | .sub(delay) |
| 801 | .expect("`capture` occurs before origin of alsa `StreamInstant`"); |
| 802 | let timestamp = crate::InputStreamTimestamp { callback, capture }; |
| 803 | |
| 804 | let info = InputCallbackInfo { timestamp }; |
| 805 | data_callback(&data, &info); |
| 806 | Ok(()) |
| 807 | })?; |
| 808 | |
| 809 | // Create error callback for stream - either dummy or real based on device type |
| 810 | let error_callback_for_stream: super::ErrorCallback = if is_default_input_device(self) { |
| 811 | Box::new(|_: StreamError| {}) |
| 812 | } else { |
| 813 | let error_callback_clone = error_callback_disconnect.clone(); |
| 814 | Box::new(move |err: StreamError| { |
| 815 | invoke_error_callback(&error_callback_clone, err); |
| 816 | }) |
| 817 | }; |
| 818 | |
| 819 | let stream = Stream::new( |
| 820 | StreamInner { |
| 821 | playing: true, |
| 822 | audio_unit, |
| 823 | device_id: self.audio_device_id, |
| 824 | _loopback_device: loopback_aggregate, |
| 825 | }, |
| 826 | error_callback_for_stream, |
| 827 | )?; |
| 828 | |
| 829 | stream |
| 830 | .inner |
| 831 | .lock() |
| 832 | .map_err(|_| BuildStreamError::BackendSpecific { |
| 833 | err: BackendSpecificError { |
| 834 | description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), |
| 835 | }, |
| 836 | })? |
| 837 | .audio_unit |
| 838 | .start()?; |
| 839 | |
| 840 | Ok(stream) |
| 841 | } |
| 842 | |
| 843 | fn build_output_stream_raw<D, E>( |
| 844 | &self, |
| 845 | config: StreamConfig, |
| 846 | sample_format: SampleFormat, |
| 847 | mut data_callback: D, |
| 848 | error_callback: E, |
| 849 | _timeout: Option<Duration>, |
| 850 | ) -> Result<Stream, BuildStreamError> |
| 851 | where |
| 852 | D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static, |
| 853 | E: FnMut(StreamError) + Send + 'static, |
| 854 | { |
| 855 | let mut audio_unit = audio_unit_from_device(self, false)?; |
| 856 | |
| 857 | // The scope and element for working with a device's output stream. |
| 858 | let scope = Scope::Input; |
| 859 | let element = Element::Output; |
| 860 | |
| 861 | // Configure device buffer (see comprehensive documentation in input stream above) |
| 862 | configure_stream_format_and_buffer(&mut audio_unit, config, sample_format, scope, element)?; |
| 863 | |
| 864 | let error_callback = Arc::new(Mutex::new(error_callback)); |
| 865 | let error_callback_disconnect = error_callback.clone(); |
| 866 | |
| 867 | // Register the callback that is being called by coreaudio whenever it needs data to be |
| 868 | // fed to the audio buffer. |
| 869 | let (bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames) = |
| 870 | setup_callback_vars(&audio_unit, config, sample_format, Scope::Output); |
| 871 | |
| 872 | type Args = render_callback::Args<data::Raw>; |
| 873 | audio_unit.set_render_callback(move |args: Args| unsafe { |
| 874 | // SAFETY: We configure the stream format as interleaved (via asbd_from_config which |
| 875 | // does not set kAudioFormatFlagIsNonInterleaved). Interleaved format always has |
| 876 | // exactly one buffer containing all channels, so mBuffers[0] is always valid. |
| 877 | let AudioBuffer { |
| 878 | mNumberChannels: channels, |
| 879 | mDataByteSize: data_byte_size, |
| 880 | mData: data, |
| 881 | } = (*args.data.data).mBuffers[0]; |
| 882 | |
| 883 | let data = data as *mut (); |
| 884 | let len = data_byte_size as usize / bytes_per_channel; |
| 885 | let mut data = Data::from_parts(data, len, sample_format); |
| 886 | |
| 887 | let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) { |
| 888 | Err(err) => { |
| 889 | invoke_error_callback(&error_callback, err.into()); |
| 890 | return Err(()); |
| 891 | } |
| 892 | Ok(cb) => cb, |
| 893 | }; |
| 894 | let buffer_frames = len / channels as usize; |
| 895 | // Use device buffer size for latency calculation if available |
| 896 | let latency_frames = |
| 897 | device_buffer_frames.unwrap_or(buffer_frames) + extra_latency_frames; |
| 898 | let delay = frames_to_duration(latency_frames, sample_rate); |
| 899 | let playback = callback |
| 900 | .add(delay) |
| 901 | .expect("`playback` occurs beyond representation supported by `StreamInstant`"); |
| 902 | let timestamp = crate::OutputStreamTimestamp { callback, playback }; |
| 903 | |
| 904 | let info = OutputCallbackInfo { timestamp }; |
| 905 | data_callback(&mut data, &info); |
| 906 | Ok(()) |
| 907 | })?; |
| 908 | |
| 909 | // Create error callback for stream - either dummy or real based on device type |
| 910 | let error_callback_for_stream: super::ErrorCallback = if is_default_output_device(self) { |
| 911 | Box::new(|_: StreamError| {}) |
| 912 | } else { |
| 913 | let error_callback_clone = error_callback_disconnect.clone(); |
| 914 | Box::new(move |err: StreamError| { |
| 915 | invoke_error_callback(&error_callback_clone, err); |
| 916 | }) |
| 917 | }; |
| 918 | |
| 919 | let stream = Stream::new( |
| 920 | StreamInner { |
| 921 | playing: true, |
| 922 | audio_unit, |
| 923 | device_id: self.audio_device_id, |
| 924 | _loopback_device: None, |
| 925 | }, |
| 926 | error_callback_for_stream, |
| 927 | )?; |
| 928 | |
| 929 | stream |
| 930 | .inner |
| 931 | .lock() |
| 932 | .map_err(|_| BuildStreamError::BackendSpecific { |
| 933 | err: BackendSpecificError { |
| 934 | description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(), |
| 935 | }, |
| 936 | })? |
| 937 | .audio_unit |
| 938 | .start()?; |
| 939 | |
| 940 | Ok(stream) |
| 941 | } |
| 942 | } |
| 943 | |
| 944 | /// Configure stream format and buffer size for CoreAudio stream. |
| 945 | /// |
| 946 | /// This handles the common setup tasks for both input and output streams: |
| 947 | /// - Sets the stream format (ASBD) |
| 948 | /// - Configures buffer size for Fixed buffer size requests |
| 949 | fn configure_stream_format_and_buffer( |
| 950 | audio_unit: &mut AudioUnit, |
| 951 | config: StreamConfig, |
| 952 | sample_format: SampleFormat, |
| 953 | scope: Scope, |
| 954 | element: Element, |
| 955 | ) -> Result<(), BuildStreamError> { |
| 956 | // Set the stream format using stream-specific scope/element |
| 957 | // - Input streams: scope=Output, element=Input (configuring output format of input element) |
| 958 | // - Output streams: scope=Input, element=Output (configuring input format of output element) |
| 959 | let asbd = asbd_from_config(config, sample_format); |
| 960 | audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?; |
| 961 | |
| 962 | // Configure device buffer size if requested |
| 963 | if let BufferSize::Fixed(buffer_size) = config.buffer_size { |
| 964 | // IMPORTANT: Buffer frame size is a DEVICE-LEVEL property, not stream-specific. |
| 965 | // Unlike stream format above, we ALWAYS use Scope::Global + Element::Output |
| 966 | // for device properties, regardless of whether this is an input or output stream. |
| 967 | // This is consistent with other device properties like: |
| 968 | // - kAudioOutputUnitProperty_CurrentDevice |
| 969 | // - kAudioDevicePropertyBufferFrameSizeRange |
| 970 | // The Element::Output here doesn't mean "output stream only" - it's the |
| 971 | // canonical element used for device-wide properties in Core Audio. |
| 972 | audio_unit.set_property( |
| 973 | kAudioDevicePropertyBufferFrameSize, |
| 974 | Scope::Global, |
| 975 | Element::Output, |
| 976 | Some(&buffer_size), |
| 977 | )?; |
| 978 | } |
| 979 | |
| 980 | Ok(()) |
| 981 | } |
| 982 | |
| 983 | /// Returns the sum of the device latency and safety offset in frames. |
| 984 | fn get_device_extra_latency_frames(audio_unit: &AudioUnit, scope: Scope) -> usize { |
| 985 | let device_latency: u32 = audio_unit |
| 986 | .get_property(kAudioDevicePropertyLatency, scope, Element::Output) |
| 987 | .unwrap_or(0); |
| 988 | let safety_offset: u32 = audio_unit |
| 989 | .get_property(kAudioDevicePropertySafetyOffset, scope, Element::Output) |
| 990 | .unwrap_or(0); |
| 991 | (device_latency + safety_offset) as usize |
| 992 | } |
| 993 | |
| 994 | /// Setup common callback variables, querying both the I/O buffer size and extra hardware latency. |
| 995 | /// |
| 996 | /// Returns `(bytes_per_channel, sample_rate, device_buffer_frames, extra_latency_frames)` |
| 997 | fn setup_callback_vars( |
| 998 | audio_unit: &AudioUnit, |
| 999 | config: StreamConfig, |
| 1000 | sample_format: SampleFormat, |
| 1001 | scope: Scope, |
| 1002 | ) -> (usize, crate::SampleRate, Option<usize>, usize) { |
| 1003 | let bytes_per_channel = sample_format.sample_size(); |
| 1004 | let sample_rate = config.sample_rate; |
| 1005 | |
| 1006 | let device_buffer_frames = get_device_buffer_frame_size(audio_unit).ok(); |
| 1007 | let extra_latency_frames = get_device_extra_latency_frames(audio_unit, scope); |
| 1008 | |
| 1009 | ( |
| 1010 | bytes_per_channel, |
| 1011 | sample_rate, |
| 1012 | device_buffer_frames, |
| 1013 | extra_latency_frames, |
| 1014 | ) |
| 1015 | } |
| 1016 | |
| 1017 | /// Query the current device buffer frame size from CoreAudio. |
| 1018 | /// |
| 1019 | /// Buffer frame size is a device-level property that always uses Scope::Global + Element::Output, |
| 1020 | /// regardless of whether the audio unit is configured for input or output streams. |
| 1021 | pub(crate) fn get_device_buffer_frame_size( |
| 1022 | audio_unit: &AudioUnit, |
| 1023 | ) -> Result<usize, coreaudio::Error> { |
| 1024 | // Device-level property: always use Scope::Global + Element::Output |
| 1025 | // This is consistent with how we set the buffer size and query the buffer size range |
| 1026 | let frames: u32 = audio_unit.get_property( |
| 1027 | kAudioDevicePropertyBufferFrameSize, |
| 1028 | Scope::Global, |
| 1029 | Element::Output, |
| 1030 | )?; |
| 1031 | Ok(frames as usize) |
| 1032 | } |