| |
| |
| |
| |
| |
| |
|
|
| #![cfg_attr(not(feature = "std"), no_std)] |
|
|
| use crate::config::MarineConfig; |
| use crate::ema::Ema; |
| use crate::packet::{SalienceMarker, SaliencePacket}; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub struct MarineProcessor { |
| |
| cfg: MarineConfig, |
|
|
| |
| prev2: f32, |
| |
| prev1: f32, |
| |
| idx: u64, |
|
|
| |
| last_peak_idx: u64, |
| |
| last_peak_amp: f32, |
|
|
| |
| ema_period: Ema, |
| |
| ema_amp: Ema, |
|
|
| |
| peak_count: u64, |
| } |
|
|
| impl MarineProcessor { |
| |
| pub fn new(cfg: MarineConfig) -> Self { |
| Self { |
| cfg, |
| prev2: 0.0, |
| prev1: 0.0, |
| idx: 0, |
| last_peak_idx: 0, |
| last_peak_amp: 0.0, |
| ema_period: Ema::new(cfg.ema_period_alpha), |
| ema_amp: Ema::new(cfg.ema_amp_alpha), |
| peak_count: 0, |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| pub fn process_sample(&mut self, sample: f32) -> Option<SalienceMarker> { |
| let i = self.idx; |
| self.idx += 1; |
|
|
| |
| if sample.abs() < self.cfg.clip_threshold { |
| self.prev2 = self.prev1; |
| self.prev1 = sample; |
| return None; |
| } |
|
|
| |
| |
| let is_peak = i >= 2 |
| && self.prev1.abs() >= self.cfg.clip_threshold |
| && self.prev1.abs() > self.prev2.abs() |
| && self.prev1.abs() > sample.abs(); |
|
|
| let mut result = None; |
|
|
| if is_peak { |
| let peak_idx = i - 1; |
| let amp = self.prev1.abs(); |
| let energy = amp * amp; |
|
|
| |
| let period = if self.last_peak_idx == 0 { |
| 0.0 |
| } else { |
| (peak_idx - self.last_peak_idx) as f32 |
| }; |
|
|
| |
| if period > self.cfg.min_period as f32 && period < self.cfg.max_period as f32 { |
| if self.ema_period.is_ready() { |
| |
| let jp = (period - self.ema_period.get()).abs() / self.ema_period.get(); |
| let ja = (amp - self.ema_amp.get()).abs() / self.ema_amp.get(); |
|
|
| |
| |
| |
| let h = 1.0; |
|
|
| |
| |
| let s = 1.0 / (1.0 + jp + ja); |
|
|
| result = Some(SalienceMarker::Peak(SaliencePacket::new( |
| jp, ja, h, s, energy, peak_idx, |
| ))); |
| } |
|
|
| |
| self.ema_period.update(period); |
| self.ema_amp.update(amp); |
| } |
|
|
| self.last_peak_idx = peak_idx; |
| self.last_peak_amp = amp; |
| self.peak_count += 1; |
| } |
|
|
| |
| self.prev2 = self.prev1; |
| self.prev1 = sample; |
|
|
| result |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| #[cfg(feature = "std")] |
| pub fn process_buffer(&mut self, samples: &[f32]) -> Vec<SaliencePacket> { |
| let mut packets = Vec::new(); |
|
|
| for &sample in samples { |
| if let Some(SalienceMarker::Peak(packet)) = self.process_sample(sample) { |
| packets.push(packet); |
| } |
| } |
|
|
| packets |
| } |
|
|
| |
| pub fn reset(&mut self) { |
| self.prev2 = 0.0; |
| self.prev1 = 0.0; |
| self.idx = 0; |
| self.last_peak_idx = 0; |
| self.last_peak_amp = 0.0; |
| self.ema_period.reset(); |
| self.ema_amp.reset(); |
| self.peak_count = 0; |
| } |
|
|
| |
| pub fn peak_count(&self) -> u64 { |
| self.peak_count |
| } |
|
|
| |
| pub fn current_index(&self) -> u64 { |
| self.idx |
| } |
|
|
| |
| pub fn is_warmed_up(&self) -> bool { |
| self.peak_count >= 3 && self.ema_period.is_ready() |
| } |
|
|
| |
| pub fn expected_period(&self) -> Option<f32> { |
| if self.ema_period.is_ready() { |
| Some(self.ema_period.get()) |
| } else { |
| None |
| } |
| } |
|
|
| |
| pub fn expected_amplitude(&self) -> Option<f32> { |
| if self.ema_amp.is_ready() { |
| Some(self.ema_amp.get()) |
| } else { |
| None |
| } |
| } |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| #[test] |
| fn test_peak_detection() { |
| let config = MarineConfig::speech_default(22050); |
| let mut processor = MarineProcessor::new(config); |
|
|
| |
| |
| let mut samples = vec![0.0; 100]; |
| for i in (10..100).step_by(10) { |
| samples[i] = 0.5; |
| if i > 0 { |
| samples[i - 1] = 0.3; |
| } |
| if i < 99 { |
| samples[i + 1] = 0.3; |
| } |
| } |
|
|
| let mut peak_count = 0; |
| for sample in &samples { |
| if let Some(SalienceMarker::Peak(_)) = processor.process_sample(*sample) { |
| peak_count += 1; |
| } |
| } |
|
|
| |
| assert!(peak_count > 0); |
| } |
|
|
| #[test] |
| fn test_jitter_calculation() { |
| let mut config = MarineConfig::speech_default(22050); |
| config.min_period = 5; |
| config.max_period = 20; |
| let mut processor = MarineProcessor::new(config); |
|
|
| |
| let mut detected_packets = vec![]; |
| for cycle in 0..10 { |
| for i in 0..10 { |
| let sample = if i == 5 { |
| 0.8 |
| } else if i == 4 || i == 6 { |
| 0.5 |
| } else { |
| 0.01 |
| }; |
|
|
| if let Some(SalienceMarker::Peak(packet)) = processor.process_sample(sample) { |
| detected_packets.push(packet); |
| } |
| } |
| } |
|
|
| |
| if detected_packets.len() > 3 { |
| let last = detected_packets.last().unwrap(); |
| |
| assert!(last.j_p < 0.5, "Period jitter too high: {}", last.j_p); |
| } |
| } |
|
|
| #[test] |
| fn test_reset() { |
| let config = MarineConfig::speech_default(22050); |
| let mut processor = MarineProcessor::new(config); |
|
|
| |
| for _ in 0..100 { |
| processor.process_sample(0.5); |
| } |
| assert!(processor.current_index() > 0); |
|
|
| |
| processor.reset(); |
| assert_eq!(processor.current_index(), 0); |
| assert_eq!(processor.peak_count(), 0); |
| assert!(!processor.is_warmed_up()); |
| } |
|
|
| #[cfg(feature = "std")] |
| #[test] |
| fn test_process_buffer() { |
| let mut config = MarineConfig::speech_default(22050); |
| config.min_period = 5; |
| config.max_period = 50; |
| let mut processor = MarineProcessor::new(config); |
|
|
| |
| let mut samples = Vec::new(); |
| for _ in 0..20 { |
| samples.extend_from_slice(&[0.01, 0.3, 0.8, 0.3, 0.01]); |
| } |
|
|
| let packets = processor.process_buffer(&samples); |
| |
| assert!(packets.len() > 0); |
| } |
| } |
|
|