Skip to main content

openentropy_core/sources/frontier/
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: 4000.0,
30    composite: false,
31};
32
33/// Entropy source that harvests PLL phase noise from audio subsystem queries.
34pub struct AudioPLLTimingSource;
35
36/// CoreAudio FFI bindings (macOS only).
37#[cfg(target_os = "macos")]
38mod coreaudio {
39    #[repr(C)]
40    pub struct AudioObjectPropertyAddress {
41        pub m_selector: u32,
42        pub m_scope: u32,
43        pub m_element: u32,
44    }
45
46    pub const AUDIO_HARDWARE_PROPERTY_DEFAULT_OUTPUT_DEVICE: u32 = 0x644F7574; // 'dOut'
47    pub const AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE: u32 = 0x6E737274; // 'nsrt'
48    pub const AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE: u32 = 0x61737264; // 'asrd'
49    pub const AUDIO_DEVICE_PROPERTY_LATENCY: u32 = 0x6C746E63; // 'ltnc'
50    pub const AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL: u32 = 0x676C6F62; // 'glob'
51    pub const AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN: u32 = 0;
52    pub const AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT: u32 = 0x6F757470; // 'outp'
53    pub const AUDIO_OBJECT_SYSTEM_OBJECT: u32 = 1;
54
55    #[link(name = "CoreAudio", kind = "framework")]
56    unsafe extern "C" {
57        pub fn AudioObjectGetPropertyData(
58            object_id: u32,
59            address: *const AudioObjectPropertyAddress,
60            qualifier_data_size: u32,
61            qualifier_data: *const std::ffi::c_void,
62            data_size: *mut u32,
63            data: *mut std::ffi::c_void,
64        ) -> i32;
65    }
66
67    /// Get the default output audio device ID, or 0 if none.
68    pub fn get_default_output_device() -> u32 {
69        let addr = AudioObjectPropertyAddress {
70            m_selector: AUDIO_HARDWARE_PROPERTY_DEFAULT_OUTPUT_DEVICE,
71            m_scope: AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
72            m_element: AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
73        };
74        let mut device: u32 = 0;
75        let mut size: u32 = std::mem::size_of::<u32>() as u32;
76        // SAFETY: AudioObjectGetPropertyData is a CoreAudio API that reads a property
77        // from the system audio object. We pass valid pointers to stack-allocated `size`
78        // and `device` with correct sizes. The function writes at most `size` bytes.
79        let status = unsafe {
80            AudioObjectGetPropertyData(
81                AUDIO_OBJECT_SYSTEM_OBJECT,
82                &addr,
83                0,
84                std::ptr::null(),
85                &mut size,
86                &mut device as *mut u32 as *mut std::ffi::c_void,
87            )
88        };
89        if status == 0 { device } else { 0 }
90    }
91
92    /// Query a device property and return the elapsed duration.
93    pub fn query_device_property(device: u32, selector: u32, scope: u32) -> std::time::Duration {
94        let addr = AudioObjectPropertyAddress {
95            m_selector: selector,
96            m_scope: scope,
97            m_element: AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
98        };
99        let mut data = [0u8; 8];
100        let mut size: u32 = 8;
101
102        let t0 = std::time::Instant::now();
103        // SAFETY: AudioObjectGetPropertyData reads a property from a valid audio device.
104        // `data` is an 8-byte stack buffer, and `size` is set to 8, which is sufficient
105        // for all queried properties (f64 sample rate or u32 latency).
106        unsafe {
107            AudioObjectGetPropertyData(
108                device,
109                &addr,
110                0,
111                std::ptr::null(),
112                &mut size,
113                data.as_mut_ptr() as *mut std::ffi::c_void,
114            );
115        }
116        t0.elapsed()
117    }
118}
119
120impl EntropySource for AudioPLLTimingSource {
121    fn info(&self) -> &SourceInfo {
122        &AUDIO_PLL_TIMING_INFO
123    }
124
125    fn is_available(&self) -> bool {
126        #[cfg(target_os = "macos")]
127        {
128            coreaudio::get_default_output_device() != 0
129        }
130        #[cfg(not(target_os = "macos"))]
131        {
132            false
133        }
134    }
135
136    fn collect(&self, n_samples: usize) -> Vec<u8> {
137        #[cfg(not(target_os = "macos"))]
138        {
139            let _ = n_samples;
140            Vec::new()
141        }
142
143        #[cfg(target_os = "macos")]
144        {
145            let device = coreaudio::get_default_output_device();
146            if device == 0 {
147                return Vec::new();
148            }
149
150            let raw_count = n_samples * 4 + 64;
151            let mut timings: Vec<u64> = Vec::with_capacity(raw_count);
152
153            // Cycle through different property queries to exercise different
154            // code paths in the audio subsystem, each crossing the PLL boundary.
155            let selectors = [
156                (
157                    coreaudio::AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE,
158                    coreaudio::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
159                ),
160                (
161                    coreaudio::AUDIO_DEVICE_PROPERTY_LATENCY,
162                    coreaudio::AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT,
163                ),
164                (
165                    coreaudio::AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE,
166                    coreaudio::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
167                ),
168            ];
169
170            for i in 0..raw_count {
171                let (sel, scope) = selectors[i % selectors.len()];
172                let elapsed = coreaudio::query_device_property(device, sel, scope);
173                timings.push(elapsed.as_nanos() as u64);
174            }
175
176            extract_timing_entropy(&timings, n_samples)
177        }
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[test]
186    fn info() {
187        let src = AudioPLLTimingSource;
188        assert_eq!(src.name(), "audio_pll_timing");
189        assert_eq!(src.info().category, SourceCategory::Thermal);
190        assert!(!src.info().composite);
191    }
192
193    #[test]
194    #[cfg(target_os = "macos")]
195    #[ignore] // Requires audio hardware
196    fn collects_bytes() {
197        let src = AudioPLLTimingSource;
198        if src.is_available() {
199            let data = src.collect(64);
200            assert!(!data.is_empty());
201            assert!(data.len() <= 64);
202        }
203    }
204}