Skip to main content

openentropy_core/sources/thermal/
audio_pll_timing.rs

1//! Audio PLL clock jitter — phase noise from the audio subsystem oscillator.
2//!
3//! The audio subsystem has its own Phase-Locked Loop (PLL) generating sample
4//! clocks. By rapidly querying CoreAudio device properties, we measure timing
5//! jitter from crossing the audio/CPU clock domain boundary.
6//!
7//! The PLL phase noise arises from:
8//! - Thermal noise in VCO transistors (Johnson-Nyquist)
9//! - Shot noise in charge pump current
10//! - Reference oscillator crystal phase noise
11//!
12
13use 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
34/// Entropy source that harvests PLL phase noise from audio subsystem queries.
35pub 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            // Cycle through different property queries to exercise different
73            // code paths in the audio subsystem, each crossing the PLL boundary.
74            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] // Requires audio hardware
115    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}