Skip to main content

use_signal_normalize/
lib.rs

1#![forbid(unsafe_code)]
2//! Primitive signal-normalization helpers.
3//!
4//! The crate provides small, explicit helpers for peak normalization, mapping a
5//! signal into a target range, and removing DC offset.
6//!
7//! # Examples
8//!
9//! ```rust
10//! use use_signal_normalize::{center_signal, normalize_peak, normalize_range};
11//!
12//! let normalized = normalize_peak(&[-2.0, 0.0, 1.0]).unwrap();
13//! let centered = center_signal(&[1.0, 2.0, 3.0]).unwrap();
14//! let ranged = normalize_range(&[0.0, 1.0], -1.0, 1.0).unwrap();
15//!
16//! assert_eq!(normalized, vec![-1.0, 0.0, 0.5]);
17//! assert_eq!(centered, vec![-1.0, 0.0, 1.0]);
18//! assert_eq!(ranged, vec![-1.0, 1.0]);
19//! ```
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum NormalizeError {
23    EmptySamples,
24    InvalidSample,
25    InvalidRange,
26}
27
28fn validated_samples(samples: &[f64]) -> Result<&[f64], NormalizeError> {
29    if samples.is_empty() {
30        return Err(NormalizeError::EmptySamples);
31    }
32
33    if samples.iter().any(|sample| !sample.is_finite()) {
34        return Err(NormalizeError::InvalidSample);
35    }
36
37    Ok(samples)
38}
39
40fn sample_min_max(samples: &[f64]) -> (f64, f64) {
41    let mut values = samples.iter().copied();
42    let first = values.next().unwrap();
43
44    values.fold((first, first), |(min, max), sample| {
45        (min.min(sample), max.max(sample))
46    })
47}
48
49pub fn normalize_peak(samples: &[f64]) -> Option<Vec<f64>> {
50    let samples = validated_samples(samples).ok()?;
51    let peak = samples
52        .iter()
53        .map(|sample| sample.abs())
54        .fold(0.0, f64::max);
55
56    if peak == 0.0 {
57        return Some(samples.to_vec());
58    }
59
60    Some(samples.iter().map(|sample| sample / peak).collect())
61}
62
63pub fn normalize_range(samples: &[f64], min: f64, max: f64) -> Result<Vec<f64>, NormalizeError> {
64    let samples = validated_samples(samples)?;
65
66    if !min.is_finite() || !max.is_finite() || min >= max {
67        return Err(NormalizeError::InvalidRange);
68    }
69
70    let (source_min, source_max) = sample_min_max(samples);
71    let source_span = source_max - source_min;
72
73    if source_span == 0.0 {
74        let midpoint = (min + max) / 2.0;
75        return Ok(vec![midpoint; samples.len()]);
76    }
77
78    let target_span = max - min;
79
80    Ok(samples
81        .iter()
82        .map(|sample| min + ((sample - source_min) / source_span) * target_span)
83        .collect())
84}
85
86pub fn center_signal(samples: &[f64]) -> Option<Vec<f64>> {
87    let samples = validated_samples(samples).ok()?;
88    let mean = samples.iter().sum::<f64>() / samples.len() as f64;
89
90    Some(samples.iter().map(|sample| sample - mean).collect())
91}
92
93pub fn remove_dc_offset(samples: &[f64]) -> Option<Vec<f64>> {
94    center_signal(samples)
95}
96
97#[cfg(test)]
98mod tests {
99    use super::{NormalizeError, center_signal, normalize_peak, normalize_range, remove_dc_offset};
100
101    #[test]
102    fn normalizes_peak_values() {
103        assert_eq!(
104            normalize_peak(&[-2.0, 0.0, 1.0]),
105            Some(vec![-1.0, 0.0, 0.5])
106        );
107        assert_eq!(normalize_peak(&[0.0, 0.0]), Some(vec![0.0, 0.0]));
108    }
109
110    #[test]
111    fn normalizes_into_target_ranges() {
112        assert_eq!(
113            normalize_range(&[0.0, 1.0], -1.0, 1.0).unwrap(),
114            vec![-1.0, 1.0]
115        );
116        assert_eq!(
117            normalize_range(&[5.0, 5.0], -1.0, 1.0).unwrap(),
118            vec![0.0, 0.0]
119        );
120    }
121
122    #[test]
123    fn centers_signals_and_removes_dc_offset() {
124        assert_eq!(center_signal(&[1.0, 2.0, 3.0]), Some(vec![-1.0, 0.0, 1.0]));
125        assert_eq!(
126            remove_dc_offset(&[1.0, 2.0, 3.0]),
127            Some(vec![-1.0, 0.0, 1.0])
128        );
129    }
130
131    #[test]
132    fn rejects_empty_and_invalid_samples() {
133        assert_eq!(normalize_peak(&[]), None);
134        assert_eq!(center_signal(&[1.0, f64::NAN]), None);
135        assert_eq!(
136            normalize_range(&[], -1.0, 1.0),
137            Err(NormalizeError::EmptySamples)
138        );
139        assert_eq!(
140            normalize_range(&[1.0, f64::INFINITY], -1.0, 1.0),
141            Err(NormalizeError::InvalidSample)
142        );
143    }
144
145    #[test]
146    fn rejects_invalid_target_ranges() {
147        assert_eq!(
148            normalize_range(&[0.0, 1.0], 1.0, 1.0),
149            Err(NormalizeError::InvalidRange)
150        );
151        assert_eq!(
152            normalize_range(&[0.0, 1.0], f64::NAN, 1.0),
153            Err(NormalizeError::InvalidRange)
154        );
155    }
156}