use crate::source::{EntropySource, Platform, Requirement, SourceCategory, SourceInfo};
use crate::sources::helpers::{command_exists, pack_nibbles, run_command_output_timeout};
const CAPTURE_DURATION: &str = "0.1";
const SAMPLE_RATE: &str = "44100";
static AUDIO_NOISE_INFO: SourceInfo = SourceInfo {
name: "audio_noise",
description: "Microphone ADC thermal noise (Johnson-Nyquist) via ffmpeg",
physics: "Records from the microphone ADC with no signal present. The LSBs capture \
Johnson-Nyquist noise \u{2014} thermal agitation of electrons in the input \
impedance. At audio frequencies (up to ~44 kHz), this noise is entirely \
classical: hf \u{226a} kT by a factor of ~10^8 at room temperature. Laptop \
audio codecs use CMOS input stages where channel thermal noise and 1/f \
flicker noise dominate; shot noise is negligible. \
Voltage noise \u{221d} \u{221a}(4kT R \u{0394}f).",
category: SourceCategory::Sensor,
platform: Platform::MacOS,
requirements: &[Requirement::AudioUnit],
entropy_rate_estimate: 6.0,
composite: false,
is_fast: false,
};
pub struct AudioNoiseConfig {
pub device_index: Option<u32>,
}
impl Default for AudioNoiseConfig {
fn default() -> Self {
let device_index = std::env::var("OPENENTROPY_AUDIO_DEVICE")
.ok()
.and_then(|s| s.parse().ok());
Self { device_index }
}
}
#[derive(Default)]
pub struct AudioNoiseSource {
pub config: AudioNoiseConfig,
}
impl EntropySource for AudioNoiseSource {
fn info(&self) -> &SourceInfo {
&AUDIO_NOISE_INFO
}
fn is_available(&self) -> bool {
cfg!(target_os = "macos") && command_exists("ffmpeg")
}
fn collect(&self, n_samples: usize) -> Vec<u8> {
let device_input = format!(":{}", self.config.device_index.unwrap_or(0));
let result = run_command_output_timeout(
"ffmpeg",
&[
"-hide_banner",
"-loglevel",
"error",
"-nostdin",
"-f",
"avfoundation",
"-i",
&device_input,
"-t",
CAPTURE_DURATION,
"-f",
"s16le",
"-ar",
SAMPLE_RATE,
"-ac",
"1",
"pipe:1",
],
5000, );
let raw_audio = match result {
Some(output) => output.stdout,
None => return Vec::new(),
};
let nibbles = raw_audio.chunks_exact(2).map(|chunk| {
let sample = i16::from_le_bytes([chunk[0], chunk[1]]);
(sample & 0x0F) as u8
});
pack_nibbles(nibbles, n_samples)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn audio_noise_info() {
let src = AudioNoiseSource::default();
assert_eq!(src.name(), "audio_noise");
assert_eq!(src.info().category, SourceCategory::Sensor);
assert_eq!(src.info().entropy_rate_estimate, 6.0);
assert!(!src.info().composite);
}
#[test]
fn audio_config_default_is_none() {
let config = AudioNoiseConfig { device_index: None };
assert!(config.device_index.is_none());
}
#[test]
fn audio_config_explicit_device() {
let src = AudioNoiseSource {
config: AudioNoiseConfig {
device_index: Some(2),
},
};
assert_eq!(src.config.device_index, Some(2));
}
#[test]
#[cfg(target_os = "macos")]
#[ignore] fn audio_noise_collects_bytes() {
let src = AudioNoiseSource::default();
if src.is_available() {
let data = src.collect(64);
assert!(!data.is_empty());
assert!(data.len() <= 64);
}
}
}