openentropy_core/sources/frontier/
counter_beat.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
42#[cfg(target_os = "macos")]
43use crate::sources::helpers::read_cntvct;
44#[cfg(target_os = "macos")]
45use crate::sources::helpers::xor_fold_u64;
46
47static COUNTER_BEAT_INFO: SourceInfo = SourceInfo {
48 name: "counter_beat",
49 description: "Two-oscillator beat frequency: CPU counter (CNTVCT_EL0) vs audio PLL crystal",
50 physics: "Reads the ARM generic timer counter (CNTVCT_EL0, driven by a 24 MHz crystal) \
51 immediately before and after a CoreAudio property query that forces \
52 synchronization with the audio PLL clock domain. The query duration in raw \
53 counter ticks is modulated by the instantaneous phase relationship between \
54 the CPU crystal and the independent audio PLL crystal. XORing the counter \
55 value with this PLL-modulated duration produces a two-oscillator beat that \
56 encodes the phase difference between two independent oscillators. \
57 Entropy arises from independent \
58 Johnson-Nyquist thermal noise in each crystal's sustaining amplifier. \
59 The raw physical signal is preserved for statistical analysis.
60 raw physical signal for statistical analysis.",
61 category: SourceCategory::Thermal,
62 platform: Platform::MacOS,
63 requirements: &[Requirement::AppleSilicon, Requirement::AudioUnit],
64 entropy_rate_estimate: 2000.0,
65 composite: false,
66};
67
68pub struct CounterBeatSource;
74
75#[cfg(target_os = "macos")]
77mod coreaudio {
78 #[repr(C)]
79 pub struct AudioObjectPropertyAddress {
80 pub m_selector: u32,
81 pub m_scope: u32,
82 pub m_element: u32,
83 }
84
85 pub const AUDIO_OBJECT_SYSTEM_OBJECT: u32 = 1;
86 pub const AUDIO_HARDWARE_PROPERTY_DEFAULT_OUTPUT_DEVICE: u32 = 0x644F7574; pub const AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE: u32 = 0x61737264; pub const AUDIO_DEVICE_PROPERTY_LATENCY: u32 = 0x6C746E63; pub const AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE: u32 = 0x6E737274; pub const AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL: u32 = 0x676C6F62; pub const AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN: u32 = 0;
92 pub const AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT: u32 = 0x6F757470; #[link(name = "CoreAudio", kind = "framework")]
95 unsafe extern "C" {
96 pub fn AudioObjectGetPropertyData(
97 object_id: u32,
98 address: *const AudioObjectPropertyAddress,
99 qualifier_data_size: u32,
100 qualifier_data: *const std::ffi::c_void,
101 data_size: *mut u32,
102 data: *mut std::ffi::c_void,
103 ) -> i32;
104 }
105
106 pub fn get_default_output_device() -> u32 {
108 let addr = AudioObjectPropertyAddress {
109 m_selector: AUDIO_HARDWARE_PROPERTY_DEFAULT_OUTPUT_DEVICE,
110 m_scope: AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
111 m_element: AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
112 };
113 let mut device: u32 = 0;
114 let mut size: u32 = std::mem::size_of::<u32>() as u32;
115 let status = unsafe {
118 AudioObjectGetPropertyData(
119 AUDIO_OBJECT_SYSTEM_OBJECT,
120 &addr,
121 0,
122 std::ptr::null(),
123 &mut size,
124 &mut device as *mut u32 as *mut std::ffi::c_void,
125 )
126 };
127 if status == 0 { device } else { 0 }
128 }
129
130 pub fn query_audio_property(device: u32, selector: u32, scope: u32) {
135 let addr = AudioObjectPropertyAddress {
136 m_selector: selector,
137 m_scope: scope,
138 m_element: AUDIO_OBJECT_PROPERTY_ELEMENT_MAIN,
139 };
140 let mut data = [0u8; 8];
141 let mut size: u32 = 8;
142 unsafe {
145 AudioObjectGetPropertyData(
146 device,
147 &addr,
148 0,
149 std::ptr::null(),
150 &mut size,
151 data.as_mut_ptr() as *mut std::ffi::c_void,
152 );
153 }
154 std::hint::black_box(data);
156 }
157}
158
159impl EntropySource for CounterBeatSource {
160 fn info(&self) -> &SourceInfo {
161 &COUNTER_BEAT_INFO
162 }
163
164 fn is_available(&self) -> bool {
165 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
166 {
167 coreaudio::get_default_output_device() != 0
168 }
169 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
170 {
171 false
172 }
173 }
174
175 fn collect(&self, n_samples: usize) -> Vec<u8> {
176 #[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
177 {
178 let _ = n_samples;
179 Vec::new()
180 }
181
182 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
183 {
184 let device = coreaudio::get_default_output_device();
185 if device == 0 {
186 return Vec::new();
187 }
188
189 let selectors = [
192 (
193 coreaudio::AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE,
194 coreaudio::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
195 ),
196 (
197 coreaudio::AUDIO_DEVICE_PROPERTY_LATENCY,
198 coreaudio::AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT,
199 ),
200 (
201 coreaudio::AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE,
202 coreaudio::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
203 ),
204 ];
205
206 let raw_count = n_samples * 4 + 64;
208 let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
209
210 for i in 0..raw_count {
211 let (sel, scope) = selectors[i % selectors.len()];
212
213 let counter_before = read_cntvct();
215
216 coreaudio::query_audio_property(device, sel, scope);
218
219 let counter_after = read_cntvct();
221
222 let pll_duration = counter_after.wrapping_sub(counter_before);
228 let beat = counter_before ^ pll_duration;
229 beats.push(beat);
230 }
231
232 if beats.len() < 4 {
233 return Vec::new();
234 }
235
236 let deltas: Vec<u64> = beats.windows(2).map(|w| w[1].wrapping_sub(w[0])).collect();
239
240 let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
241
242 let mut output: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
243 output.truncate(n_samples);
244 output
245 }
246 }
247}
248
249#[cfg(test)]
250mod tests {
251 use super::*;
252
253 #[test]
254 fn info() {
255 let src = CounterBeatSource;
256 assert_eq!(src.name(), "counter_beat");
257 assert_eq!(src.info().category, SourceCategory::Thermal);
258 assert!(!src.info().composite);
259 }
260
261 #[test]
262 fn physics_mentions_two_oscillators() {
263 let src = CounterBeatSource;
264 assert!(src.info().physics.contains("CNTVCT_EL0"));
265 assert!(src.info().physics.contains("two-oscillator"));
266 assert!(src.info().physics.contains("phase difference"));
267 }
268
269 #[test]
270 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
271 fn cntvct_is_nonzero() {
272 let v = read_cntvct();
273 assert!(v > 0);
274 }
275
276 #[test]
277 #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
278 #[ignore] fn collects_bytes() {
280 let src = CounterBeatSource;
281 if src.is_available() {
282 let data = src.collect(64);
283 assert!(!data.is_empty());
284 assert!(data.len() <= 64);
285 }
286 }
287}