use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
#[cfg(target_os = "macos")]
use crate::sources::helpers::read_cntvct;
#[cfg(target_os = "macos")]
use crate::sources::helpers::xor_fold_u64;
static COUNTER_BEAT_INFO: SourceInfo = SourceInfo {
name: "counter_beat",
description: "Two-oscillator beat frequency: CPU counter (CNTVCT_EL0) vs audio PLL crystal",
physics: "Reads the ARM generic timer counter (CNTVCT_EL0, driven by a 24 MHz crystal) \
immediately before and after a CoreAudio property query that forces \
synchronization with the audio PLL clock domain. The query duration in raw \
counter ticks is modulated by the instantaneous phase relationship between \
the CPU crystal and the independent audio PLL crystal. XORing the counter \
value with this PLL-modulated duration produces a two-oscillator beat that \
encodes the phase difference between two independent oscillators. \
Entropy arises from independent \
Johnson-Nyquist thermal noise in each crystal's sustaining amplifier. \
The raw physical signal is preserved for statistical analysis.",
category: SourceCategory::Thermal,
platform: Platform::MacOS,
requirements: &[Requirement::AppleSilicon, Requirement::AudioUnit],
entropy_rate_estimate: 3.0,
composite: false,
is_fast: false,
};
pub struct CounterBeatSource;
impl EntropySource for CounterBeatSource {
fn info(&self) -> &SourceInfo {
&COUNTER_BEAT_INFO
}
fn is_available(&self) -> bool {
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
super::coreaudio_ffi::get_default_output_device() != 0
}
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
false
}
}
fn collect(&self, n_samples: usize) -> Vec<u8> {
#[cfg(not(all(target_os = "macos", target_arch = "aarch64")))]
{
let _ = n_samples;
Vec::new()
}
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
{
use super::coreaudio_ffi;
let device = coreaudio_ffi::get_default_output_device();
if device == 0 {
return Vec::new();
}
let selectors = [
(
coreaudio_ffi::AUDIO_DEVICE_PROPERTY_ACTUAL_SAMPLE_RATE,
coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
),
(
coreaudio_ffi::AUDIO_DEVICE_PROPERTY_LATENCY,
coreaudio_ffi::AUDIO_DEVICE_PROPERTY_SCOPE_OUTPUT,
),
(
coreaudio_ffi::AUDIO_DEVICE_PROPERTY_NOMINAL_SAMPLE_RATE,
coreaudio_ffi::AUDIO_OBJECT_PROPERTY_SCOPE_GLOBAL,
),
];
let raw_count = n_samples * 4 + 64;
let mut beats: Vec<u64> = Vec::with_capacity(raw_count);
for i in 0..raw_count {
let (sel, scope) = selectors[i % selectors.len()];
let counter_before = read_cntvct();
coreaudio_ffi::query_device_property_timed(device, sel, scope);
let counter_after = read_cntvct();
let pll_duration = counter_after.wrapping_sub(counter_before);
let beat = counter_before ^ pll_duration;
beats.push(beat);
}
if beats.len() < 4 {
return Vec::new();
}
let deltas: Vec<u64> = beats.windows(2).map(|w| w[1].wrapping_sub(w[0])).collect();
let xored: Vec<u64> = deltas.windows(2).map(|w| w[0] ^ w[1]).collect();
let mut output: Vec<u8> = xored.iter().map(|&x| xor_fold_u64(x)).collect();
output.truncate(n_samples);
output
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn info() {
let src = CounterBeatSource;
assert_eq!(src.name(), "counter_beat");
assert_eq!(src.info().category, SourceCategory::Thermal);
assert!(!src.info().composite);
}
#[test]
fn physics_mentions_two_oscillators() {
let src = CounterBeatSource;
assert!(src.info().physics.contains("CNTVCT_EL0"));
assert!(src.info().physics.contains("two-oscillator"));
assert!(src.info().physics.contains("phase difference"));
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
fn cntvct_is_nonzero() {
let v = read_cntvct();
assert!(v > 0);
}
#[test]
#[cfg(all(target_os = "macos", target_arch = "aarch64"))]
#[ignore] fn collects_bytes() {
let src = CounterBeatSource;
if src.is_available() {
let data = src.collect(64);
assert!(!data.is_empty());
assert!(data.len() <= 64);
}
}
}