openentropy_core/sources/thermal/
audio_pll_timing.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
14#[cfg(target_os = "macos")]
15use crate::sources::helpers::extract_timing_entropy;
16
17static AUDIO_PLL_TIMING_INFO: SourceInfo = SourceInfo {
18 name: "audio_pll_timing",
19 description: "Audio PLL clock jitter from CoreAudio device property queries",
20 physics: "Rapidly queries CoreAudio device properties (sample rate, latency) that \
21 cross the audio PLL / CPU clock domain boundary. The audio subsystem\u{2019}s \
22 PLL has thermally-driven phase noise from VCO transistor Johnson-Nyquist \
23 noise, charge pump shot noise, and crystal reference jitter. Each query \
24 timing captures the instantaneous phase relationship between these \
25 independent clock domains.",
26 category: SourceCategory::Thermal,
27 platform: Platform::MacOS,
28 requirements: &[Requirement::AudioUnit],
29 entropy_rate_estimate: 3.0,
30 composite: false,
31 is_fast: true,
32};
33
34pub struct AudioPLLTimingSource;
36
37impl EntropySource for AudioPLLTimingSource {
38 fn info(&self) -> &SourceInfo {
39 &AUDIO_PLL_TIMING_INFO
40 }
41
42 fn is_available(&self) -> bool {
43 #[cfg(target_os = "macos")]
44 {
45 super::coreaudio_ffi::get_default_output_device() != 0
46 }
47 #[cfg(not(target_os = "macos"))]
48 {
49 false
50 }
51 }
52
53 fn collect(&self, n_samples: usize) -> Vec<u8> {
54 #[cfg(not(target_os = "macos"))]
55 {
56 let _ = n_samples;
57 Vec::new()
58 }
59
60 #[cfg(target_os = "macos")]
61 {
62 use super::coreaudio_ffi;
63
64 let device = coreaudio_ffi::get_default_output_device();
65 if device == 0 {
66 return Vec::new();
67 }
68
69 let raw_count = n_samples * 4 + 64;
70 let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
71
72 let selectors = [
75 (
76 coreaudio_ffi::AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE,
77 coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
78 ),
79 (
80 coreaudio_ffi::AUDIO_DEVICE_PROPERTY_LATENCY,
81 coreaudio_ffi::AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT,
82 ),
83 (
84 coreaudio_ffi::AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE,
85 coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
86 ),
87 ];
88
89 for i in 0..raw_count {
90 let (sel, scope) = selectors[i % selectors.len()];
91 let elapsed = coreaudio_ffi::query_device_property_timed(device, sel, scope);
92 timings.push(elapsed.as_nanos() as u64);
93 }
94
95 extract_timing_entropy(&timings, n_samples)
96 }
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn info() {
106 let src = AudioPLLTimingSource;
107 assert_eq!(src.name(), "audio_pll_timing");
108 assert_eq!(src.info().category, SourceCategory::Thermal);
109 assert!(!src.info().composite);
110 }
111
112 #[test]
113 #[cfg(target_os = "macos")]
114 #[ignore] fn collects_bytes() {
116 let src = AudioPLLTimingSource;
117 if src.is_available() {
118 let data = src.collect(64);
119 assert!(!data.is_empty());
120 assert!(data.len() <= 64);
121 }
122 }
123}