zk-audio 0.1.0

Audio processing library for voice recording and enhancement
Documentation
use crate::core::{AudioFrame, AudioProfile, CaptureDiagnostics};
use crate::processors::{analyze_frame, classify_frame, FrameClass};

#[derive(Debug)]
pub struct RuntimeStats {
    profile: AudioProfile,
    noise_floor: f32,
    analyzed_frames: u64,
    noise_only_frames: u64,
    transitional_frames: u64,
    speech_like_frames: u64,
}

impl RuntimeStats {
    pub fn new(profile: AudioProfile) -> Self {
        Self {
            profile,
            noise_floor: 0.01,
            analyzed_frames: 0,
            noise_only_frames: 0,
            transitional_frames: 0,
            speech_like_frames: 0,
        }
    }

    pub fn observe(&mut self, frame: &AudioFrame) {
        if frame.samples.is_empty() {
            return;
        }

        let features = analyze_frame(&frame.samples, self.noise_floor);
        let frame_class = classify_frame(self.profile, features, self.noise_floor);
        self.analyzed_frames += 1;

        match frame_class {
            FrameClass::NoiseOnly => {
                self.noise_only_frames += 1;
                self.noise_floor = self.noise_floor * 0.92 + features.rms.max(0.0005) * 0.08;
            }
            FrameClass::Transitional => {
                self.transitional_frames += 1;
                self.noise_floor = self.noise_floor * 0.992 + features.rms.max(0.0005) * 0.008;
            }
            FrameClass::SpeechLike => {
                self.speech_like_frames += 1;
                self.noise_floor = self.noise_floor * 0.997 + features.rms.max(0.0005) * 0.003;
            }
        }
    }

    pub fn apply(&self, diagnostics: &mut CaptureDiagnostics) {
        diagnostics.analyzed_frames = self.analyzed_frames;
        diagnostics.noise_only_frames = self.noise_only_frames;
        diagnostics.transitional_frames = self.transitional_frames;
        diagnostics.speech_like_frames = self.speech_like_frames;
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::core::{AudioProfile, AudioSpec};

    fn frame(samples: Vec<f32>) -> AudioFrame {
        AudioFrame {
            samples,
            spec: AudioSpec {
                sample_rate: 44_100,
                channels: 1,
            },
        }
    }

    #[test]
    fn runtime_stats_classify_frames_into_buckets() {
        let mut stats = RuntimeStats::new(AudioProfile::VoiceHvac);
        stats.observe(&frame(vec![
            0.02, -0.02, 0.02, -0.02, 0.02, -0.02, 0.02, -0.02,
        ]));
        stats.observe(&frame(vec![
            0.0, 0.18, 0.29, 0.12, -0.04, -0.16, -0.22, -0.09,
        ]));

        let mut diagnostics = CaptureDiagnostics::default();
        stats.apply(&mut diagnostics);

        assert_eq!(diagnostics.analyzed_frames, 2);
        assert_eq!(diagnostics.noise_only_frames, 1);
        assert_eq!(diagnostics.speech_like_frames, 1);
    }
}