@@ -970,6 +970,60 @@ impl Device {
970970 Ok ( stream)
971971 }
972972
973+ /// Calculate latency-adjusted capture and playback timestamps with graceful error handling.
974+ ///
975+ /// Note: input/output streams use .expect() here and will panic - while it is
976+ /// inconsistent, I believe it is safer for duplex to handle gracefully on the real-time
977+ /// audio thread. Errors here should be extremely rare (timestamp arithmetic
978+ /// overflow/underflow).
979+ ///
980+ /// While theoretically and probably practically impossible, this is a critical
981+ /// user-experience issue -- the app shouldn't crash and potentially lose end-user data.
982+ /// The only reason `sub` would overflow is if delay is massively too large, which would
983+ /// indicate some kind of major bug or failure in the OS since callback_instant is derived
984+ /// from host time. Still, I think the best practice is NOT to panic but tell the app
985+ /// and fallback to a degraded latency estimate of no latency.
986+ fn calculate_duplex_timestamps < E > (
987+ callback_instant : crate :: StreamInstant ,
988+ delay : std:: time:: Duration ,
989+ error_callback : & Arc < Mutex < E > > ,
990+ ) -> ( crate :: StreamInstant , crate :: StreamInstant )
991+ where
992+ E : FnMut ( StreamError ) + Send + ' static ,
993+ {
994+ let capture = match callback_instant. sub ( delay) {
995+ Some ( c) => c,
996+ None => {
997+ invoke_error_callback (
998+ error_callback,
999+ StreamError :: BackendSpecific {
1000+ err : BackendSpecificError {
1001+ description : "Timestamp underflow calculating capture time" . to_string ( ) ,
1002+ } ,
1003+ } ,
1004+ ) ;
1005+ callback_instant
1006+ }
1007+ } ;
1008+
1009+ let playback = match callback_instant. add ( delay) {
1010+ Some ( p) => p,
1011+ None => {
1012+ invoke_error_callback (
1013+ error_callback,
1014+ StreamError :: BackendSpecific {
1015+ err : BackendSpecificError {
1016+ description : "Timestamp overflow calculating playback time" . to_string ( ) ,
1017+ } ,
1018+ } ,
1019+ ) ;
1020+ callback_instant
1021+ }
1022+ } ;
1023+
1024+ ( capture, playback)
1025+ }
1026+
9731027 /// Build a duplex stream with synchronized input and output.
9741028 ///
9751029 /// This creates a single HAL AudioUnit with both input and output enabled,
@@ -1117,6 +1171,16 @@ impl Device {
11171171 in_number_frames : u32 ,
11181172 io_data : * mut AudioBufferList |
11191173 -> i32 {
1174+ // Validate output buffer structure early to avoid wasted work
1175+ if io_data. is_null ( ) {
1176+ return kAudio_ParamError;
1177+ }
1178+ // SAFETY: io_data validated as non-null above
1179+ let buffer_list = unsafe { & mut * io_data } ;
1180+ if buffer_list. mNumberBuffers == 0 {
1181+ return kAudio_ParamError;
1182+ }
1183+
11201184 let num_frames = in_number_frames as usize ;
11211185 let input_samples = num_frames * input_channels;
11221186 let input_bytes = input_samples * sample_bytes;
@@ -1153,6 +1217,17 @@ impl Device {
11531217 Ok ( cb) => cb,
11541218 } ;
11551219
1220+ let buffer = & mut buffer_list. mBuffers [ 0 ] ;
1221+ if buffer. mData . is_null ( ) {
1222+ return kAudio_ParamError;
1223+ }
1224+ let output_samples = buffer. mDataByteSize as usize / sample_bytes;
1225+
1226+ // SAFETY: buffer.mData validated as non-null above
1227+ let mut output_data = unsafe {
1228+ Data :: from_parts ( buffer. mData as * mut ( ) , output_samples, sample_format)
1229+ } ;
1230+
11561231 // Calculate latency-adjusted timestamps (matching input/output pattern)
11571232 let buffer_frames = num_frames;
11581233 // Use device buffer size for latency calculation if available
@@ -1162,62 +1237,66 @@ impl Device {
11621237 ) ;
11631238 let delay = frames_to_duration ( latency_frames, sample_rate) ;
11641239
1165- // Capture time: when input was actually captured (in the past)
1166- let capture = callback_instant
1167- . sub ( delay)
1168- . expect ( "`capture` occurs before origin of `StreamInstant`" ) ;
1240+ let ( capture, playback) = Self :: calculate_duplex_timestamps (
1241+ callback_instant,
1242+ delay,
1243+ & error_callback_for_callback,
1244+ ) ;
11691245
1170- // Playback time: when output will actually play (in the future)
1171- let playback = callback_instant
1172- . add ( delay)
1173- . expect ( "`playback` occurs beyond representation supported by `StreamInstant`" ) ;
1246+ // Create callback info with latency-adjusted times
1247+ let input_timestamp = crate :: InputStreamTimestamp {
1248+ callback : callback_instant,
1249+ capture,
1250+ } ;
1251+ let output_timestamp = crate :: OutputStreamTimestamp {
1252+ callback : callback_instant,
1253+ playback,
1254+ } ;
11741255
11751256 // Pull input from Element 1 using AudioUnitRender
1176- // We use the pre-allocated input_buffer
1177- unsafe {
1178- // Set up AudioBufferList pointing to our input buffer
1179- let mut input_buffer_list = AudioBufferList {
1180- mNumberBuffers : 1 ,
1181- mBuffers : [ AudioBuffer {
1182- mNumberChannels : input_channels as u32 ,
1183- mDataByteSize : input_bytes as u32 ,
1184- mData : input_buffer. as_mut_ptr ( ) as * mut std:: ffi:: c_void ,
1185- } ] ,
1186- } ;
1187-
1188- let status = AudioUnitRender (
1257+ // use the pre-allocated input_buffer
1258+ // Set up AudioBufferList pointing to our input buffer
1259+ let mut input_buffer_list = AudioBufferList {
1260+ mNumberBuffers : 1 ,
1261+ mBuffers : [ AudioBuffer {
1262+ mNumberChannels : input_channels as u32 ,
1263+ mDataByteSize : input_bytes as u32 ,
1264+ mData : input_buffer. as_mut_ptr ( ) as * mut std:: ffi:: c_void ,
1265+ } ] ,
1266+ } ;
1267+
1268+ // SAFETY: AudioUnitRender is called with valid parameters:
1269+ // - raw_audio_unit is valid for the callback duration
1270+ // - input_buffer_list is created just above on the stack
1271+ // - All other parameters come from Core Audio itself
1272+ let status = unsafe {
1273+ AudioUnitRender (
11891274 raw_audio_unit,
11901275 io_action_flags. as_ptr ( ) ,
11911276 in_time_stamp,
11921277 1 , // Element 1 = input
11931278 in_number_frames,
11941279 NonNull :: new_unchecked ( & mut input_buffer_list) ,
1195- ) ;
1280+ )
1281+ } ;
11961282
1197- if status != 0 {
1198- // Report error but continue with silence for graceful degradation
1199- invoke_error_callback (
1200- & error_callback_for_callback ,
1201- StreamError :: BackendSpecific {
1202- err : BackendSpecificError {
1203- description : format ! (
1204- "AudioUnitRender failed for input: OSStatus {}" ,
1205- status
1206- ) ,
1207- } ,
1283+ if status != 0 {
1284+ // Report error but continue with silence for graceful degradation
1285+ // The application should decide what to do.
1286+ invoke_error_callback (
1287+ & error_callback_for_callback ,
1288+ StreamError :: BackendSpecific {
1289+ err : BackendSpecificError {
1290+ description : format ! (
1291+ "AudioUnitRender failed for input: OSStatus {}" ,
1292+ status
1293+ ) ,
12081294 } ,
1209- ) ;
1210- input_buffer[ ..input_bytes] . fill ( 0 ) ;
1211- }
1212- }
1213-
1214- // Get output buffer from CoreAudio
1215- if io_data. is_null ( ) {
1216- // Return error - Core Audio should never pass null, this is a fatal error
1217- return kAudio_ParamError;
1295+ } ,
1296+ ) ;
1297+ input_buffer[ ..input_bytes] . fill ( 0 ) ;
12181298 }
12191299
1220- // Create Data wrappers for input and output
12211300 let input_data = unsafe {
12221301 Data :: from_parts (
12231302 input_buffer. as_mut_ptr ( ) as * mut ( ) ,
@@ -1226,35 +1305,7 @@ impl Device {
12261305 )
12271306 } ;
12281307
1229- // SAFETY: io_data is guaranteed valid by Core Audio for the duration of this callback
1230- let buffer_list = unsafe { & mut * io_data } ;
1231- if buffer_list. mNumberBuffers == 0 {
1232- // Return error - no buffers available, cannot render
1233- return kAudio_ParamError;
1234- }
1235- let buffer = & mut buffer_list. mBuffers [ 0 ] ;
1236- if buffer. mData . is_null ( ) {
1237- // Return error - null buffer data, cannot render
1238- return kAudio_ParamError;
1239- }
1240- let output_samples = buffer. mDataByteSize as usize / sample_bytes;
1241- // SAFETY: buffer.mData points to valid audio data provided by Core Audio
1242- let mut output_data = unsafe {
1243- Data :: from_parts ( buffer. mData as * mut ( ) , output_samples, sample_format)
1244- } ;
1245-
1246- // Create callback info with latency-adjusted times
1247- let input_timestamp = crate :: InputStreamTimestamp {
1248- callback : callback_instant,
1249- capture,
1250- } ;
1251- let output_timestamp = crate :: OutputStreamTimestamp {
1252- callback : callback_instant,
1253- playback,
1254- } ;
12551308 let callback_info = DuplexCallbackInfo :: new ( input_timestamp, output_timestamp) ;
1256-
1257- // Call user callback with input and output Data
12581309 data_callback ( & input_data, & mut output_data, & callback_info) ;
12591310
12601311 // Return 0 (noErr) to indicate successful render
0 commit comments