#![allow(dead_code)]
use alloc::boxed::Box;
pub const FFT_RESOLUTION: usize = 512;
pub const CYCLES_PER_WINDOW: usize = 1;
pub const FFT_MIN_FUNDAMENTAL_MAG: f32 = 1e-4;
pub const FFT_FUND_SEARCH_BINS: usize = 3;
pub const INTERHARMONIC_GROUPS: usize = 49;
pub const CYCLES_FOR_INTERHARMONIC: usize = 10;
const MAGNITUDES_LEN: usize = FFT_RESOLUTION / 2 + 1;
const RAW_BUFFER_LEN: usize = 512;
pub fn resample_signal(signal: &[f32], new_len: usize) -> alloc::vec::Vec<f32> {
let n = signal.len();
let step = n as f32 / new_len as f32;
let mut resampled = alloc::vec::Vec::with_capacity(new_len);
for i in 0..new_len {
let pos = i as f32 * step;
let idx0 = crate::math::floor(pos) as usize % n;
let idx1 = (idx0 + 1) % n;
let fraction = pos - idx0 as f32;
let y0 = signal[idx0];
let y1 = signal[idx1];
resampled.push(y0 + (y1 - y0) * fraction);
}
resampled
}
fn calculate_harmonics_and_thd(
magnitudes: &[f32],
fund_bin: usize,
fundamental_mag: f32,
) -> ([f32; crate::types::NUMBER_HARMONICS], f32) {
let mut harmonics = [0.0; crate::types::NUMBER_HARMONICS];
let mut thd_sq_sum = 0.0;
harmonics[0] = 100.0;
if fundamental_mag < FFT_MIN_FUNDAMENTAL_MAG || fund_bin == 0 {
return (harmonics, 0.0);
}
for order in 2..=crate::types::NUMBER_HARMONICS {
let center_bin = order * fund_bin;
if center_bin < magnitudes.len() {
let max_mag = magnitudes[center_bin];
let ratio = max_mag / fundamental_mag;
harmonics[order - 1] = ratio * 100.0;
thd_sq_sum += ratio * ratio;
}
}
let thd = crate::math::sqrt(thd_sq_sum) * 100.0;
(harmonics, thd)
}
fn compute_magnitudes(
sync_buffer: &mut [f32; FFT_RESOLUTION],
magnitudes: &mut [f32; MAGNITUDES_LEN],
) -> Option<([f32; crate::types::NUMBER_HARMONICS], f32)> {
let spectrum = microfft::real::rfft_512(sync_buffer);
let n = spectrum.len().min(MAGNITUDES_LEN);
let scale_factor = 2.0 / (FFT_RESOLUTION as f32);
magnitudes[0] =
crate::math::sqrt(spectrum[0].re * spectrum[0].re + spectrum[0].im * spectrum[0].im)
/ (FFT_RESOLUTION as f32);
for i in 1..n {
let mag_raw =
crate::math::sqrt(spectrum[i].re * spectrum[i].re + spectrum[i].im * spectrum[i].im);
magnitudes[i] = mag_raw * scale_factor;
}
let expected_bin = CYCLES_PER_WINDOW;
let search_start = expected_bin.saturating_sub(1).max(1);
let search_end = (expected_bin + 1).min(magnitudes.len() - 1);
let (fund_bin, &fundamental_mag) = magnitudes
.iter()
.enumerate()
.skip(search_start)
.take(search_end - search_start + 1)
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(core::cmp::Ordering::Equal))?;
if fundamental_mag < FFT_MIN_FUNDAMENTAL_MAG {
return None;
}
Some(calculate_harmonics_and_thd(
magnitudes,
fund_bin,
fundamental_mag,
))
}
pub struct FftCache {
raw_buffer: Box<[f32; RAW_BUFFER_LEN]>,
raw_write_pos: usize,
raw_filled: bool,
pub sync_buffer: Box<[f32; FFT_RESOLUTION]>,
magnitudes: Box<[f32; MAGNITUDES_LEN]>,
}
impl FftCache {
pub fn new(_fft_len: usize) -> Self {
Self {
raw_buffer: Box::new([0.0; RAW_BUFFER_LEN]),
raw_write_pos: 0,
raw_filled: false,
sync_buffer: Box::new([0.0; FFT_RESOLUTION]),
magnitudes: Box::new([0.0; MAGNITUDES_LEN]),
}
}
pub fn compute_from_sync_buffer(
&mut self,
) -> Option<([f32; crate::types::NUMBER_HARMONICS], f32)> {
remove_mean(self.sync_buffer.as_mut());
compute_magnitudes(self.sync_buffer.as_mut(), self.magnitudes.as_mut())
}
pub fn push_raw_sample(&mut self, sample: f32) {
self.raw_buffer[self.raw_write_pos] = sample;
self.raw_write_pos = (self.raw_write_pos + 1) % RAW_BUFFER_LEN;
if self.raw_write_pos == 0 {
self.raw_filled = true;
}
}
fn extract_last_n(&self, n: usize) -> alloc::vec::Vec<f32> {
let mut out = alloc::vec::Vec::with_capacity(n);
let start = (self.raw_write_pos + RAW_BUFFER_LEN - n) % RAW_BUFFER_LEN;
for k in 0..n {
out.push(self.raw_buffer[(start + k) % RAW_BUFFER_LEN]);
}
out
}
pub fn compute_harmonics_and_thd(
&mut self,
freq: f32,
fs: f32,
) -> Option<([f32; crate::types::NUMBER_HARMONICS], f32)> {
if freq <= 0.0 || fs <= 0.0 {
return None;
}
let raw_span = crate::math::round((CYCLES_PER_WINDOW as f32) * fs / freq) as usize;
let raw_span = raw_span.clamp(2, RAW_BUFFER_LEN);
if !self.raw_filled && self.raw_write_pos < raw_span {
return None; }
let raw_window = self.extract_last_n(raw_span);
let resampled = resample_signal(&raw_window, FFT_RESOLUTION);
self.sync_buffer.copy_from_slice(&resampled);
remove_mean(self.sync_buffer.as_mut());
compute_magnitudes(self.sync_buffer.as_mut(), self.magnitudes.as_mut())
}
}
#[derive(Debug, Clone)]
pub struct InterharmonicAccumulator {
count: usize,
coeffs: [f32; INTERHARMONIC_GROUPS],
q1: [f32; INTERHARMONIC_GROUPS],
q2: [f32; INTERHARMONIC_GROUPS],
}
impl InterharmonicAccumulator {
pub fn new(fs_sync: f32) -> Self {
let mut coeffs = [0.0; INTERHARMONIC_GROUPS];
for (i, c) in coeffs.iter_mut().enumerate() {
let f_center = (i as f32 + 1.5) * crate::FREQ_NOMINAL_50;
*c = 2.0 * libm::cosf(core::f32::consts::TAU * f_center / fs_sync);
}
Self {
count: 0,
coeffs,
q1: [0.0; INTERHARMONIC_GROUPS],
q2: [0.0; INTERHARMONIC_GROUPS],
}
}
pub fn push_cycle(&mut self, sync_data: &[f32; FFT_RESOLUTION]) {
let remaining = (FFT_RESOLUTION * CYCLES_FOR_INTERHARMONIC).saturating_sub(self.count);
let n = remaining.min(FFT_RESOLUTION);
for &sample in sync_data[..n].iter() {
for i in 0..INTERHARMONIC_GROUPS {
let q0 = sample + self.coeffs[i] * self.q1[i] - self.q2[i];
self.q2[i] = self.q1[i];
self.q1[i] = q0;
}
}
self.count += n;
}
pub fn is_ready(&self) -> bool {
self.count >= FFT_RESOLUTION * CYCLES_FOR_INTERHARMONIC
}
pub fn reset(&mut self) {
self.count = 0;
self.q1 = [0.0; INTERHARMONIC_GROUPS];
self.q2 = [0.0; INTERHARMONIC_GROUPS];
}
pub fn compute(&mut self, fundamental_mag: f32) -> Option<[f32; INTERHARMONIC_GROUPS]> {
if !self.is_ready() {
return None;
}
let n = self.count as f32;
let mut result = [0.0; INTERHARMONIC_GROUPS];
for (i, res) in result.iter_mut().enumerate() {
let power = self.q1[i] * self.q1[i] + self.q2[i] * self.q2[i]
- self.coeffs[i] * self.q1[i] * self.q2[i];
let mag = crate::math::sqrt(power / (n * n)) * 2.0;
*res = if fundamental_mag > FFT_MIN_FUNDAMENTAL_MAG {
(mag / fundamental_mag) * 100.0
} else {
0.0
};
}
self.count = 0;
self.q1 = [0.0; INTERHARMONIC_GROUPS];
self.q2 = [0.0; INTERHARMONIC_GROUPS];
Some(result)
}
}
fn remove_mean(signal: &mut [f32]) {
let mean = signal.iter().sum::<f32>() / signal.len() as f32;
for sample in signal.iter_mut() {
*sample -= mean;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn generate_sine(freq: f32, fs: f32, n: usize, amp: f32) -> alloc::vec::Vec<f32> {
(0..n)
.map(|i| {
let t = i as f32 / fs;
crate::math::sin(core::f32::consts::TAU * freq * t) * amp
})
.collect()
}
#[test]
fn test_interharmonic_accumulator_clean_sine() {
let fs_sync = 25600.0;
let mut acc = InterharmonicAccumulator::new(fs_sync);
assert!(!acc.is_ready());
let freq = 50.0;
let amp = 230.0;
let n = FFT_RESOLUTION;
for _ in 0..CYCLES_FOR_INTERHARMONIC {
let samples = generate_sine(freq, fs_sync, n, amp);
let mut buf = [0.0; FFT_RESOLUTION];
buf.copy_from_slice(&samples);
acc.push_cycle(&buf);
}
assert!(acc.is_ready());
let result = acc.compute(amp).unwrap();
for (i, &val) in result.iter().enumerate() {
assert!(val < 0.1, "Interharmonic group {} too high: {}%", i, val);
}
}
#[test]
fn test_interharmonic_accumulator_with_75hz() {
let fs_sync = 25600.0;
let mut acc = InterharmonicAccumulator::new(fs_sync);
let f_fund = 50.0;
let f_inter = 75.0;
let amp = 230.0;
let inter_amp = 2.3;
let total_n = FFT_RESOLUTION * CYCLES_FOR_INTERHARMONIC;
let full_signal: alloc::vec::Vec<f32> = (0..total_n)
.map(|i| {
let t = i as f32 / fs_sync;
crate::math::sin(core::f32::consts::TAU * f_fund * t) * amp
+ crate::math::sin(core::f32::consts::TAU * f_inter * t) * inter_amp
})
.collect();
for c in 0..CYCLES_FOR_INTERHARMONIC {
let mut buf = [0.0; FFT_RESOLUTION];
let offset = c * FFT_RESOLUTION;
buf.copy_from_slice(&full_signal[offset..offset + FFT_RESOLUTION]);
acc.push_cycle(&buf);
}
let result = acc.compute(amp).unwrap();
assert!(
(result[0] - 1.0).abs() < 0.3,
"Group 0 (75 Hz) expected ~1%, got {}%",
result[0]
);
for (i, &val) in result.iter().enumerate().skip(1) {
assert!(val < 0.5, "Interharmonic group {} too high: {}%", i, val);
}
}
}