Skip to main content

openentropy_core/sources/
audio.rs

1//! AudioNoiseSource — Microphone ADC noise via ffmpeg.
2//!
3//! Captures a short burst of audio from the default input device using ffmpeg's
4//! avfoundation backend, then extracts the lower 4 bits of each int16 sample.
5//! These LSBs are dominated by Johnson-Nyquist thermal noise.
6
7use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
8
9use super::helpers::{command_exists, pack_nibbles};
10
11/// Duration of audio capture in seconds.
12const CAPTURE_DURATION: &str = "0.1";
13
14/// Sample rate for audio capture.
15const SAMPLE_RATE: &str = "44100";
16
17static AUDIO_NOISE_INFO: SourceInfo = SourceInfo {
18    name: "audio_noise",
19    description: "Microphone ADC thermal noise (Johnson-Nyquist) via ffmpeg",
20    physics: "Records from the microphone ADC with no signal present. The LSBs capture \
21              Johnson-Nyquist noise \u{2014} thermal agitation of electrons in the input \
22              impedance. This is genuine quantum-origin entropy: random electron motion \
23              in a resistor at temperature T produces voltage noise proportional to \
24              \u{221a}(4kT R \u{0394}f).",
25    category: SourceCategory::Sensor,
26    platform: Platform::MacOS,
27    requirements: &[Requirement::AudioUnit],
28    entropy_rate_estimate: 10000.0,
29    composite: false,
30};
31
32/// Entropy source that harvests thermal noise from the microphone ADC.
33pub struct AudioNoiseSource;
34
35impl EntropySource for AudioNoiseSource {
36    fn info(&self) -> &SourceInfo {
37        &AUDIO_NOISE_INFO
38    }
39
40    fn is_available(&self) -> bool {
41        cfg!(target_os = "macos") && command_exists("ffmpeg")
42    }
43
44    fn collect(&self, n_samples: usize) -> Vec<u8> {
45        // Capture raw signed 16-bit PCM audio from the default input device.
46        // ffmpeg -f avfoundation -i ":0" -t 0.1 -f s16le -ar 44100 -ac 1 pipe:1
47        let result = std::process::Command::new("ffmpeg")
48            .args([
49                "-f",
50                "avfoundation",
51                "-i",
52                ":0",
53                "-t",
54                CAPTURE_DURATION,
55                "-f",
56                "s16le",
57                "-ar",
58                SAMPLE_RATE,
59                "-ac",
60                "1",
61                "pipe:1",
62            ])
63            .stdin(std::process::Stdio::null())
64            .stdout(std::process::Stdio::piped())
65            .stderr(std::process::Stdio::null())
66            .output();
67
68        let raw_audio = match result {
69            Ok(output) if output.status.success() => output.stdout,
70            _ => return Vec::new(),
71        };
72
73        // Each sample is 2 bytes (signed 16-bit little-endian).
74        // Extract the lower 4 bits of each sample as entropy.
75        let nibbles = raw_audio.chunks_exact(2).map(|chunk| {
76            let sample = i16::from_le_bytes([chunk[0], chunk[1]]);
77            (sample & 0x0F) as u8
78        });
79
80        pack_nibbles(nibbles, n_samples)
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn audio_noise_info() {
90        let src = AudioNoiseSource;
91        assert_eq!(src.name(), "audio_noise");
92        assert_eq!(src.info().category, SourceCategory::Sensor);
93        assert_eq!(src.info().entropy_rate_estimate, 10000.0);
94        assert!(!src.info().composite);
95    }
96
97    #[test]
98    #[cfg(target_os = "macos")]
99    #[ignore] // Requires microphone and ffmpeg
100    fn audio_noise_collects_bytes() {
101        let src = AudioNoiseSource;
102        if src.is_available() {
103            let data = src.collect(64);
104            assert!(!data.is_empty());
105            assert!(data.len() <= 64);
106        }
107    }
108}