nandi/jolt-nativepublic Fork 0
cfd3e3677bed92e80cfe447469a249cfdc0b1401
Commits
Clone
git clone https://git.rickub.com/nandi/jolt-native.git
git clone ssh://git@rickub.com/nandi/jolt-native.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

mod.rs · 595 lines · 18.6 KBRust Blame HistoryRaw
Lift freeq's AV media plane out of sleek 90f8b89 nandi 19d ago1//! CoreAudio implementation for iOS using AVAudioSession and RemoteIO Audio Units.
2
3use std::sync::Mutex;
4
5use coreaudio::audio_unit::render_callback::data;
6use coreaudio::audio_unit::{render_callback, AudioUnit, Element, Scope};
7use objc2_audio_toolbox::{kAudioOutputUnitProperty_EnableIO, kAudioUnitProperty_StreamFormat};
8use objc2_core_audio_types::AudioBuffer;
9
10use objc2_avf_audio::AVAudioSession;
11
12use super::{asbd_from_config, frames_to_duration, host_time_to_stream_instant};
13use crate::traits::{DeviceTrait, HostTrait, StreamTrait};
14
15use crate::{
16 BackendSpecificError, BufferSize, BuildStreamError, ChannelCount, Data,
17 DefaultStreamConfigError, DeviceDescription, DeviceDescriptionBuilder, DeviceId, DeviceIdError,
18 DeviceNameError, DevicesError, InputCallbackInfo, OutputCallbackInfo, PauseStreamError,
19 PlayStreamError, SampleFormat, SampleRate, StreamConfig, StreamError, SupportedBufferSize,
20 SupportedStreamConfig, SupportedStreamConfigRange, SupportedStreamConfigsError,
21};
22
23use self::enumerate::{
24 default_input_device, default_output_device, Devices, SupportedInputConfigs,
25 SupportedOutputConfigs,
26};
27use std::ptr::NonNull;
28use std::time::Duration;
29
30pub mod enumerate;
31
32// These days the default of iOS is now F32 and no longer I16
33const SUPPORTED_SAMPLE_FORMAT: SampleFormat = SampleFormat::F32;
34
35#[derive(Clone, Debug, PartialEq, Eq, Hash)]
36pub struct Device;
37
38pub struct Host;
39
40impl Host {
41 pub fn new() -> Result<Self, crate::HostUnavailable> {
42 Ok(Host)
43 }
44}
45
46impl HostTrait for Host {
47 type Devices = Devices;
48 type Device = Device;
49
50 fn is_available() -> bool {
51 true
52 }
53
54 fn devices(&self) -> Result<Self::Devices, DevicesError> {
55 Ok(Devices::new())
56 }
57
58 fn default_input_device(&self) -> Option<Self::Device> {
59 default_input_device()
60 }
61
62 fn default_output_device(&self) -> Option<Self::Device> {
63 default_output_device()
64 }
65}
66
67impl Device {
68 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
69 // Query AVAudioSession to determine actual input/output availability
70 // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton
71 let direction = unsafe {
72 let audio_session = AVAudioSession::sharedInstance();
73 let input_channels = Some(audio_session.inputNumberOfChannels() as ChannelCount);
74 let output_channels = Some(audio_session.outputNumberOfChannels() as ChannelCount);
75
76 crate::device_description::direction_from_counts(input_channels, output_channels)
77 };
78
79 Ok(DeviceDescriptionBuilder::new("Default Device".to_string())
80 .direction(direction)
81 .build())
82 }
83
84 fn id(&self) -> Result<DeviceId, DeviceIdError> {
85 Ok(DeviceId(
86 crate::platform::HostId::CoreAudio,
87 "default".to_string(),
88 ))
89 }
90
91 fn supported_input_configs(
92 &self,
93 ) -> Result<SupportedInputConfigs, SupportedStreamConfigsError> {
94 Ok(get_supported_stream_configs(true))
95 }
96
97 fn supported_output_configs(
98 &self,
99 ) -> Result<SupportedOutputConfigs, SupportedStreamConfigsError> {
100 Ok(get_supported_stream_configs(false))
101 }
102
103 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
104 // Get the primary (exact channel count) config from supported configs
105 get_supported_stream_configs(true)
106 .next()
107 .map(|range| range.with_max_sample_rate())
108 .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)
109 }
110
111 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
112 // Get the maximum channel count config from supported configs
113 get_supported_stream_configs(false)
114 .last()
115 .map(|range| range.with_max_sample_rate())
116 .ok_or(DefaultStreamConfigError::StreamTypeNotSupported)
117 }
118}
119
120impl DeviceTrait for Device {
121 type SupportedInputConfigs = SupportedInputConfigs;
122 type SupportedOutputConfigs = SupportedOutputConfigs;
123 type Stream = Stream;
124
125 fn description(&self) -> Result<DeviceDescription, DeviceNameError> {
126 Device::description(self)
127 }
128
129 fn id(&self) -> Result<DeviceId, DeviceIdError> {
130 Device::id(self)
131 }
132
133 fn supported_input_configs(
134 &self,
135 ) -> Result<Self::SupportedInputConfigs, SupportedStreamConfigsError> {
136 Device::supported_input_configs(self)
137 }
138
139 fn supported_output_configs(
140 &self,
141 ) -> Result<Self::SupportedOutputConfigs, SupportedStreamConfigsError> {
142 Device::supported_output_configs(self)
143 }
144
145 fn default_input_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
146 Device::default_input_config(self)
147 }
148
149 fn default_output_config(&self) -> Result<SupportedStreamConfig, DefaultStreamConfigError> {
150 Device::default_output_config(self)
151 }
152
153 fn build_input_stream_raw<D, E>(
154 &self,
155 config: StreamConfig,
156 sample_format: SampleFormat,
157 data_callback: D,
158 error_callback: E,
159 _timeout: Option<Duration>,
160 ) -> Result<Self::Stream, BuildStreamError>
161 where
162 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
163 E: FnMut(StreamError) + Send + 'static,
164 {
165 // Configure buffer size and create audio unit
166 let mut audio_unit = setup_stream_audio_unit(config, sample_format, true)?;
167
168 // Query device buffer size for latency calculation
169 let device_buffer_frames = Some(get_device_buffer_frames());
170
171 // Set up input callback
172 setup_input_callback(
173 &mut audio_unit,
174 sample_format,
175 config.sample_rate,
176 device_buffer_frames,
177 data_callback,
178 error_callback,
179 )?;
180
181 audio_unit.start()?;
182
183 Ok(Stream::new(StreamInner {
184 playing: true,
185 audio_unit,
186 }))
187 }
188
189 /// Create an output stream.
190 fn build_output_stream_raw<D, E>(
191 &self,
192 config: StreamConfig,
193 sample_format: SampleFormat,
194 data_callback: D,
195 error_callback: E,
196 _timeout: Option<Duration>,
197 ) -> Result<Self::Stream, BuildStreamError>
198 where
199 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
200 E: FnMut(StreamError) + Send + 'static,
201 {
202 // Configure buffer size and create audio unit
203 let mut audio_unit = setup_stream_audio_unit(config, sample_format, false)?;
204
205 // Query device buffer size for latency calculation
206 let device_buffer_frames = Some(get_device_buffer_frames());
207
208 // Set up output callback
209 setup_output_callback(
210 &mut audio_unit,
211 sample_format,
212 config.sample_rate,
213 device_buffer_frames,
214 data_callback,
215 error_callback,
216 )?;
217
218 audio_unit.start()?;
219
220 Ok(Stream::new(StreamInner {
221 playing: true,
222 audio_unit,
223 }))
224 }
225}
226
227pub struct Stream {
228 inner: Mutex<StreamInner>,
229}
230
231impl Stream {
232 fn new(inner: StreamInner) -> Self {
233 Self {
234 inner: Mutex::new(inner),
235 }
236 }
237}
238
239impl StreamTrait for Stream {
240 fn play(&self) -> Result<(), PlayStreamError> {
241 let mut stream = self
242 .inner
243 .lock()
244 .map_err(|_| PlayStreamError::BackendSpecific {
245 err: BackendSpecificError {
246 description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(),
247 },
248 })?;
249
250 if !stream.playing {
251 if let Err(e) = stream.audio_unit.start() {
252 let description = format!("{}", e);
253 let err = BackendSpecificError { description };
254 return Err(err.into());
255 }
256 stream.playing = true;
257 }
258 Ok(())
259 }
260
261 fn pause(&self) -> Result<(), PauseStreamError> {
262 let mut stream = self
263 .inner
264 .lock()
265 .map_err(|_| PauseStreamError::BackendSpecific {
266 err: BackendSpecificError {
267 description: "A cpal stream operation panicked while holding the lock - this is a bug, please report it".to_string(),
268 },
269 })?;
270
271 if stream.playing {
272 if let Err(e) = stream.audio_unit.stop() {
273 let description = format!("{}", e);
274 let err = BackendSpecificError { description };
275 return Err(err.into());
276 }
277 stream.playing = false;
278 }
279 Ok(())
280 }
281
282 fn buffer_size(&self) -> Option<crate::FrameCount> {
283 Some(get_device_buffer_frames() as crate::FrameCount)
284 }
285}
286
287struct StreamInner {
288 playing: bool,
289 audio_unit: AudioUnit,
290}
291
292fn create_audio_unit() -> Result<AudioUnit, coreaudio::Error> {
293 AudioUnit::new(coreaudio::audio_unit::IOType::RemoteIO)
294}
295
296fn configure_for_recording(audio_unit: &mut AudioUnit) -> Result<(), coreaudio::Error> {
297 // Enable mic recording
298 let enable_input = 1u32;
299 audio_unit.set_property(
300 kAudioOutputUnitProperty_EnableIO,
301 Scope::Input,
302 Element::Input,
303 Some(&enable_input),
304 )?;
305
306 // Disable output
307 let disable_output = 0u32;
308 audio_unit.set_property(
309 kAudioOutputUnitProperty_EnableIO,
310 Scope::Output,
311 Element::Output,
312 Some(&disable_output),
313 )?;
314
315 Ok(())
316}
317
318/// Configure AVAudioSession with the requested buffer size.
319///
320/// Note: iOS may not honor the exact request due to system constraints.
321fn set_audio_session_buffer_size(
322 buffer_size: u32,
323 sample_rate: crate::SampleRate,
324) -> Result<(), BuildStreamError> {
325 // SAFETY: AVAudioSession::sharedInstance() returns the global audio session singleton
326 let audio_session = unsafe { AVAudioSession::sharedInstance() };
327
328 // Calculate preferred buffer duration in seconds
329 let buffer_duration = buffer_size as f64 / sample_rate as f64;
330
331 // Set the preferred IO buffer duration
332 // SAFETY: setPreferredIOBufferDuration_error is safe to call with valid duration
333 unsafe {
334 audio_session
335 .setPreferredIOBufferDuration_error(buffer_duration)
336 .map_err(|_| BuildStreamError::StreamConfigNotSupported)?;
337 }
338
339 Ok(())
340}
341
342/// Get the actual buffer size from AVAudioSession.
343///
344/// This queries the current IO buffer duration from AVAudioSession and converts
345/// it to frames based on the current sample rate.
346fn get_device_buffer_frames() -> usize {
347 // SAFETY: AVAudioSession methods are safe to call on the singleton instance
348 unsafe {
349 let audio_session = AVAudioSession::sharedInstance();
350 let buffer_duration = audio_session.IOBufferDuration();
351 let sample_rate = audio_session.sampleRate();
352 (buffer_duration * sample_rate) as usize
353 }
354}
355
356/// Get supported stream config ranges for input (is_input=true) or output (is_input=false).
357fn get_supported_stream_configs(is_input: bool) -> std::vec::IntoIter<SupportedStreamConfigRange> {
358 // SAFETY: AVAudioSession methods are safe to call on the singleton instance
359 let (sample_rate, max_channels) = unsafe {
360 let audio_session = AVAudioSession::sharedInstance();
361 let sample_rate = audio_session.sampleRate() as u32;
362 let max_channels = if is_input {
363 audio_session.inputNumberOfChannels() as u16
364 } else {
365 audio_session.outputNumberOfChannels() as u16
366 };
367 (sample_rate, max_channels)
368 };
369
370 // Typical iOS hardware buffer frame limits according to Apple Technical Q&A QA1631.
371 let buffer_size = SupportedBufferSize::Range {
372 min: 256,
373 max: 4096,
374 };
375
376 // For input, only return the exact channel count (no flexibility)
377 // For output, support flexible channel counts up to the hardware maximum
378 let min_channels = if is_input { max_channels } else { 1 };
379
380 let configs: Vec<_> = (min_channels..=max_channels)
381 .map(|channels| SupportedStreamConfigRange {
382 channels,
383 min_sample_rate: sample_rate,
384 max_sample_rate: sample_rate,
385 buffer_size,
386 sample_format: SUPPORTED_SAMPLE_FORMAT,
387 })
388 .collect();
389
390 configs.into_iter()
391}
392
393/// Setup audio unit with common configuration for input or output streams.
394fn setup_stream_audio_unit(
395 config: StreamConfig,
396 sample_format: SampleFormat,
397 is_input: bool,
398) -> Result<AudioUnit, BuildStreamError> {
399 // Configure buffer size via AVAudioSession
400 if let BufferSize::Fixed(buffer_size) = config.buffer_size {
401 set_audio_session_buffer_size(buffer_size, config.sample_rate)?;
402 }
403
404 let mut audio_unit = create_audio_unit()?;
405
406 if is_input {
407 audio_unit.uninitialize()?;
408 configure_for_recording(&mut audio_unit)?;
409 audio_unit.initialize()?;
410 }
411
412 // Set the stream format in interleaved mode
413 // For input: Output scope of Input element (data coming out of input)
414 // For output: Input scope of Output element (data going into output)
415 let (scope, element) = if is_input {
416 (Scope::Output, Element::Input)
417 } else {
418 (Scope::Input, Element::Output)
419 };
420
421 let asbd = asbd_from_config(config, sample_format);
422 audio_unit.set_property(kAudioUnitProperty_StreamFormat, scope, element, Some(&asbd))?;
423
424 Ok(audio_unit)
425}
426
427/// Extract AudioBuffer and convert to Data, handling differences between input and output.
428///
429/// # Safety
430///
431/// Caller must ensure:
432/// - `args.data.data` points to valid AudioBufferList
433/// - For input: AudioBufferList has at least one buffer
434/// - Buffer data remains valid for the callback duration
435#[inline]
436unsafe fn extract_audio_buffer(
437 args: &render_callback::Args<data::Raw>,
438 bytes_per_channel: usize,
439 sample_format: SampleFormat,
440 is_input: bool,
441) -> (AudioBuffer, Data) {
442 let buffer = if is_input {
443 // Input: access through buffer array
444 let first_buf_ptr = core::ptr::addr_of!((*args.data.data).mBuffers) as *const AudioBuffer;
445 core::ptr::read_unaligned(first_buf_ptr)
446 } else {
447 // Output: direct access
448 let buf_ptr = core::ptr::addr_of!((*args.data.data).mBuffers[0]);
449 core::ptr::read_unaligned(buf_ptr)
450 };
451
452 let mut data_ptr = buffer.mData as *mut ();
453 let mut len = buffer.mDataByteSize as usize / bytes_per_channel;
454
455 // SAFETY: slice::from_raw_parts requires a non-null pointer.
456 if data_ptr.is_null() {
457 data_ptr = NonNull::dangling().as_ptr();
458 len = 0;
459 }
460
461 let data = Data::from_parts(data_ptr, len, sample_format);
462
463 (buffer, data)
464}
465
466/// Setup input callback with proper latency calculation.
467fn setup_input_callback<D, E>(
468 audio_unit: &mut AudioUnit,
469 sample_format: SampleFormat,
470 sample_rate: SampleRate,
471 device_buffer_frames: Option<usize>,
472 mut data_callback: D,
473 mut error_callback: E,
474) -> Result<(), BuildStreamError>
475where
476 D: FnMut(&Data, &InputCallbackInfo) + Send + 'static,
477 E: FnMut(StreamError) + Send + 'static,
478{
479 let bytes_per_channel = sample_format.sample_size();
480 type Args = render_callback::Args<data::Raw>;
481
482 audio_unit.set_input_callback(move |args: Args| {
483 // SAFETY: CoreAudio provides valid AudioBufferList for the callback duration
484 let (buffer, data) =
485 unsafe { extract_audio_buffer(&args, bytes_per_channel, sample_format, true) };
486
487 let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
488 Err(err) => {
489 error_callback(err.into());
490 return Err(());
491 }
492 Ok(cb) => cb,
493 };
494
495 let latency_frames = device_buffer_frames.unwrap_or_else(|| {
496 let channels = buffer.mNumberChannels as usize;
497 if channels > 0 {
498 data.len() / channels
499 } else {
500 0
501 }
502 });
503 let delay = frames_to_duration(latency_frames, sample_rate);
504 let capture = callback
505 .sub(delay)
506 .expect("`capture` occurs before origin of alsa `StreamInstant`");
507 let timestamp = crate::InputStreamTimestamp { callback, capture };
508
509 let info = InputCallbackInfo { timestamp };
510 data_callback(&data, &info);
511 Ok(())
512 })?;
513
514 Ok(())
515}
516
517/// Setup output callback with proper latency calculation.
518fn setup_output_callback<D, E>(
519 audio_unit: &mut AudioUnit,
520 sample_format: SampleFormat,
521 sample_rate: SampleRate,
522 device_buffer_frames: Option<usize>,
523 mut data_callback: D,
524 mut error_callback: E,
525) -> Result<(), BuildStreamError>
526where
527 D: FnMut(&mut Data, &OutputCallbackInfo) + Send + 'static,
528 E: FnMut(StreamError) + Send + 'static,
529{
530 let bytes_per_channel = sample_format.sample_size();
531 type Args = render_callback::Args<data::Raw>;
532
533 audio_unit.set_render_callback(move |args: Args| {
534 // SAFETY: CoreAudio provides valid AudioBufferList for the callback duration
535 let (buffer, mut data) =
536 unsafe { extract_audio_buffer(&args, bytes_per_channel, sample_format, false) };
537
538 let callback = match host_time_to_stream_instant(args.time_stamp.mHostTime) {
539 Err(err) => {
540 error_callback(err.into());
541 return Err(());
542 }
543 Ok(cb) => cb,
544 };
545
546 let latency_frames = device_buffer_frames.unwrap_or_else(|| {
547 let channels = buffer.mNumberChannels as usize;
548 if channels > 0 {
549 data.len() / channels
550 } else {
551 0
552 }
553 });
554 let delay = frames_to_duration(latency_frames, sample_rate);
555 let playback = callback
556 .add(delay)
557 .expect("`playback` occurs beyond representation supported by `StreamInstant`");
558 let timestamp = crate::OutputStreamTimestamp { callback, playback };
559
560 let info = OutputCallbackInfo { timestamp };
561 data_callback(&mut data, &info);
562 Ok(())
563 })?;
564
565 Ok(())
566}
567
568#[cfg(test)]
569mod tests {
570 use crate::{BufferSize, SampleRate, StreamConfig};
571
572 #[test]
573 fn test_ios_fixed_buffer_size() {
574 let host = crate::default_host();
575 let device = host.default_output_device().unwrap();
576
577 let config = StreamConfig {
578 channels: 2,
579 sample_rate: SampleRate(48000),
580 buffer_size: BufferSize::Fixed(512),
581 };
582
583 let result = device.build_output_stream(
584 &config,
585 |_data: &mut [f32], _info: &crate::OutputCallbackInfo| {},
586 |_err| {},
587 None,
588 );
589
590 assert!(
591 result.is_ok(),
592 "BufferSize::Fixed should be supported on iOS via AVAudioSession"
593 );
594 }
595}