loqa_voice_dsp/
config.rs

1//! Configuration for voice analysis
2//!
3//! This module provides configuration structures for the `VoiceAnalyzer`.
4
5/// Pitch detection algorithm selection
6#[derive(Debug, Clone, Copy, PartialEq)]
7pub enum PitchAlgorithm {
8    /// Automatic selection: tries pYIN -> YIN -> Autocorr (recommended)
9    Auto,
10    /// Probabilistic YIN only (best for noisy/breathy voice)
11    PYIN,
12    /// Classic YIN only (fast, accurate for clean signals)
13    YIN,
14    /// Autocorrelation only (fallback for very noisy signals)
15    Autocorr,
16}
17
18/// Configuration for voice analysis
19#[derive(Debug, Clone, PartialEq)]
20pub struct AnalysisConfig {
21    /// Sample rate in Hz
22    pub sample_rate: u32,
23    /// Frame size in samples
24    pub frame_size: usize,
25    /// Hop size in samples (for streaming analysis)
26    pub hop_size: usize,
27    /// Minimum expected frequency in Hz
28    pub min_frequency: f32,
29    /// Maximum expected frequency in Hz
30    pub max_frequency: f32,
31    /// Pitch detection algorithm to use
32    pub algorithm: PitchAlgorithm,
33    /// YIN threshold (0.0-1.0, lower = stricter)
34    pub threshold: f32,
35    /// Minimum confidence for voiced detection (0.0-1.0)
36    pub min_confidence: f32,
37    /// Enable parabolic interpolation for sub-sample accuracy
38    pub interpolate: bool,
39}
40
41impl Default for AnalysisConfig {
42    fn default() -> Self {
43        Self {
44            sample_rate: 16000,
45            frame_size: 2048,
46            hop_size: 1024,
47            min_frequency: 80.0,
48            max_frequency: 400.0,
49            algorithm: PitchAlgorithm::Auto,
50            threshold: 0.15,
51            min_confidence: 0.5,
52            interpolate: true,
53        }
54    }
55}
56
57impl AnalysisConfig {
58    /// Create a new configuration with default values
59    pub fn new() -> Self {
60        Self::default()
61    }
62
63    /// Set the sample rate in Hz
64    pub fn with_sample_rate(mut self, sample_rate: u32) -> Self {
65        self.sample_rate = sample_rate;
66        self
67    }
68
69    /// Set the frame size in samples
70    pub fn with_frame_size(mut self, frame_size: usize) -> Self {
71        self.frame_size = frame_size;
72        self
73    }
74
75    /// Set the hop size in samples
76    pub fn with_hop_size(mut self, hop_size: usize) -> Self {
77        self.hop_size = hop_size;
78        self
79    }
80
81    /// Set the minimum frequency in Hz
82    pub fn with_min_frequency(mut self, min_frequency: f32) -> Self {
83        self.min_frequency = min_frequency;
84        self
85    }
86
87    /// Set the maximum frequency in Hz
88    pub fn with_max_frequency(mut self, max_frequency: f32) -> Self {
89        self.max_frequency = max_frequency;
90        self
91    }
92
93    /// Set the pitch detection algorithm
94    pub fn with_algorithm(mut self, algorithm: PitchAlgorithm) -> Self {
95        self.algorithm = algorithm;
96        self
97    }
98
99    /// Set the YIN threshold (0.0-1.0)
100    pub fn with_threshold(mut self, threshold: f32) -> Self {
101        self.threshold = threshold;
102        self
103    }
104
105    /// Set the minimum confidence for voiced detection (0.0-1.0)
106    pub fn with_min_confidence(mut self, min_confidence: f32) -> Self {
107        self.min_confidence = min_confidence;
108        self
109    }
110
111    /// Enable or disable parabolic interpolation
112    pub fn with_interpolate(mut self, interpolate: bool) -> Self {
113        self.interpolate = interpolate;
114        self
115    }
116
117    /// Validate the configuration
118    ///
119    /// # Errors
120    ///
121    /// Returns an error if:
122    /// - `min_frequency` >= `max_frequency`
123    /// - `frame_size` < 512
124    /// - `hop_size` > `frame_size`
125    /// - `sample_rate` == 0
126    /// - `threshold` or `min_confidence` not in [0.0, 1.0]
127    pub fn validate(&self) -> Result<(), String> {
128        if self.sample_rate == 0 {
129            return Err("sample_rate must be greater than 0".into());
130        }
131
132        if self.min_frequency >= self.max_frequency {
133            return Err(format!(
134                "min_frequency ({}) must be less than max_frequency ({})",
135                self.min_frequency, self.max_frequency
136            ));
137        }
138
139        if self.min_frequency <= 0.0 {
140            return Err("min_frequency must be greater than 0".into());
141        }
142
143        if self.frame_size < 512 {
144            return Err("frame_size must be at least 512 samples".into());
145        }
146
147        if self.hop_size == 0 {
148            return Err("hop_size must be greater than 0".into());
149        }
150
151        if self.hop_size > self.frame_size {
152            return Err(format!(
153                "hop_size ({}) cannot exceed frame_size ({})",
154                self.hop_size, self.frame_size
155            ));
156        }
157
158        if !(0.0..=1.0).contains(&self.threshold) {
159            return Err(format!(
160                "threshold ({}) must be in range [0.0, 1.0]",
161                self.threshold
162            ));
163        }
164
165        if !(0.0..=1.0).contains(&self.min_confidence) {
166            return Err(format!(
167                "min_confidence ({}) must be in range [0.0, 1.0]",
168                self.min_confidence
169            ));
170        }
171
172        Ok(())
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn test_default_config() {
182        let config = AnalysisConfig::default();
183        assert_eq!(config.sample_rate, 16000);
184        assert_eq!(config.frame_size, 2048);
185        assert_eq!(config.hop_size, 1024);
186        assert_eq!(config.min_frequency, 80.0);
187        assert_eq!(config.max_frequency, 400.0);
188        assert_eq!(config.algorithm, PitchAlgorithm::Auto);
189        assert_eq!(config.threshold, 0.15);
190        assert_eq!(config.min_confidence, 0.5);
191        assert!(config.interpolate);
192    }
193
194    #[test]
195    fn test_builder_pattern() {
196        let config = AnalysisConfig::default()
197            .with_sample_rate(48000)
198            .with_frame_size(4096)
199            .with_algorithm(PitchAlgorithm::PYIN);
200
201        assert_eq!(config.sample_rate, 48000);
202        assert_eq!(config.frame_size, 4096);
203        assert_eq!(config.algorithm, PitchAlgorithm::PYIN);
204    }
205
206    #[test]
207    fn test_validation_valid() {
208        let config = AnalysisConfig::default();
209        assert!(config.validate().is_ok());
210    }
211
212    #[test]
213    fn test_validation_min_max_frequency() {
214        let config = AnalysisConfig::default()
215            .with_min_frequency(400.0)
216            .with_max_frequency(80.0);
217        assert!(config.validate().is_err());
218
219        let config = AnalysisConfig::default()
220            .with_min_frequency(200.0)
221            .with_max_frequency(200.0);
222        assert!(config.validate().is_err());
223    }
224
225    #[test]
226    fn test_validation_frame_size() {
227        let config = AnalysisConfig::default().with_frame_size(256);
228        assert!(config.validate().is_err());
229    }
230
231    #[test]
232    fn test_validation_hop_size() {
233        let config = AnalysisConfig::default()
234            .with_frame_size(1024)
235            .with_hop_size(2048);
236        assert!(config.validate().is_err());
237
238        let config = AnalysisConfig::default().with_hop_size(0);
239        assert!(config.validate().is_err());
240    }
241
242    #[test]
243    fn test_validation_threshold() {
244        let config = AnalysisConfig::default().with_threshold(-0.1);
245        assert!(config.validate().is_err());
246
247        let config = AnalysisConfig::default().with_threshold(1.5);
248        assert!(config.validate().is_err());
249    }
250
251    #[test]
252    fn test_validation_min_confidence() {
253        let config = AnalysisConfig::default().with_min_confidence(-0.1);
254        assert!(config.validate().is_err());
255
256        let config = AnalysisConfig::default().with_min_confidence(1.5);
257        assert!(config.validate().is_err());
258    }
259
260    #[test]
261    fn test_pitch_algorithm_enum() {
262        let auto = PitchAlgorithm::Auto;
263        let pyin = PitchAlgorithm::PYIN;
264        let yin = PitchAlgorithm::YIN;
265        let autocorr = PitchAlgorithm::Autocorr;
266
267        assert_eq!(auto, PitchAlgorithm::Auto);
268        assert_ne!(auto, pyin);
269        assert_ne!(pyin, yin);
270        assert_ne!(yin, autocorr);
271    }
272}