Skip to content

Commit a344d4c

Browse files
committed
harden duplex callback.
place validation at the top of duplex_proc narrow the scope of unsafe blocks and add SAFETY: comments replace expect on arithmetic overflow to notify app of an error and fallback to 0 latency extract timestamp calculation to separate fn for readability (the comment is long)
1 parent fc5e274 commit a344d4c

1 file changed

Lines changed: 122 additions & 71 deletions

File tree

src/host/coreaudio/macos/device.rs

Lines changed: 122 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)