Skip to main content

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