@@ -34,11 +34,6 @@ type DuplexProcFn = dyn FnMut(
3434 * mut AudioBufferList ,
3535) -> i32 ;
3636
37- /// Wrapper for the boxed duplex callback closure.
38- ///
39- /// This struct is allocated on the heap and its pointer is passed to CoreAudio
40- /// as the refcon. The extern "C" callback function casts the refcon back to
41- /// this type and calls the closure.
4237pub ( crate ) struct DuplexProcWrapper {
4338 callback : Box < DuplexProcFn > ,
4439}
@@ -52,16 +47,8 @@ pub(crate) struct DuplexProcWrapper {
5247// 3. CoreAudio guarantees single-threaded callback invocation
5348unsafe impl Send for DuplexProcWrapper { }
5449
55- /// CoreAudio render callback for duplex audio.
56- ///
57- /// This is a thin wrapper that casts the refcon back to our DuplexProcWrapper
58- /// and calls the inner closure. The closure owns all the callback state via
59- /// move semantics, so no Mutex is needed.
60- ///
61- /// Note: `extern "C-unwind"` is required here because `AURenderCallbackStruct`
62- /// from coreaudio-sys types the `inputProc` field as `extern "C-unwind"`.
63- /// We use `catch_unwind` to prevent panics from unwinding through CoreAudio's
64- /// C frames, which would be undefined behavior.
50+ // `extern "C-unwind"` matches `AURenderCallbackStruct::inputProc`.
51+ // `catch_unwind` prevents panics from unwinding through CoreAudio's C frames.
6552extern "C-unwind" fn duplex_input_proc (
6653 in_ref_con : NonNull < c_void > ,
6754 io_action_flags : NonNull < AudioUnitRenderActionFlags > ,
@@ -70,12 +57,10 @@ extern "C-unwind" fn duplex_input_proc(
7057 in_number_frames : u32 ,
7158 io_data : * mut AudioBufferList ,
7259) -> i32 {
73- // SAFETY: `in_ref_con` points to a heap-allocated `DuplexProcWrapper` created
74- // via `Box::into_raw` in `build_duplex_stream`, and remains valid for the
75- // lifetime of the audio unit (reclaimed in `StreamInner::drop`). The `as_mut()` call
76- // produces an exclusive `&mut` reference, which is sound because CoreAudio
77- // guarantees single-threaded callback invocation — this function is never
78- // called concurrently, so only one `&mut` to the wrapper exists at a time.
60+ // SAFETY: `in_ref_con` originates from `Box::into_raw` in `build_duplex_stream_raw`.
61+ // `StreamInner::drop` stops the audio unit before reclaiming the pointer,
62+ // so it remains valid for the lifetime of the callback.
63+ // Called from a single render thread per audio unit, so `as_mut()` has exclusive access.
7964 let wrapper = unsafe { in_ref_con. cast :: < DuplexProcWrapper > ( ) . as_mut ( ) } ;
8065 match std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
8166 ( wrapper. callback ) (
@@ -92,11 +77,7 @@ extern "C-unwind" fn duplex_input_proc(
9277}
9378
9479impl Device {
95- /// Build a duplex stream with synchronized input and output.
96- ///
97- /// This creates a single HAL AudioUnit with both input and output enabled,
98- /// ensuring they share the same hardware clock.
99- /// For details, see: https://developer.apple.com/library/archive/technotes/tn2091/_index.html
80+ // See: https://developer.apple.com/library/archive/technotes/tn2091/_index.html
10081 pub ( crate ) fn build_duplex_stream_raw < D , E > (
10182 & self ,
10283 config : & crate :: duplex:: DuplexStreamConfig ,
@@ -109,87 +90,64 @@ impl Device {
10990 D : FnMut ( & Data , & mut Data , & DuplexCallbackInfo ) + Send + ' static ,
11091 E : FnMut ( StreamError ) + Send + ' static ,
11192 {
112- // Validate that device supports duplex
11393 if !self . supports_duplex ( ) {
11494 return Err ( BuildStreamError :: StreamConfigNotSupported ) ;
11595 }
11696
117- // Potentially change the device sample rate to match the config.
11897 set_sample_rate ( self . audio_device_id , config. sample_rate ) ?;
11998
120- // Create HAL AudioUnit - always use HalOutput for duplex
12199 let mut audio_unit = AudioUnit :: new ( coreaudio:: audio_unit:: IOType :: HalOutput ) ?;
122100
123- // Enable BOTH input and output on the AudioUnit
124- // Enable input on Element 1
125101 audio_unit. set_property (
126102 kAudioOutputUnitProperty_EnableIO,
127103 Scope :: Input ,
128104 Element :: Input ,
129105 Some ( & AUDIO_UNIT_IO_ENABLED ) ,
130106 ) ?;
131107
132- // Enable output on Element 0 (usually enabled by default, but be explicit)
133108 audio_unit. set_property (
134109 kAudioOutputUnitProperty_EnableIO,
135110 Scope :: Output ,
136111 Element :: Output ,
137112 Some ( & AUDIO_UNIT_IO_ENABLED ) ,
138113 ) ?;
139114
140- // Set device for the unit (applies to both input and output)
141115 audio_unit. set_property (
142116 kAudioOutputUnitProperty_CurrentDevice,
143117 Scope :: Global ,
144118 Element :: Output ,
145119 Some ( & self . audio_device_id ) ,
146120 ) ?;
147121
148- // Create StreamConfig for input side
149122 let input_stream_config = StreamConfig {
150123 channels : config. input_channels ,
151124 sample_rate : config. sample_rate ,
152125 buffer_size : config. buffer_size ,
153126 } ;
154127
155- // Create StreamConfig for output side
156128 let output_stream_config = StreamConfig {
157129 channels : config. output_channels ,
158130 sample_rate : config. sample_rate ,
159131 buffer_size : config. buffer_size ,
160132 } ;
161133
162- // Core Audio's HAL AU has two buses, each with a hardware side and a
163- // client (app) side. We set the stream format on the client-facing side;
164- // the AU's built-in converter handles translation to/from the hardware format.
165- //
166- // Mic ─[Scope::Input]──▶ Input Bus ──[Scope::Output]─▶ App
167- // (hardware side) (client side)
168- //
169- // App ─[Scope::Input]──▶ Output Bus ─[Scope::Output]─▶ Speaker
170- // (client side) (hardware side)
171- //
172- // So the client side is Scope::Output for the input bus (where we read
173- // captured samples) and Scope::Input for the output bus (where we write
174- // playback samples). See Apple TN2091 for details.
175- let input_asbd = asbd_from_config ( & input_stream_config, sample_format) ;
134+ // Client-side format: Scope::Output for input bus, Scope::Input for output bus.
135+ let input_asbd = asbd_from_config ( input_stream_config, sample_format) ;
176136 audio_unit. set_property (
177137 kAudioUnitProperty_StreamFormat,
178138 Scope :: Output ,
179139 Element :: Input ,
180140 Some ( & input_asbd) ,
181141 ) ?;
182142
183- let output_asbd = asbd_from_config ( & output_stream_config, sample_format) ;
143+ let output_asbd = asbd_from_config ( output_stream_config, sample_format) ;
184144 audio_unit. set_property (
185145 kAudioUnitProperty_StreamFormat,
186146 Scope :: Input ,
187147 Element :: Output ,
188148 Some ( & output_asbd) ,
189149 ) ?;
190150
191- // Buffer frame size is a device-level property. Element::Output (0) is
192- // the standard convention for Scope::Global properties.
193151 if let BufferSize :: Fixed ( buffer_size) = & config. buffer_size {
194152 audio_unit. set_property (
195153 kAudioDevicePropertyBufferFrameSize,
@@ -199,7 +157,6 @@ impl Device {
199157 ) ?;
200158 }
201159
202- // Allocate input buffer for the current device buffer size.
203160 let current_buffer_size = get_device_buffer_frame_size ( & audio_unit) . map_err ( |e| {
204161 BuildStreamError :: BackendSpecific {
205162 err : BackendSpecificError {
@@ -217,7 +174,6 @@ impl Device {
217174 let input_buffer_bytes = current_buffer_size * input_channels * sample_bytes;
218175 let mut input_buffer: Box < [ u8 ] > = vec ! [ 0u8 ; input_buffer_bytes] . into_boxed_slice ( ) ;
219176
220- // Wrap error callback in Arc<Mutex> for sharing between callback and disconnect handler
221177 let error_callback = Arc :: new ( Mutex :: new ( error_callback) ) ;
222178 let error_callback_for_callback = error_callback. clone ( ) ;
223179
@@ -238,7 +194,7 @@ impl Device {
238194 if io_data. is_null ( ) {
239195 return kAudio_ParamError;
240196 }
241- // SAFETY: io_data validated as non-null above
197+ // SAFETY: io_data validated as non-null above.
242198 let buffer_list = unsafe { & mut * io_data } ;
243199 if buffer_list. mNumberBuffers == 0 {
244200 return kAudio_ParamError;
@@ -253,16 +209,12 @@ impl Device {
253209 return kAudio_ParamError;
254210 }
255211
256- // SAFETY: in_time_stamp is valid per CoreAudio contract
212+ // SAFETY: in_time_stamp is valid per CoreAudio callback contract.
257213 let timestamp: & AudioTimeStamp = unsafe { in_time_stamp. as_ref ( ) } ;
258214
259- // Create StreamInstant for callback_instant
260215 let callback_instant = match host_time_to_stream_instant ( timestamp. mHostTime ) {
261216 Err ( err) => {
262217 invoke_error_callback ( & error_callback_for_callback, err. into ( ) ) ;
263- // Return 0 (noErr) to keep the stream alive while notifying the error
264- // callback. This matches input/output stream behavior and allows graceful
265- // degradation rather than stopping the stream on transient errors.
266218 return 0 ;
267219 }
268220 Ok ( cb) => cb,
@@ -274,7 +226,7 @@ impl Device {
274226 }
275227 let output_samples = buffer. mDataByteSize as usize / sample_bytes;
276228
277- // SAFETY: buffer.mData validated as non-null above
229+ // SAFETY: buffer.mData validated as non-null above.
278230 let mut output_data = unsafe {
279231 Data :: from_parts ( buffer. mData as * mut ( ) , output_samples, sample_format)
280232 } ;
@@ -289,7 +241,6 @@ impl Device {
289241 & error_callback_for_callback,
290242 ) ;
291243
292- // Create callback info with latency-adjusted times
293244 let input_timestamp = crate :: InputStreamTimestamp {
294245 callback : callback_instant,
295246 capture,
@@ -299,9 +250,6 @@ impl Device {
299250 playback,
300251 } ;
301252
302- // Pull input from Element 1 using AudioUnitRender
303- // use the pre-allocated input_buffer
304- // Set up AudioBufferList pointing to our input buffer
305253 let mut input_buffer_list = AudioBufferList {
306254 mNumberBuffers : 1 ,
307255 mBuffers : [ AudioBuffer {
@@ -311,13 +259,8 @@ impl Device {
311259 } ] ,
312260 } ;
313261
314- // SAFETY: AudioUnitRender is called with valid parameters:
315- // - raw_audio_unit is valid for the callback duration
316- // - input_buffer_list is created just above on the stack with a pointer to
317- // input_buffer, which is properly aligned (Rust's standard allocators return
318- // pointers aligned to at least 8 bytes, exceeding f32/i16 requirements)
319- // - input_buffer has been bounds-checked above to ensure sufficient capacity
320- // - All other parameters (timestamps, flags, etc.) come from CoreAudio itself
262+ // SAFETY: raw_audio_unit is valid for the callback duration,
263+ // input_buffer_list points to bounds-checked input_buffer.
321264 let status = unsafe {
322265 AudioUnitRender (
323266 raw_audio_unit,
@@ -330,8 +273,6 @@ impl Device {
330273 } ;
331274
332275 if status != 0 {
333- // Report error but continue with silence for graceful degradation
334- // The application should decide what to do.
335276 invoke_error_callback (
336277 & error_callback_for_callback,
337278 StreamError :: BackendSpecific {
@@ -346,18 +287,8 @@ impl Device {
346287 input_buffer[ ..input_bytes] . fill ( 0 ) ;
347288 }
348289
349- // SAFETY: Creating Data from input_buffer is safe because:
350- // - input_buffer is a valid Vec<u8> owned by this closure
351- // - input_samples (num_frames * input_channels) was bounds-checked above to ensure
352- // input_samples * sample_bytes <= input_buffer.len()
353- // - AudioUnitRender just filled the buffer with valid audio data (or we filled
354- // it with silence on error)
355- // - The Data lifetime is scoped to this callback and doesn't outlive input_buffer
356- // - The pointer is suitably aligned: We successfully passed this buffer to
357- // AudioUnitRender (line 1314), which requires properly aligned buffers and would
358- // have failed if alignment were incorrect. Additionally, in practice, Rust's
359- // standard allocators (System, jemalloc, etc.) return pointers aligned to at
360- // least 8 bytes, which exceeds the requirements for f32 (4 bytes) and i16 (2 bytes).
290+ // SAFETY: input_buffer is bounds-checked, filled by AudioUnitRender
291+ // (or zeroed on error), and outlives this Data reference.
361292 let input_data = unsafe {
362293 Data :: from_parts (
363294 input_buffer. as_mut_ptr ( ) as * mut ( ) ,
@@ -369,18 +300,15 @@ impl Device {
369300 let callback_info = DuplexCallbackInfo :: new ( input_timestamp, output_timestamp) ;
370301 data_callback ( & input_data, & mut output_data, & callback_info) ;
371302
372- // Return 0 (noErr) to indicate successful render
373303 0
374304 } ,
375305 ) ;
376306
377- // Box the wrapper and get raw pointer for CoreAudio
378307 let wrapper = Box :: new ( DuplexProcWrapper {
379308 callback : duplex_proc,
380309 } ) ;
381310 let wrapper_ptr = Box :: into_raw ( wrapper) ;
382311
383- // Set up the render callback
384312 let render_callback = AURenderCallbackStruct {
385313 inputProc : Some ( duplex_input_proc) ,
386314 inputProcRefCon : wrapper_ptr as * mut std:: ffi:: c_void ,
@@ -393,7 +321,6 @@ impl Device {
393321 Some ( & render_callback) ,
394322 ) ?;
395323
396- // Create the stream inner, storing the callback pointer for cleanup
397324 let inner = StreamInner {
398325 playing : true ,
399326 audio_unit : ManuallyDrop :: new ( audio_unit) ,
@@ -402,17 +329,13 @@ impl Device {
402329 duplex_callback_ptr : Some ( DuplexCallbackPtr ( wrapper_ptr) ) ,
403330 } ;
404331
405- // Always propagate disconnect errors for duplex streams. A duplex stream
406- // is broken when either direction changes device.
407332 let error_callback_clone = error_callback. clone ( ) ;
408333 let error_callback_for_stream: super :: ErrorCallback = Box :: new ( move |err : StreamError | {
409334 invoke_error_callback ( & error_callback_clone, err) ;
410335 } ) ;
411336
412- // Create the duplex stream
413337 let stream = Stream :: new ( inner, error_callback_for_stream, true ) ?;
414338
415- // Start the audio unit
416339 stream
417340 . inner
418341 . lock ( )
0 commit comments