scirs2_fft/
window.rs

1//! Window functions for signal processing
2//!
3//! This module provides various window functions commonly used in signal processing
4//! to reduce spectral leakage when performing Fourier transforms on finite data.
5//!
6//! Window functions are used to taper the samples at the beginning and end of the
7//! data to reduce the spectral leakage effect in FFT processing.
8
9use crate::error::{FFTError, FFTResult};
10use scirs2_core::ndarray::{Array1, ArrayBase, Data, Ix1};
11use scirs2_core::numeric::{Float, FromPrimitive};
12use std::f64::consts::PI;
13use std::fmt::Debug;
14use std::str::FromStr;
15
16/// Window function types
17#[derive(Debug, Clone, PartialEq)]
18pub enum Window {
19    /// Rectangular window (no windowing)
20    Rectangular,
21    /// Hann window (raised cosine)
22    Hann,
23    /// Hamming window (raised cosine with offset)
24    Hamming,
25    /// Blackman window
26    Blackman,
27    /// Bartlett window (triangular)
28    Bartlett,
29    /// Flat top window
30    FlatTop,
31    /// Parzen window
32    Parzen,
33    /// Bohman window
34    Bohman,
35    /// Blackman-Harris window
36    BlackmanHarris,
37    /// Nuttall window
38    Nuttall,
39    /// Barthann window
40    Barthann,
41    /// Cosine window
42    Cosine,
43    /// Exponential window
44    Exponential,
45    /// Tukey window (tapered cosine)
46    Tukey(f64),
47    /// Kaiser window
48    Kaiser(f64),
49    /// Gaussian window
50    Gaussian(f64),
51    /// General cosine window with custom coefficients
52    GeneralCosine(Vec<f64>),
53}
54
55impl FromStr for Window {
56    type Err = FFTError;
57
58    fn from_str(s: &str) -> Result<Self, Self::Err> {
59        match s.to_lowercase().as_str() {
60            "rectangular" | "boxcar" | "rect" => Ok(Window::Rectangular),
61            "hann" | "hanning" => Ok(Window::Hann),
62            "hamming" => Ok(Window::Hamming),
63            "blackman" => Ok(Window::Blackman),
64            "bartlett" | "triangular" | "triangle" => Ok(Window::Bartlett),
65            "flattop" | "flat" => Ok(Window::FlatTop),
66            "parzen" => Ok(Window::Parzen),
67            "bohman" => Ok(Window::Bohman),
68            "blackmanharris" | "blackman-harris" => Ok(Window::BlackmanHarris),
69            "nuttall" => Ok(Window::Nuttall),
70            "barthann" => Ok(Window::Barthann),
71            "cosine" | "cos" => Ok(Window::Cosine),
72            "exponential" | "exp" => Ok(Window::Exponential),
73            _ => Err(FFTError::ValueError(format!("Unknown window type: {s}"))),
74        }
75    }
76}
77
78/// Get a window of the specified type and length.
79///
80/// # Arguments
81///
82/// * `window` - Window type or name
83/// * `n` - Window length
84/// * `sym` - Whether the window is symmetric (default: true)
85///
86/// # Returns
87///
88/// A vector containing the window values.
89///
90/// # Examples
91///
92/// ```
93/// use scirs2_fft::window::{Window, get_window};
94///
95/// // Create a Hann window
96/// let win = get_window(Window::Hann, 10, true).unwrap();
97/// assert_eq!(win.len(), 10);
98///
99/// // Create a Kaiser window with beta=8.6
100/// let win = get_window(Window::Kaiser(8.6), 10, true).unwrap();
101/// assert_eq!(win.len(), 10);
102///
103/// // Create a window by name
104/// let win = get_window("hamming", 10, true).unwrap();
105/// assert_eq!(win.len(), 10);
106/// ```
107#[allow(dead_code)]
108pub fn get_window<T>(window: T, n: usize, sym: bool) -> FFTResult<Array1<f64>>
109where
110    T: Into<WindowParam>,
111{
112    if n == 0 {
113        return Err(FFTError::ValueError(
114            "Window length must be positive".to_string(),
115        ));
116    }
117
118    let window_param = window.into();
119    let window_type = match window_param {
120        WindowParam::Type(wt) => wt,
121        WindowParam::Name(s) => Window::from_str(&s)?,
122    };
123
124    match window_type {
125        Window::Rectangular => rectangular(n),
126        Window::Hann => hann(n, sym),
127        Window::Hamming => hamming(n, sym),
128        Window::Blackman => blackman(n, sym),
129        Window::Bartlett => bartlett(n, sym),
130        Window::FlatTop => flattop(n, sym),
131        Window::Parzen => parzen(n, sym),
132        Window::Bohman => bohman(n),
133        Window::BlackmanHarris => blackmanharris(n, sym),
134        Window::Nuttall => nuttall(n, sym),
135        Window::Barthann => barthann(n, sym),
136        Window::Cosine => cosine(n, sym),
137        Window::Exponential => exponential(n, sym, 1.0),
138        Window::Tukey(alpha) => tukey(n, sym, alpha),
139        Window::Kaiser(beta) => kaiser(n, sym, beta),
140        Window::Gaussian(std) => gaussian(n, sym, std),
141        Window::GeneralCosine(coeffs) => general_cosine(n, sym, &coeffs),
142    }
143}
144
145/// Helper enum to handle different window parameter types
146#[derive(Debug)]
147pub enum WindowParam {
148    /// Window type enum
149    Type(Window),
150    /// Window name as string
151    Name(String),
152}
153
154impl From<Window> for WindowParam {
155    fn from(window: Window) -> Self {
156        WindowParam::Type(window)
157    }
158}
159
160impl From<&str> for WindowParam {
161    fn from(s: &str) -> Self {
162        WindowParam::Name(s.to_string())
163    }
164}
165
166impl From<String> for WindowParam {
167    fn from(s: String) -> Self {
168        WindowParam::Name(s)
169    }
170}
171
172/// Rectangular window
173///
174/// A rectangular window is a window with constant value 1.
175#[allow(dead_code)]
176fn rectangular(n: usize) -> FFTResult<Array1<f64>> {
177    Ok(Array1::ones(n))
178}
179
180/// Hann window
181///
182/// The Hann window is a taper formed by using a raised cosine with ends that
183/// touch zero.
184#[allow(dead_code)]
185fn hann(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
186    general_cosine(n, sym, &[0.5, 0.5])
187}
188
189/// Hamming window
190///
191/// The Hamming window is a taper formed by using a raised cosine with non-zero
192/// endpoints.
193#[allow(dead_code)]
194fn hamming(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
195    general_cosine(n, sym, &[0.54, 0.46])
196}
197
198/// Blackman window
199///
200/// The Blackman window is a taper formed by using the first three terms of
201/// a summation of cosines.
202#[allow(dead_code)]
203fn blackman(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
204    general_cosine(n, sym, &[0.42, 0.5, 0.08])
205}
206
207/// Bartlett window
208///
209/// The Bartlett window is a triangular window that touches zero at both ends.
210#[allow(dead_code)]
211fn bartlett(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
212    if n == 1 {
213        return Ok(Array1::ones(1));
214    }
215
216    let mut n = n;
217    if !sym {
218        n += 1;
219    }
220
221    let mut w = Array1::zeros(n);
222    let range: Vec<f64> = (0..n).map(|i| i as f64).collect();
223
224    for (i, &x) in range.iter().enumerate() {
225        if x < (n as f64) / 2.0 {
226            w[i] = 2.0 * x / (n as f64 - 1.0);
227        } else {
228            w[i] = 2.0 - 2.0 * x / (n as f64 - 1.0);
229        }
230    }
231
232    if !sym {
233        // Remove last element for asymmetric case
234        let w_slice = w.slice(scirs2_core::ndarray::s![0..n - 1]).to_owned();
235        Ok(w_slice)
236    } else {
237        Ok(w)
238    }
239}
240
241/// Flat top window
242///
243/// The flat top window is a window with the main lobe widened and flattened
244/// at the expense of higher sidelobes compared to other windows.
245#[allow(dead_code)]
246fn flattop(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
247    general_cosine(
248        n,
249        sym,
250        &[
251            0.215_578_95,
252            0.416_631_58,
253            0.277_263_158,
254            0.083_578_947,
255            0.006_947_368,
256        ],
257    )
258}
259
260/// Parzen window
261///
262/// The Parzen window is a piecewise cubic approximation of the Gaussian window.
263#[allow(dead_code)]
264fn parzen(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
265    if n == 1 {
266        return Ok(Array1::ones(1));
267    }
268
269    let mut n = n;
270    if !sym {
271        n += 1;
272    }
273
274    let mut w = Array1::zeros(n);
275    let half_n = (n as f64) / 2.0;
276
277    for i in 0..n {
278        let x = (i as f64 - half_n + 0.5).abs() / half_n;
279
280        if x <= 0.5 {
281            w[i] = 1.0 - 6.0 * x.powi(2) * (1.0 - x);
282        } else if x <= 1.0 {
283            w[i] = 2.0 * (1.0 - x).powi(3);
284        }
285    }
286
287    if !sym {
288        // Remove last element for asymmetric case
289        let w_slice = w.slice(scirs2_core::ndarray::s![0..n - 1]).to_owned();
290        Ok(w_slice)
291    } else {
292        Ok(w)
293    }
294}
295
296/// Bohman window
297///
298/// The Bohman window is the convolution of two half-duration cosine lobes.
299#[allow(dead_code)]
300fn bohman(n: usize) -> FFTResult<Array1<f64>> {
301    if n == 1 {
302        return Ok(Array1::ones(1));
303    }
304
305    let mut w = Array1::zeros(n);
306    let half_n = (n as f64 - 1.0) / 2.0;
307
308    for i in 0..n {
309        let x = ((i as f64) - half_n).abs() / half_n;
310        if x <= 1.0 {
311            w[i] = (1.0 - x) * (PI * x).cos() + (PI * x).sin() / PI;
312        }
313    }
314
315    Ok(w)
316}
317
318/// Blackman-Harris window
319///
320/// The Blackman-Harris window is a variation of the Blackman window with
321/// better sidelobe suppression.
322#[allow(dead_code)]
323fn blackmanharris(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
324    general_cosine(n, sym, &[0.35875, 0.48829, 0.14128, 0.01168])
325}
326
327/// Nuttall window
328///
329/// The Nuttall window is a Blackman-Harris window with slightly different
330/// coefficients for improved sidelobe behavior.
331#[allow(dead_code)]
332fn nuttall(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
333    general_cosine(
334        n,
335        sym,
336        &[0.363_581_9, 0.489_177_5, 0.136_599_5, 0.010_641_1],
337    )
338}
339
340/// Barthann window
341///
342/// The Barthann window is the convolution of the Bartlett and Hann windows.
343#[allow(dead_code)]
344fn barthann(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
345    if n == 1 {
346        return Ok(Array1::ones(1));
347    }
348
349    let mut n = n;
350    if !sym {
351        n += 1;
352    }
353
354    let mut w = Array1::zeros(n);
355    let fac = 1.0 / (n as f64 - 1.0);
356
357    for i in 0..n {
358        let x = i as f64 * fac;
359        w[i] = 0.62 - 0.48 * (2.0 * x - 1.0).abs() + 0.38 * (2.0 * PI * (2.0 * x - 1.0)).cos();
360    }
361
362    if !sym {
363        // Remove last element for asymmetric case
364        let w_slice = w.slice(scirs2_core::ndarray::s![0..n - 1]).to_owned();
365        Ok(w_slice)
366    } else {
367        Ok(w)
368    }
369}
370
371/// Cosine window
372///
373/// Simple cosine (half-cycle) window.
374#[allow(dead_code)]
375fn cosine(n: usize, sym: bool) -> FFTResult<Array1<f64>> {
376    if n == 1 {
377        return Ok(Array1::ones(1));
378    }
379
380    let mut w = Array1::zeros(n);
381
382    let range: Vec<f64> = if sym {
383        (0..n).map(|i| i as f64 / (n as f64 - 1.0)).collect()
384    } else {
385        (0..n).map(|i| i as f64 / n as f64).collect()
386    };
387
388    for (i, &x) in range.iter().enumerate() {
389        w[i] = (PI * x).sin();
390    }
391
392    Ok(w)
393}
394
395/// Exponential window
396///
397/// The exponential window has the form: w(n) = exp(-|n - center| / tau)
398///
399/// # Arguments
400///
401/// * `n` - Window length
402/// * `sym` - Whether the window is symmetric
403/// * `tau` - Decay constant (tau > 0)
404#[allow(dead_code)]
405fn exponential(n: usize, sym: bool, tau: f64) -> FFTResult<Array1<f64>> {
406    if tau <= 0.0 {
407        return Err(FFTError::ValueError("tau must be positive".to_string()));
408    }
409
410    if n == 1 {
411        return Ok(Array1::ones(1));
412    }
413
414    let center = if sym { (n as f64 - 1.0) / 2.0 } else { 0.0 };
415
416    let mut w = Array1::zeros(n);
417
418    for i in 0..n {
419        let x = (i as f64 - center).abs() / (tau * (n as f64));
420        w[i] = (-x).exp();
421    }
422
423    Ok(w)
424}
425
426/// Tukey window
427///
428/// The Tukey window is a cosine lobe of width alpha * N/2 with tails that are
429/// rectangular windows.
430///
431/// # Arguments
432///
433/// * `n` - Window length
434/// * `sym` - Whether the window is symmetric
435/// * `alpha` - Shape parameter of the Tukey window, representing the ratio of cosine-tapered
436///   section length to the total window length (0 <= alpha <= 1)
437#[allow(dead_code)]
438fn tukey(n: usize, sym: bool, alpha: f64) -> FFTResult<Array1<f64>> {
439    if !(0.0..=1.0).contains(&alpha) {
440        return Err(FFTError::ValueError(
441            "alpha must be between 0 and 1".to_string(),
442        ));
443    }
444
445    if n == 1 {
446        return Ok(Array1::ones(1));
447    }
448
449    if alpha == 0.0 {
450        return rectangular(n);
451    }
452
453    if alpha == 1.0 {
454        return hann(n, sym);
455    }
456
457    let mut w = Array1::ones(n);
458    let width = (alpha * (n as f64 - 1.0) / 2.0).floor() as usize;
459
460    // Left taper
461    for i in 0..width {
462        let x = 0.5 * (1.0 + ((PI * i as f64) / width as f64).cos());
463        w[i] = x;
464    }
465
466    // Right taper
467    for i in 0..width {
468        let idx = n - 1 - i;
469        let x = 0.5 * (1.0 + ((PI * i as f64) / width as f64).cos());
470        w[idx] = x;
471    }
472
473    Ok(w)
474}
475
476/// Kaiser window
477///
478/// The Kaiser window is a taper formed by using a Bessel function of the first kind.
479///
480/// # Arguments
481///
482/// * `n` - Window length
483/// * `sym` - Whether the window is symmetric
484/// * `beta` - Shape parameter for Kaiser window
485#[allow(dead_code)]
486fn kaiser(n: usize, sym: bool, beta: f64) -> FFTResult<Array1<f64>> {
487    if n == 1 {
488        return Ok(Array1::ones(1));
489    }
490
491    if beta < 0.0 {
492        return Err(FFTError::ValueError(
493            "beta must be non-negative".to_string(),
494        ));
495    }
496
497    let mut n = n;
498    if !sym {
499        n += 1;
500    }
501
502    let mut w = Array1::zeros(n);
503    let alpha = 0.5 * (n as f64 - 1.0);
504    let i0_beta = bessel_i0(beta);
505
506    for i in 0..n {
507        let x = beta * (1.0 - ((i as f64 - alpha) / alpha).powi(2)).sqrt();
508        w[i] = bessel_i0(x) / i0_beta;
509    }
510
511    if !sym {
512        // Remove last element for asymmetric case
513        let w_slice = w.slice(scirs2_core::ndarray::s![0..n - 1]).to_owned();
514        Ok(w_slice)
515    } else {
516        Ok(w)
517    }
518}
519
520/// Gaussian window
521///
522/// The Gaussian window is defined as a Gaussian function with specified standard deviation.
523///
524/// # Arguments
525///
526/// * `n` - Window length
527/// * `sym` - Whether the window is symmetric
528/// * `std` - Standard deviation of the Gaussian window
529#[allow(dead_code)]
530fn gaussian(n: usize, sym: bool, std: f64) -> FFTResult<Array1<f64>> {
531    if n == 1 {
532        return Ok(Array1::ones(1));
533    }
534
535    if std <= 0.0 {
536        return Err(FFTError::ValueError("std must be positive".to_string()));
537    }
538
539    let mut w = Array1::zeros(n);
540    let center = if sym { (n as f64 - 1.0) / 2.0 } else { 0.0 };
541
542    for i in 0..n {
543        let x = (i as f64 - center) / (std * (n as f64 - 1.0) / 2.0);
544        w[i] = (-0.5 * x.powi(2)).exp();
545    }
546
547    Ok(w)
548}
549
550/// General cosine window
551///
552/// General cosine window with custom coefficients.
553///
554/// # Arguments
555///
556/// * `n` - Window length
557/// * `sym` - Whether the window is symmetric
558/// * `a` - Window coefficients in the form: w(n) = a₀ - a₁*cos(2πn/N) + a₂*cos(4πn/N) - ...
559#[allow(dead_code)]
560fn general_cosine(n: usize, sym: bool, a: &[f64]) -> FFTResult<Array1<f64>> {
561    if n == 1 {
562        return Ok(Array1::ones(1));
563    }
564
565    let mut w = Array1::zeros(n);
566
567    // Calculate window values
568    let fac = if sym {
569        2.0 * PI / (n as f64 - 1.0)
570    } else {
571        2.0 * PI / n as f64
572    };
573
574    for i in 0..n {
575        let mut win_val = a[0];
576
577        for (k, &coef) in a.iter().enumerate().skip(1) {
578            let sign = if k % 2 == 1 { -1.0 } else { 1.0 };
579            win_val += sign * coef * ((k as f64) * fac * (i as f64)).cos();
580        }
581
582        w[i] = win_val;
583    }
584
585    Ok(w)
586}
587
588/// Modified Bessel function of the first kind, order 0
589///
590/// Approximation of the modified Bessel function I₀(x) using polynomial
591/// approximation for numerical stability.
592#[allow(dead_code)]
593fn bessel_i0(x: f64) -> f64 {
594    let ax = x.abs();
595
596    // For small arguments, use polynomial approximation
597    if ax < 3.75 {
598        let y = (x / 3.75).powi(2);
599        return y.mul_add(
600            3.515_622_9
601                + y * (3.089_942_4
602                    + y * (1.206_749_2 + y * (0.265_973_2 + y * (0.036_076_8 + y * 0.004_581_3)))),
603            1.0,
604        );
605    }
606
607    // For large arguments, use asymptotic expansion
608    let y = 3.75 / ax;
609    let exp_term = (ax).exp() / (ax).sqrt();
610
611    exp_term
612        * y.mul_add(
613            0.013_285_92
614                + y * (0.002_253_19
615                    + y * (-0.001_575_65
616                        + y * (0.009_162_81
617                            + y * (-0.020_577_06
618                                + y * (0.026_355_37 + y * (-0.016_476_33 + y * 0.003_923_77)))))),
619            0.398_942_28,
620        )
621}
622
623/// Apply window function to a signal
624///
625/// # Arguments
626///
627/// * `x` - Input signal
628/// * `window` - Window function to apply
629///
630/// # Returns
631///
632/// The windowed signal.
633///
634/// # Examples
635///
636/// ```
637/// use scirs2_fft::window::{Window, apply_window};
638/// use scirs2_core::ndarray::Array1;
639///
640/// let signal = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
641/// let window = Window::Hann;
642/// let windowed = apply_window(&signal, window).unwrap();
643/// ```
644/// # Errors
645///
646/// Returns an error if the window calculation fails or if the value cannot be
647/// converted to the target floating point type.
648///
649/// # Panics
650///
651/// Panics if the conversion from f64 to type F fails.
652#[allow(dead_code)]
653pub fn apply_window<F, S>(x: &ArrayBase<S, Ix1>, window: Window) -> FFTResult<Array1<F>>
654where
655    S: Data<Elem = F>,
656    F: Float + FromPrimitive + Debug,
657{
658    let n = x.len();
659    let win = get_window(window, n, true)?;
660
661    let mut result = Array1::zeros(n);
662    for i in 0..n {
663        result[i] = x[i] * F::from_f64(win[i]).unwrap();
664    }
665
666    Ok(result)
667}
668
669/// Compute the equivalent noise bandwidth of a window
670///
671/// # Arguments
672///
673/// * `window` - Window function
674/// * `n` - Window length
675///
676/// # Returns
677///
678/// The equivalent noise bandwidth of the window.
679///
680/// # Examples
681///
682/// ```
683/// use scirs2_fft::window::{Window, enbw};
684/// use approx::assert_relative_eq;
685///
686/// let bandwidth = enbw(Window::Hann, 1024).unwrap();
687/// assert_relative_eq!(bandwidth, 1.5, epsilon = 0.01);
688/// ```
689/// # Errors
690///
691/// Returns an error if the window calculation fails.
692#[allow(dead_code)]
693pub fn enbw(window: Window, n: usize) -> FFTResult<f64> {
694    let w = get_window(window, n, true)?;
695
696    let sum_squared = w.iter().map(|&x| x.powi(2)).sum::<f64>();
697    let square_sum = w.iter().sum::<f64>().powi(2);
698
699    // Safe conversion as n is unlikely to exceed f64 precision in practice
700    let n_f64 = n as f64;
701    Ok(n_f64 * sum_squared / square_sum)
702}
703
704#[cfg(test)]
705mod tests {
706    use super::*;
707    use approx::assert_relative_eq;
708
709    #[test]
710    fn test_rectangular() {
711        let win = rectangular(5).unwrap();
712        let expected = [1.0, 1.0, 1.0, 1.0, 1.0];
713        for (a, &b) in win.iter().zip(expected.iter()) {
714            assert_relative_eq!(a, &b, epsilon = 1e-10);
715        }
716    }
717
718    #[test]
719    fn test_hann() {
720        let win = hann(5, true).unwrap();
721        let expected = [0.0, 0.5, 1.0, 0.5, 0.0];
722        for (a, &b) in win.iter().zip(expected.iter()) {
723            assert_relative_eq!(a, &b, epsilon = 1e-10);
724        }
725    }
726
727    #[test]
728    fn test_hamming() {
729        let win = hamming(5, true).unwrap();
730        let expected = [0.08, 0.54, 1.0, 0.54, 0.08];
731        for (a, &b) in win.iter().zip(expected.iter()) {
732            assert_relative_eq!(a, &b, epsilon = 1e-10);
733        }
734    }
735
736    #[test]
737    fn test_blackman() {
738        let win = blackman(5, true).unwrap();
739        let expected = [0.0, 0.34, 1.0, 0.34, 0.0];
740        for (a, &b) in win.iter().zip(expected.iter()) {
741            assert_relative_eq!(a, &b, epsilon = 0.01);
742        }
743    }
744
745    #[test]
746    fn test_from_str() {
747        assert_eq!(Window::from_str("hann").unwrap(), Window::Hann);
748        assert_eq!(Window::from_str("hamming").unwrap(), Window::Hamming);
749        assert_eq!(Window::from_str("blackman").unwrap(), Window::Blackman);
750        assert_eq!(
751            Window::from_str("rectangular").unwrap(),
752            Window::Rectangular
753        );
754        assert!(Window::from_str("invalid").is_err());
755    }
756
757    #[test]
758    fn test_get_window() {
759        let win1 = get_window(Window::Hann, 5, true).unwrap();
760        let win2 = get_window("hann", 5, true).unwrap();
761
762        for (a, b) in win1.iter().zip(win2.iter()) {
763            assert_relative_eq!(a, b, epsilon = 1e-10);
764        }
765    }
766
767    #[test]
768    fn test_apply_window() {
769        let signal = Array1::from_vec(vec![1.0, 2.0, 3.0, 4.0, 5.0]);
770        let win = apply_window(&signal.view(), Window::Hann).unwrap();
771
772        let expected = Array1::from_vec(vec![0.0, 1.0, 3.0, 2.0, 0.0]);
773        for (a, b) in win.iter().zip(expected.iter()) {
774            assert_relative_eq!(a, b, epsilon = 1e-10);
775        }
776    }
777
778    #[test]
779    fn test_enbw() {
780        // Known ENBW values for common windows
781        let rect_enbw = enbw(Window::Rectangular, 1024).unwrap();
782        assert_relative_eq!(rect_enbw, 1.0, epsilon = 1e-10);
783
784        let hann_enbw = enbw(Window::Hann, 1024).unwrap();
785        assert_relative_eq!(hann_enbw, 1.5, epsilon = 0.01);
786
787        let hamming_enbw = enbw(Window::Hamming, 1024).unwrap();
788        assert_relative_eq!(hamming_enbw, 1.36, epsilon = 0.01);
789    }
790}