openentropy_core/sources/
audio.rs1use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
8
9use super::helpers::{command_exists, pack_nibbles};
10
11const CAPTURE_DURATION: &str = "0.1";
13
14const 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
32pub 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 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 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] 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}