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, 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::Hardware,
26    platform_requirements: &["macos"],
27    entropy_rate_estimate: 10000.0,
28    composite: false,
29};
30
31/// Entropy source that harvests thermal noise from the microphone ADC.
32pub struct AudioNoiseSource;
33
34impl EntropySource for AudioNoiseSource {
35    fn info(&self) -> &SourceInfo {
36        &AUDIO_NOISE_INFO
37    }
38
39    fn is_available(&self) -> bool {
40        cfg!(target_os = "macos") && command_exists("ffmpeg")
41    }
42
43    fn collect(&self, n_samples: usize) -> Vec<u8> {
44        // Capture raw signed 16-bit PCM audio from the default input device.
45        // ffmpeg -f avfoundation -i ":0" -t 0.1 -f s16le -ar 44100 -ac 1 pipe:1
46        let result = std::process::Command::new("ffmpeg")
47            .args([
48                "-f",
49                "avfoundation",
50                "-i",
51                ":0",
52                "-t",
53                CAPTURE_DURATION,
54                "-f",
55                "s16le",
56                "-ar",
57                SAMPLE_RATE,
58                "-ac",
59                "1",
60                "pipe:1",
61            ])
62            .stdin(std::process::Stdio::null())
63            .stdout(std::process::Stdio::piped())
64            .stderr(std::process::Stdio::null())
65            .output();
66
67        let raw_audio = match result {
68            Ok(output) if output.status.success() => output.stdout,
69            _ => return Vec::new(),
70        };
71
72        // Each sample is 2 bytes (signed 16-bit little-endian).
73        // Extract the lower 4 bits of each sample as entropy.
74        let nibbles = raw_audio.chunks_exact(2).map(|chunk| {
75            let sample = i16::from_le_bytes([chunk[0], chunk[1]]);
76            (sample & 0x0F) as u8
77        });
78
79        pack_nibbles(nibbles, n_samples)
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn audio_noise_info() {
89        let src = AudioNoiseSource;
90        assert_eq!(src.name(), "audio_noise");
91        assert_eq!(src.info().category, SourceCategory::Hardware);
92        assert_eq!(src.info().entropy_rate_estimate, 10000.0);
93    }
94}