kizzasi_io/signal/
processor.rs

1//! Signal processing and analysis
2//!
3//! This module provides the main signal processor for audio and signal analysis,
4//! including FFT operations, filtering, and feature extraction.
5
6use super::filters::Filter;
7use super::functions::bessel_i0;
8use super::spectral::{Spectrogram, WindowType};
9use crate::error::{IoError, IoResult};
10use rustfft::{num_complex::Complex, FftPlanner};
11use scirs2_core::ndarray::Array1;
12use std::f32::consts::PI;
13
14/// Signal processor for pre-processing sensor data
15pub struct SignalProcessor {
16    pub(super) buffer_size: usize,
17    pub(super) sample_rate: f32,
18    pub(super) fft_planner: FftPlanner<f32>,
19}
20
21impl SignalProcessor {
22    /// Create a new signal processor
23    pub fn new(buffer_size: usize) -> Self {
24        Self {
25            buffer_size,
26            sample_rate: 44100.0,
27            fft_planner: FftPlanner::new(),
28        }
29    }
30
31    /// Set the sample rate
32    pub fn with_sample_rate(mut self, rate: f32) -> Self {
33        self.sample_rate = rate;
34        self
35    }
36
37    /// Get the buffer size
38    pub fn buffer_size(&self) -> usize {
39        self.buffer_size
40    }
41
42    /// Get the sample rate
43    pub fn sample_rate(&self) -> f32 {
44        self.sample_rate
45    }
46
47    /// Apply FFT to signal
48    pub fn fft(&mut self, signal: &Array1<f32>) -> IoResult<Vec<Complex<f32>>> {
49        let n = signal.len();
50        let fft = self.fft_planner.plan_fft_forward(n);
51        let mut buffer: Vec<Complex<f32>> = signal.iter().map(|&x| Complex::new(x, 0.0)).collect();
52        fft.process(&mut buffer);
53        Ok(buffer)
54    }
55
56    /// Apply inverse FFT
57    pub fn ifft(&mut self, spectrum: &mut [Complex<f32>]) -> IoResult<Array1<f32>> {
58        let n = spectrum.len();
59        let ifft = self.fft_planner.plan_fft_inverse(n);
60        ifft.process(spectrum);
61        let scale = 1.0 / n as f32;
62        let result: Vec<f32> = spectrum.iter().map(|c| c.re * scale).collect();
63        Ok(Array1::from_vec(result))
64    }
65
66    /// Check if a number is a power of 2
67    fn is_power_of_2(n: usize) -> bool {
68        n != 0 && (n & (n - 1)) == 0
69    }
70
71    /// Optimized FFT for power-of-2 sizes
72    ///
73    /// Uses specialized FFT planning for power-of-2 sizes which can be more efficient.
74    /// Falls back to standard FFT for non-power-of-2 sizes.
75    pub fn fft_pow2(&mut self, signal: &Array1<f32>) -> IoResult<Vec<Complex<f32>>> {
76        let n = signal.len();
77        if Self::is_power_of_2(n) {
78            let fft = self.fft_planner.plan_fft_forward(n);
79            let mut buffer = Vec::with_capacity(n);
80            buffer.extend(signal.iter().map(|&x| Complex::new(x, 0.0)));
81            fft.process(&mut buffer);
82            Ok(buffer)
83        } else {
84            self.fft(signal)
85        }
86    }
87
88    /// Optimized inverse FFT for power-of-2 sizes
89    pub fn ifft_pow2(&mut self, spectrum: &mut [Complex<f32>]) -> IoResult<Array1<f32>> {
90        let n = spectrum.len();
91        if Self::is_power_of_2(n) {
92            let ifft = self.fft_planner.plan_fft_inverse(n);
93            ifft.process(spectrum);
94            let scale = 1.0 / n as f32;
95            let result: Vec<f32> = spectrum.iter().map(|c| c.re * scale).collect();
96            Ok(Array1::from_vec(result))
97        } else {
98            self.ifft(spectrum)
99        }
100    }
101
102    /// Compute power spectrum efficiently for power-of-2 sizes
103    ///
104    /// Returns the magnitude squared of the FFT.
105    pub fn power_spectrum_pow2(&mut self, signal: &Array1<f32>) -> IoResult<Vec<f32>> {
106        let spectrum = self.fft_pow2(signal)?;
107        Ok(spectrum.iter().map(|c| c.norm_sqr()).collect())
108    }
109
110    /// Zero-pad signal to next power of 2 for optimal FFT performance
111    pub fn zero_pad_pow2(signal: &Array1<f32>) -> Array1<f32> {
112        let n = signal.len();
113        if Self::is_power_of_2(n) {
114            return signal.clone();
115        }
116        let next_pow2 = n.next_power_of_two();
117        let mut padded = vec![0.0f32; next_pow2];
118        padded[..n].copy_from_slice(signal.as_slice().unwrap());
119        Array1::from_vec(padded)
120    }
121
122    /// Apply a filter to the signal
123    pub fn apply_filter(&mut self, signal: &Array1<f32>, filter: Filter) -> IoResult<Array1<f32>> {
124        match filter {
125            Filter::MovingAverage { window } => self.moving_average(signal, window),
126            Filter::LowPass { cutoff, .. } => self.lowpass_fft(signal, cutoff),
127            Filter::HighPass { cutoff, .. } => self.highpass_fft(signal, cutoff),
128            Filter::BandPass { low, high, .. } => self.bandpass_fft(signal, low, high),
129            Filter::Iir(mut iir) => Ok(iir.process(signal)),
130            Filter::Fir(mut fir) => Ok(fir.process(signal)),
131        }
132    }
133
134    /// Compute power spectrum (magnitude squared)
135    pub fn power_spectrum(&mut self, signal: &Array1<f32>) -> IoResult<Array1<f32>> {
136        let spectrum = self.fft(signal)?;
137        let power: Vec<f32> = spectrum.iter().map(|c| c.norm_sqr()).collect();
138        Ok(Array1::from_vec(power))
139    }
140
141    /// Compute magnitude spectrum
142    pub fn magnitude_spectrum(&mut self, signal: &Array1<f32>) -> IoResult<Array1<f32>> {
143        let spectrum = self.fft(signal)?;
144        let magnitude: Vec<f32> = spectrum.iter().map(|c| c.norm()).collect();
145        Ok(Array1::from_vec(magnitude))
146    }
147
148    /// Compute phase spectrum
149    pub fn phase_spectrum(&mut self, signal: &Array1<f32>) -> IoResult<Array1<f32>> {
150        let spectrum = self.fft(signal)?;
151        let phase: Vec<f32> = spectrum.iter().map(|c| c.arg()).collect();
152        Ok(Array1::from_vec(phase))
153    }
154
155    /// Compute zero-crossings count
156    pub fn zero_crossings(signal: &Array1<f32>) -> usize {
157        signal
158            .windows(2)
159            .into_iter()
160            .filter(|w| w[0].signum() != w[1].signum())
161            .count()
162    }
163
164    /// Compute peak-to-peak amplitude
165    pub fn peak_to_peak(signal: &Array1<f32>) -> f32 {
166        let max = signal.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
167        let min = signal.iter().cloned().fold(f32::INFINITY, f32::min);
168        max - min
169    }
170
171    /// Apply DC removal (subtract mean)
172    pub fn remove_dc(signal: &Array1<f32>) -> Array1<f32> {
173        let mean = signal.mean().unwrap_or(0.0);
174        signal.mapv(|x| x - mean)
175    }
176
177    /// Apply envelope detection via Hilbert transform approximation
178    pub fn envelope(&mut self, signal: &Array1<f32>) -> IoResult<Array1<f32>> {
179        let n = signal.len();
180        let mut spectrum = self.fft(signal)?;
181        spectrum[0] = Complex::new(0.0, 0.0);
182        for item in spectrum.iter_mut().take(n / 2).skip(1) {
183            *item *= Complex::new(2.0, 0.0);
184        }
185        for item in spectrum.iter_mut().skip(n / 2 + 1) {
186            *item = Complex::new(0.0, 0.0);
187        }
188        let ifft = self.fft_planner.plan_fft_inverse(n);
189        ifft.process(&mut spectrum);
190        let scale = 1.0 / n as f32;
191        let envelope: Vec<f32> = spectrum.iter().map(|c| c.norm() * scale).collect();
192        Ok(Array1::from_vec(envelope))
193    }
194
195    /// Simple moving average filter
196    fn moving_average(&self, signal: &Array1<f32>, window: usize) -> IoResult<Array1<f32>> {
197        if window == 0 || window > signal.len() {
198            return Err(IoError::SignalError("Invalid window size".into()));
199        }
200        let mut result = Array1::zeros(signal.len());
201        let mut sum: f32 = signal.iter().take(window).sum();
202        for i in 0..signal.len() {
203            if i >= window {
204                sum -= signal[i - window];
205                sum += signal[i];
206            }
207            result[i] = sum / window.min(i + 1) as f32;
208        }
209        Ok(result)
210    }
211
212    /// Low-pass filter using FFT
213    fn lowpass_fft(&mut self, signal: &Array1<f32>, cutoff: f32) -> IoResult<Array1<f32>> {
214        let mut spectrum = self.fft(signal)?;
215        let n = spectrum.len();
216        let cutoff_bin = (cutoff / self.sample_rate * n as f32) as usize;
217        for item in spectrum.iter_mut().take(n - cutoff_bin).skip(cutoff_bin) {
218            *item = Complex::new(0.0, 0.0);
219        }
220        self.ifft(&mut spectrum)
221    }
222
223    /// High-pass filter using FFT
224    fn highpass_fft(&mut self, signal: &Array1<f32>, cutoff: f32) -> IoResult<Array1<f32>> {
225        let mut spectrum = self.fft(signal)?;
226        let n = spectrum.len();
227        let cutoff_bin = (cutoff / self.sample_rate * n as f32) as usize;
228        for i in 0..cutoff_bin.min(n / 2) {
229            spectrum[i] = Complex::new(0.0, 0.0);
230            if n - 1 - i < n {
231                spectrum[n - 1 - i] = Complex::new(0.0, 0.0);
232            }
233        }
234        self.ifft(&mut spectrum)
235    }
236
237    /// Band-pass filter using FFT
238    fn bandpass_fft(&mut self, signal: &Array1<f32>, low: f32, high: f32) -> IoResult<Array1<f32>> {
239        let mut spectrum = self.fft(signal)?;
240        let n = spectrum.len();
241        let low_bin = (low / self.sample_rate * n as f32) as usize;
242        let high_bin = (high / self.sample_rate * n as f32) as usize;
243        for (i, item) in spectrum.iter_mut().enumerate() {
244            let freq_bin = if i <= n / 2 { i } else { n - i };
245            if freq_bin < low_bin || freq_bin > high_bin {
246                *item = Complex::new(0.0, 0.0);
247            }
248        }
249        self.ifft(&mut spectrum)
250    }
251
252    /// Normalize signal to [-1, 1] range
253    pub fn normalize(signal: &Array1<f32>) -> Array1<f32> {
254        let max_abs = signal.iter().map(|x| x.abs()).fold(0.0f32, f32::max);
255        if max_abs > 0.0 {
256            signal.mapv(|x| x / max_abs)
257        } else {
258            signal.clone()
259        }
260    }
261
262    /// Compute RMS (Root Mean Square) of signal
263    pub fn rms(signal: &Array1<f32>) -> f32 {
264        let sum_sq: f32 = signal.iter().map(|x| x * x).sum();
265        (sum_sq / signal.len() as f32).sqrt()
266    }
267
268    /// SIMD-optimized RMS computation
269    ///
270    /// Uses chunked processing for better cache locality and potential SIMD vectorization.
271    #[cfg(feature = "simd")]
272    pub fn rms_simd(signal: &Array1<f32>) -> f32 {
273        Self::rms_simd_impl(signal.as_slice().unwrap())
274    }
275
276    /// SIMD-optimized RMS implementation for slices
277    #[cfg(feature = "simd")]
278    fn rms_simd_impl(data: &[f32]) -> f32 {
279        const CHUNK_SIZE: usize = 8;
280        let chunks = data.chunks_exact(CHUNK_SIZE);
281        let remainder = chunks.remainder();
282        let mut sum_sq = chunks.fold(0.0f32, |acc, chunk| {
283            let chunk_sum: f32 = chunk.iter().map(|x| x * x).sum();
284            acc + chunk_sum
285        });
286        sum_sq += remainder.iter().map(|x| x * x).sum::<f32>();
287        (sum_sq / data.len() as f32).sqrt()
288    }
289
290    /// SIMD-optimized normalization
291    ///
292    /// Normalizes signal to [-1, 1] range using chunked processing.
293    #[cfg(feature = "simd")]
294    pub fn normalize_simd(signal: &Array1<f32>) -> Array1<f32> {
295        let max_abs = Self::max_abs_simd(signal.as_slice().unwrap());
296        if max_abs > 0.0 {
297            signal.mapv(|x| x / max_abs)
298        } else {
299            signal.clone()
300        }
301    }
302
303    /// SIMD-optimized max absolute value
304    #[cfg(feature = "simd")]
305    fn max_abs_simd(data: &[f32]) -> f32 {
306        const CHUNK_SIZE: usize = 8;
307        let chunks = data.chunks_exact(CHUNK_SIZE);
308        let remainder = chunks.remainder();
309        let mut max_val = 0.0f32;
310        for chunk in chunks {
311            for &val in chunk {
312                max_val = max_val.max(val.abs());
313            }
314        }
315        for &val in remainder {
316            max_val = max_val.max(val.abs());
317        }
318        max_val
319    }
320
321    /// SIMD-optimized vector addition
322    ///
323    /// Adds two signals element-wise using chunked processing.
324    #[cfg(feature = "simd")]
325    pub fn add_simd(a: &Array1<f32>, b: &Array1<f32>) -> IoResult<Array1<f32>> {
326        if a.len() != b.len() {
327            return Err(IoError::SignalError("Signals must have same length".into()));
328        }
329        let a_slice = a.as_slice().unwrap();
330        let b_slice = b.as_slice().unwrap();
331        let mut result = vec![0.0f32; a.len()];
332        const CHUNK_SIZE: usize = 8;
333        let chunks_a = a_slice.chunks_exact(CHUNK_SIZE);
334        let chunks_b = b_slice.chunks_exact(CHUNK_SIZE);
335        let result_chunks = result.chunks_exact_mut(CHUNK_SIZE);
336        for ((chunk_a, chunk_b), result_chunk) in chunks_a.zip(chunks_b).zip(result_chunks) {
337            for i in 0..CHUNK_SIZE {
338                result_chunk[i] = chunk_a[i] + chunk_b[i];
339            }
340        }
341        let remainder_start = (a.len() / CHUNK_SIZE) * CHUNK_SIZE;
342        for i in remainder_start..a.len() {
343            result[i] = a_slice[i] + b_slice[i];
344        }
345        Ok(Array1::from_vec(result))
346    }
347
348    /// SIMD-optimized vector multiplication
349    ///
350    /// Multiplies two signals element-wise using chunked processing.
351    #[cfg(feature = "simd")]
352    pub fn multiply_simd(a: &Array1<f32>, b: &Array1<f32>) -> IoResult<Array1<f32>> {
353        if a.len() != b.len() {
354            return Err(IoError::SignalError("Signals must have same length".into()));
355        }
356        let a_slice = a.as_slice().unwrap();
357        let b_slice = b.as_slice().unwrap();
358        let mut result = vec![0.0f32; a.len()];
359        const CHUNK_SIZE: usize = 8;
360        let chunks_a = a_slice.chunks_exact(CHUNK_SIZE);
361        let chunks_b = b_slice.chunks_exact(CHUNK_SIZE);
362        let result_chunks = result.chunks_exact_mut(CHUNK_SIZE);
363        for ((chunk_a, chunk_b), result_chunk) in chunks_a.zip(chunks_b).zip(result_chunks) {
364            for i in 0..CHUNK_SIZE {
365                result_chunk[i] = chunk_a[i] * chunk_b[i];
366            }
367        }
368        let remainder_start = (a.len() / CHUNK_SIZE) * CHUNK_SIZE;
369        for i in remainder_start..a.len() {
370            result[i] = a_slice[i] * b_slice[i];
371        }
372        Ok(Array1::from_vec(result))
373    }
374
375    /// Compute spectrogram (Short-Time Fourier Transform)
376    ///
377    /// Returns a 2D array where rows are time frames and columns are frequency bins.
378    /// Only the positive frequency bins (0 to n_fft/2) are returned.
379    pub fn spectrogram(
380        &mut self,
381        signal: &Array1<f32>,
382        n_fft: usize,
383        hop_length: usize,
384        window: WindowType,
385    ) -> IoResult<Spectrogram> {
386        if n_fft == 0 || hop_length == 0 {
387            return Err(IoError::SignalError(
388                "n_fft and hop_length must be > 0".into(),
389            ));
390        }
391        if signal.len() < n_fft {
392            return Err(IoError::SignalError("Signal shorter than n_fft".into()));
393        }
394        let window_coeffs = Self::create_window(window, n_fft);
395        let num_frames = (signal.len() - n_fft) / hop_length + 1;
396        let num_bins = n_fft / 2 + 1;
397        let mut magnitudes = Vec::with_capacity(num_frames * num_bins);
398        let mut phases = Vec::with_capacity(num_frames * num_bins);
399        for frame_idx in 0..num_frames {
400            let start = frame_idx * hop_length;
401            let frame: Vec<f32> = signal
402                .iter()
403                .skip(start)
404                .take(n_fft)
405                .zip(window_coeffs.iter())
406                .map(|(&s, &w)| s * w)
407                .collect();
408            let spectrum = self.fft(&Array1::from_vec(frame))?;
409            for bin in spectrum.iter().take(num_bins) {
410                magnitudes.push(bin.norm());
411                phases.push(bin.arg());
412            }
413        }
414        Ok(Spectrogram {
415            magnitudes,
416            phases,
417            num_frames,
418            num_bins,
419            hop_length,
420            sample_rate: self.sample_rate,
421        })
422    }
423
424    /// Create a window function
425    pub fn create_window(window_type: WindowType, size: usize) -> Vec<f32> {
426        match window_type {
427            WindowType::Rectangular => vec![1.0; size],
428            WindowType::Hann => (0..size)
429                .map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (size - 1) as f32).cos()))
430                .collect(),
431            WindowType::Hamming => (0..size)
432                .map(|i| 0.54 - 0.46 * (2.0 * PI * i as f32 / (size - 1) as f32).cos())
433                .collect(),
434            WindowType::Blackman => (0..size)
435                .map(|i| {
436                    let n = i as f32 / (size - 1) as f32;
437                    0.42 - 0.5 * (2.0 * PI * n).cos() + 0.08 * (4.0 * PI * n).cos()
438                })
439                .collect(),
440            WindowType::Bartlett => (0..size)
441                .map(|i| {
442                    let half = (size - 1) as f32 / 2.0;
443                    1.0 - ((i as f32 - half) / half).abs()
444                })
445                .collect(),
446            WindowType::Kaiser { beta } => {
447                let i0_beta = bessel_i0(beta);
448                (0..size)
449                    .map(|i| {
450                        let n = 2.0 * i as f32 / (size - 1) as f32 - 1.0;
451                        bessel_i0(beta * (1.0 - n * n).sqrt()) / i0_beta
452                    })
453                    .collect()
454            }
455        }
456    }
457
458    /// Apply window function to a frame (convenience method)
459    pub fn apply_window_to_frame(frame: &[f32], window_type: WindowType) -> Vec<f32> {
460        let window = Self::create_window(window_type, frame.len());
461        frame
462            .iter()
463            .zip(window.iter())
464            .map(|(&s, &w)| s * w)
465            .collect()
466    }
467
468    /// Compute Mel filterbank
469    pub fn mel_filterbank(
470        num_filters: usize,
471        n_fft: usize,
472        sample_rate: f32,
473        f_min: f32,
474        f_max: f32,
475    ) -> Vec<Vec<f32>> {
476        let num_bins = n_fft / 2 + 1;
477        let mel_min = Self::hz_to_mel(f_min);
478        let mel_max = Self::hz_to_mel(f_max);
479        let mel_points: Vec<f32> = (0..=num_filters + 1)
480            .map(|i| mel_min + (mel_max - mel_min) * i as f32 / (num_filters + 1) as f32)
481            .collect();
482        let hz_points: Vec<f32> = mel_points.iter().map(|&m| Self::mel_to_hz(m)).collect();
483        let bin_points: Vec<usize> = hz_points
484            .iter()
485            .map(|&f| ((n_fft as f32 + 1.0) * f / sample_rate).floor() as usize)
486            .collect();
487        let mut filterbank = Vec::with_capacity(num_filters);
488        for m in 0..num_filters {
489            let mut filter = vec![0.0; num_bins];
490            let left = bin_points[m];
491            let center = bin_points[m + 1];
492            let right = bin_points[m + 2];
493            let rise_denom = (center - left).max(1) as f32;
494            for (offset, val) in filter[left..center.min(num_bins)].iter_mut().enumerate() {
495                *val = offset as f32 / rise_denom;
496            }
497            let fall_denom = (right - center).max(1) as f32;
498            for (offset, val) in filter[center..right.min(num_bins)].iter_mut().enumerate() {
499                *val = (right - center - offset) as f32 / fall_denom;
500            }
501            filterbank.push(filter);
502        }
503        filterbank
504    }
505
506    /// Convert frequency in Hz to Mel scale
507    pub fn hz_to_mel(hz: f32) -> f32 {
508        2595.0 * (1.0 + hz / 700.0).log10()
509    }
510
511    /// Convert Mel scale to frequency in Hz
512    pub fn mel_to_hz(mel: f32) -> f32 {
513        700.0 * (10.0_f32.powf(mel / 2595.0) - 1.0)
514    }
515
516    /// Compute Mel-frequency cepstral coefficients (MFCCs)
517    pub fn mfcc(
518        &mut self,
519        signal: &Array1<f32>,
520        n_mfcc: usize,
521        n_fft: usize,
522        hop_length: usize,
523        n_mels: usize,
524    ) -> IoResult<Vec<Vec<f32>>> {
525        let f_max = self.sample_rate / 2.0;
526        let filterbank = Self::mel_filterbank(n_mels, n_fft, self.sample_rate, 0.0, f_max);
527        let spec = self.spectrogram(signal, n_fft, hop_length, WindowType::Hann)?;
528        let mut mfccs = Vec::with_capacity(spec.num_frames);
529        for frame_idx in 0..spec.num_frames {
530            let power: Vec<f32> = (0..spec.num_bins)
531                .map(|bin| {
532                    let mag = spec.magnitudes[frame_idx * spec.num_bins + bin];
533                    mag * mag
534                })
535                .collect();
536            let mel_energies: Vec<f32> = filterbank
537                .iter()
538                .map(|filter| {
539                    let energy: f32 = filter.iter().zip(power.iter()).map(|(&f, &p)| f * p).sum();
540                    (energy + 1e-10).ln()
541                })
542                .collect();
543            let mfcc_frame = Self::dct(&mel_energies, n_mfcc);
544            mfccs.push(mfcc_frame);
545        }
546        Ok(mfccs)
547    }
548
549    /// Discrete Cosine Transform (Type-II)
550    fn dct(input: &[f32], n_coeffs: usize) -> Vec<f32> {
551        let n = input.len();
552        (0..n_coeffs)
553            .map(|k| {
554                let sum: f32 = input
555                    .iter()
556                    .enumerate()
557                    .map(|(i, &x)| {
558                        x * (PI * k as f32 * (2.0 * i as f32 + 1.0) / (2.0 * n as f32)).cos()
559                    })
560                    .sum();
561                sum * (2.0 / n as f32).sqrt()
562            })
563            .collect()
564    }
565}