Skip to main content

use_signal_window/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive windowing helpers.
3//!
4//! The crate intentionally limits itself to a few deterministic windows without
5//! introducing FFT or DSP dependencies.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_signal_window::{WindowKind, apply_window, window_coefficients};
11//!
12//! let coefficients = window_coefficients(WindowKind::Hann, 4).unwrap();
13//! let windowed = apply_window(&[1.0, 1.0, 1.0, 1.0], WindowKind::Hann).unwrap();
14//!
15//! assert_eq!(coefficients, windowed);
16//! ```
17
18use std::f64::consts::PI;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum WindowKind {
22    Rectangular,
23    Hann,
24    Hamming,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum WindowError {
29    InvalidLength,
30    InvalidSample,
31}
32
33pub fn window_coefficients(kind: WindowKind, len: usize) -> Result<Vec<f64>, WindowError> {
34    if len == 0 {
35        return Err(WindowError::InvalidLength);
36    }
37
38    if len == 1 {
39        return Ok(vec![1.0]);
40    }
41
42    let denominator = (len - 1) as f64;
43    let mut coefficients = Vec::with_capacity(len);
44
45    for index in 0..len {
46        let angle = 2.0 * PI * index as f64 / denominator;
47        let coefficient = match kind {
48            WindowKind::Rectangular => 1.0,
49            WindowKind::Hann => 0.5 - 0.5 * angle.cos(),
50            WindowKind::Hamming => 0.54 - 0.46 * angle.cos(),
51        };
52        coefficients.push(coefficient);
53    }
54
55    Ok(coefficients)
56}
57
58pub fn apply_window(samples: &[f64], kind: WindowKind) -> Result<Vec<f64>, WindowError> {
59    if samples.iter().any(|sample| !sample.is_finite()) {
60        return Err(WindowError::InvalidSample);
61    }
62
63    let coefficients = window_coefficients(kind, samples.len())?;
64
65    Ok(samples
66        .iter()
67        .zip(coefficients)
68        .map(|(sample, coefficient)| sample * coefficient)
69        .collect())
70}
71
72#[cfg(test)]
73mod tests {
74    use super::{WindowError, WindowKind, apply_window, window_coefficients};
75
76    #[test]
77    fn computes_window_coefficients_with_expected_lengths() {
78        let coefficients = window_coefficients(WindowKind::Hann, 4).unwrap();
79
80        assert_eq!(coefficients.len(), 4);
81        assert!((coefficients[0] - 0.0).abs() < 1.0e-12);
82        assert!((coefficients[3] - 0.0).abs() < 1.0e-12);
83    }
84
85    #[test]
86    fn handles_single_value_windows_deliberately() {
87        assert_eq!(
88            window_coefficients(WindowKind::Rectangular, 1).unwrap(),
89            vec![1.0]
90        );
91        assert_eq!(window_coefficients(WindowKind::Hann, 1).unwrap(), vec![1.0]);
92        assert_eq!(
93            apply_window(&[2.0], WindowKind::Hamming).unwrap(),
94            vec![2.0]
95        );
96    }
97
98    #[test]
99    fn applies_windows_to_samples() {
100        let windowed = apply_window(&[1.0, 1.0, 1.0, 1.0], WindowKind::Rectangular).unwrap();
101
102        assert_eq!(windowed, vec![1.0, 1.0, 1.0, 1.0]);
103    }
104
105    #[test]
106    fn rejects_empty_windows() {
107        assert_eq!(
108            window_coefficients(WindowKind::Rectangular, 0),
109            Err(WindowError::InvalidLength)
110        );
111        assert_eq!(
112            apply_window(&[], WindowKind::Hann),
113            Err(WindowError::InvalidLength)
114        );
115    }
116
117    #[test]
118    fn rejects_non_finite_samples() {
119        assert_eq!(
120            apply_window(&[1.0, f64::NAN], WindowKind::Hann),
121            Err(WindowError::InvalidSample)
122        );
123    }
124}