#![allow(dead_code)]
use crossbeam_channel::{Receiver, Sender};
use std::sync::{
atomic::{AtomicBool, Ordering},
Arc,
};
use std::thread;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct AudioFeatures {
pub rms: f32,
pub centroid: f32,
pub flux: f32,
pub bands: [f32; 8],
}
impl Default for AudioFeatures {
fn default() -> Self {
Self {
rms: 0.0,
centroid: 0.5,
flux: 0.0,
bands: [0.0; 8],
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct OdePatch {
pub sigma: Option<f64>,
pub rho: Option<f64>,
pub beta: Option<f64>,
pub trigger_system_switch: bool,
pub features: AudioFeatures,
}
impl OdePatch {
pub fn is_empty(&self) -> bool {
self.sigma.is_none()
&& self.rho.is_none()
&& self.beta.is_none()
&& !self.trigger_system_switch
}
}
#[derive(Debug, Clone)]
pub struct BridgeConfig {
pub sigma_min: f64,
pub sigma_max: f64,
pub rho_min: f64,
pub rho_max: f64,
pub beta_min: f64,
pub beta_max: f64,
pub flux_switch_threshold: f32,
pub sigma_min_delta: f64,
pub rho_min_delta: f64,
pub fft_size: usize,
pub hop_size: usize,
pub rms_ref: f32,
pub flux_ref: f32,
}
impl Default for BridgeConfig {
fn default() -> Self {
Self {
sigma_min: 5.0,
sigma_max: 30.0,
rho_min: 15.0,
rho_max: 60.0,
beta_min: 1.5,
beta_max: 4.0,
flux_switch_threshold: 0.6,
sigma_min_delta: 0.25,
rho_min_delta: 0.5,
fft_size: 1024,
hop_size: 512,
rms_ref: 0.3,
flux_ref: 50.0,
}
}
}
pub struct AudioInputAnalyzer {
config: BridgeConfig,
frame_buf: Vec<f32>,
write_pos: usize,
hop_counter: usize,
prev_magnitudes: Vec<f32>,
window: Vec<f32>,
tx: Sender<AudioFeatures>,
}
impl AudioInputAnalyzer {
pub fn new(config: BridgeConfig, tx: Sender<AudioFeatures>) -> Self {
let n = config.fft_size;
let window: Vec<f32> = (0..n)
.map(|i| {
0.5 * (1.0 - (2.0 * std::f32::consts::PI * i as f32 / (n - 1) as f32).cos())
})
.collect();
Self {
frame_buf: vec![0.0; n],
write_pos: 0,
hop_counter: 0,
prev_magnitudes: vec![0.0; n / 2 + 1],
window,
config,
tx,
}
}
pub fn feed(&mut self, sample: f32) {
let n = self.config.fft_size;
self.frame_buf[self.write_pos % n] = sample;
self.write_pos += 1;
self.hop_counter += 1;
if self.hop_counter >= self.config.hop_size {
self.hop_counter = 0;
self.analyze();
}
}
fn analyze(&mut self) {
let n = self.config.fft_size;
let mut windowed: Vec<f32> = (0..n)
.map(|i| {
let buf_idx = (self.write_pos + i) % n;
self.frame_buf[buf_idx] * self.window[i]
})
.collect();
cooley_tukey_fft(&mut windowed);
let half = n / 2 + 1;
let magnitudes: Vec<f32> = (0..half)
.map(|k| {
let re = windowed[2 * k % n];
let im = if 2 * k + 1 < n { windowed[2 * k + 1] } else { 0.0 };
(re * re + im * im).sqrt()
})
.collect();
let rms_raw = {
let sum_sq: f32 = self.frame_buf.iter().map(|&s| s * s).sum();
(sum_sq / n as f32).sqrt()
};
let rms = (rms_raw / self.config.rms_ref).clamp(0.0, 1.0);
let total_mag: f32 = magnitudes.iter().sum();
let centroid_raw = if total_mag > 1e-9 {
magnitudes
.iter()
.enumerate()
.map(|(k, &m)| k as f32 * m)
.sum::<f32>()
/ total_mag
} else {
half as f32 / 2.0
};
let centroid = (centroid_raw / half as f32).clamp(0.0, 1.0);
let flux_raw: f32 = magnitudes
.iter()
.zip(self.prev_magnitudes.iter())
.map(|(&a, &b)| (a - b).abs())
.sum();
let flux = (flux_raw / self.config.flux_ref).clamp(0.0, 1.0);
let band_size = half / 8;
let mut bands = [0.0f32; 8];
for (b, band) in bands.iter_mut().enumerate() {
let start = b * band_size;
let end = ((b + 1) * band_size).min(half);
let energy: f32 = magnitudes[start..end].iter().map(|&m| m * m).sum();
let peak = if end > start {
magnitudes[start..end].iter().cloned().fold(0.0f32, f32::max)
} else {
1.0
};
*band = if peak > 1e-9 {
(energy / peak / (end - start) as f32).clamp(0.0, 1.0)
} else {
0.0
};
}
self.prev_magnitudes.clone_from_slice(&magnitudes);
let _ = self.tx.send(AudioFeatures {
rms,
centroid,
flux,
bands,
});
}
}
pub struct AudioOdeBridge {
config: BridgeConfig,
feature_rx: Receiver<AudioFeatures>,
patch_tx: Sender<OdePatch>,
last_sigma: f64,
last_rho: f64,
last_beta: f64,
}
impl AudioOdeBridge {
pub fn from_channels(
config: BridgeConfig,
feature_rx: Receiver<AudioFeatures>,
patch_tx: Sender<OdePatch>,
) -> Self {
let sigma_init = (config.sigma_min + config.sigma_max) / 2.0;
let rho_init = (config.rho_min + config.rho_max) / 2.0;
let beta_init = (config.beta_min + config.beta_max) / 2.0;
Self {
config,
feature_rx,
patch_tx,
last_sigma: sigma_init,
last_rho: rho_init,
last_beta: beta_init,
}
}
pub fn process_frame(&mut self, f: AudioFeatures) {
let cfg = &self.config;
let sigma = cfg.sigma_min + (cfg.sigma_max - cfg.sigma_min) * f.rms as f64;
let rho = cfg.rho_min + (cfg.rho_max - cfg.rho_min) * f.centroid as f64;
let beta = cfg.beta_min + (cfg.beta_max - cfg.beta_min) * f.bands[3] as f64;
let sigma_changed = (sigma - self.last_sigma).abs() >= cfg.sigma_min_delta;
let rho_changed = (rho - self.last_rho).abs() >= cfg.rho_min_delta;
let beta_changed = (beta - self.last_beta).abs() >= 0.05;
let flux_triggered = f.flux >= cfg.flux_switch_threshold;
if sigma_changed || rho_changed || beta_changed || flux_triggered {
if sigma_changed {
self.last_sigma = sigma;
}
if rho_changed {
self.last_rho = rho;
}
if beta_changed {
self.last_beta = beta;
}
let patch = OdePatch {
sigma: if sigma_changed { Some(sigma) } else { None },
rho: if rho_changed { Some(rho) } else { None },
beta: if beta_changed { Some(beta) } else { None },
trigger_system_switch: flux_triggered,
features: f,
};
let _ = self.patch_tx.send(patch);
}
}
pub fn drain(&mut self) {
while let Ok(f) = self.feature_rx.try_recv() {
self.process_frame(f);
}
}
pub fn run_background(mut self, stop: Arc<AtomicBool>) {
thread::Builder::new()
.name("audio-ode-bridge".into())
.spawn(move || {
while !stop.load(Ordering::Relaxed) {
if let Ok(f) = self.feature_rx.recv_timeout(std::time::Duration::from_millis(50)) {
self.process_frame(f);
}
}
})
.expect("audio-ode-bridge thread");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DualModeKind {
ForwardOnly,
ReverseOnly,
Both,
}
impl Default for DualModeKind {
fn default() -> Self {
Self::ForwardOnly
}
}
pub struct DualMode {
pub kind: DualModeKind,
pub bridge_config: BridgeConfig,
stop: Arc<AtomicBool>,
}
impl DualMode {
pub fn new(bridge_config: BridgeConfig) -> Self {
Self {
kind: DualModeKind::ForwardOnly,
bridge_config,
stop: Arc::new(AtomicBool::new(false)),
}
}
pub fn set_mode(&mut self, kind: DualModeKind) {
self.kind = kind;
log::info!("[audio_driven] mode changed to {:?}", kind);
}
pub fn reverse_active(&self) -> bool {
matches!(self.kind, DualModeKind::ReverseOnly | DualModeKind::Both)
}
pub fn forward_active(&self) -> bool {
matches!(self.kind, DualModeKind::ForwardOnly | DualModeKind::Both)
}
pub fn build_reverse_pipeline(
&self,
) -> (AudioInputAnalyzer, Receiver<OdePatch>, Arc<AtomicBool>) {
let (feat_tx, feat_rx) = crossbeam_channel::unbounded::<AudioFeatures>();
let (patch_tx, patch_rx) = crossbeam_channel::unbounded::<OdePatch>();
let stop = Arc::clone(&self.stop);
let analyzer = AudioInputAnalyzer::new(self.bridge_config.clone(), feat_tx);
let bridge =
AudioOdeBridge::from_channels(self.bridge_config.clone(), feat_rx, patch_tx);
bridge.run_background(Arc::clone(&stop));
(analyzer, patch_rx, stop)
}
pub fn stop(&self) {
self.stop.store(true, Ordering::Relaxed);
}
}
fn cooley_tukey_fft(buf: &mut Vec<f32>) {
let n = buf.len();
if n < 2 {
return;
}
let half = n / 2;
let mut c: Vec<(f32, f32)> = buf[..half]
.iter()
.map(|&re| (re, 0.0))
.collect();
let bits = (half as f32).log2() as usize;
for i in 0..half {
let j = bit_reverse(i, bits);
if i < j {
c.swap(i, j);
}
}
let mut len = 2usize;
while len <= half {
let ang = -2.0 * std::f32::consts::PI / len as f32;
let w_re = ang.cos();
let w_im = ang.sin();
let mut k = 0;
while k < half {
let (mut wr, mut wi) = (1.0f32, 0.0f32);
for j in 0..len / 2 {
let (ur, ui) = c[k + j];
let vr = wr * c[k + j + len / 2].0 - wi * c[k + j + len / 2].1;
let vi = wr * c[k + j + len / 2].1 + wi * c[k + j + len / 2].0;
c[k + j] = (ur + vr, ui + vi);
c[k + j + len / 2] = (ur - vr, ui - vi);
let new_wr = wr * w_re - wi * w_im;
wi = wr * w_im + wi * w_re;
wr = new_wr;
}
k += len;
}
len *= 2;
}
for (i, (re, im)) in c.iter().enumerate() {
if 2 * i < n {
buf[2 * i] = *re;
}
if 2 * i + 1 < n {
buf[2 * i + 1] = *im;
}
}
}
fn bit_reverse(x: usize, bits: usize) -> usize {
let mut result = 0usize;
let mut v = x;
for _ in 0..bits {
result = (result << 1) | (v & 1);
v >>= 1;
}
result
}
#[cfg(test)]
mod tests {
use super::*;
fn make_channels() -> (Sender<AudioFeatures>, Receiver<AudioFeatures>) {
crossbeam_channel::unbounded()
}
#[test]
fn audio_features_default() {
let f = AudioFeatures::default();
assert_eq!(f.rms, 0.0);
assert_eq!(f.bands, [0.0; 8]);
}
#[test]
fn analyzer_feeds_without_panic() {
let (tx, _rx) = make_channels();
let cfg = BridgeConfig {
fft_size: 16,
hop_size: 8,
..Default::default()
};
let mut analyzer = AudioInputAnalyzer::new(cfg, tx);
for i in 0..32 {
analyzer.feed((i as f32 * 0.01).sin());
}
}
#[test]
fn bridge_maps_rms_to_sigma_range() {
let (feat_tx, feat_rx) = crossbeam_channel::unbounded::<AudioFeatures>();
let (patch_tx, patch_rx) = crossbeam_channel::unbounded::<OdePatch>();
let cfg = BridgeConfig::default();
let sigma_min = cfg.sigma_min;
let sigma_max = cfg.sigma_max;
let mut bridge = AudioOdeBridge::from_channels(cfg, feat_rx, patch_tx);
let _ = feat_tx.send(AudioFeatures {
rms: 1.0,
centroid: 0.5,
flux: 0.0,
bands: [0.0; 8],
});
bridge.drain();
if let Ok(patch) = patch_rx.try_recv() {
if let Some(sigma) = patch.sigma {
assert!(
sigma >= sigma_min && sigma <= sigma_max + 1e-6,
"sigma {sigma} out of range [{sigma_min}, {sigma_max}]"
);
}
}
}
#[test]
fn bridge_triggers_switch_on_high_flux() {
let (feat_tx, feat_rx) = crossbeam_channel::unbounded::<AudioFeatures>();
let (patch_tx, patch_rx) = crossbeam_channel::unbounded::<OdePatch>();
let cfg = BridgeConfig {
flux_switch_threshold: 0.5,
..Default::default()
};
let mut bridge = AudioOdeBridge::from_channels(cfg, feat_rx, patch_tx);
let _ = feat_tx.send(AudioFeatures {
rms: 0.0,
centroid: 0.5,
flux: 0.9,
bands: [0.0; 8],
});
bridge.drain();
let patch = patch_rx.try_recv().expect("patch should be emitted");
assert!(patch.trigger_system_switch, "high flux should trigger switch");
}
#[test]
fn dual_mode_forward_active_by_default() {
let mode = DualMode::new(BridgeConfig::default());
assert!(mode.forward_active());
assert!(!mode.reverse_active());
}
#[test]
fn dual_mode_both() {
let mut mode = DualMode::new(BridgeConfig::default());
mode.set_mode(DualModeKind::Both);
assert!(mode.forward_active());
assert!(mode.reverse_active());
}
#[test]
fn bit_reverse_identity_for_zero() {
assert_eq!(bit_reverse(0, 8), 0);
}
#[test]
fn ode_patch_is_empty_when_no_changes() {
let p = OdePatch {
sigma: None,
rho: None,
beta: None,
trigger_system_switch: false,
features: AudioFeatures::default(),
};
assert!(p.is_empty());
}
}