stt-cli 0.2.1

Speech to text Cli using Groq API and OpenAI API
// src/audio/buffer.rs
//
// This module implements the audio buffer that stores and manages audio samples.
// It handles the recording state and provides methods to add, retrieve, and manage samples.

use std::{
    sync::{atomic::Ordering, Arc, Mutex},
    time::Duration,
};
use tracing::error;

use crate::audio_state::RecordingState;

/// Constants for audio processing
pub const SAMPLE_RATE: u32 = 16000;
pub const BUFFER_SIZE: u32 = 1024;
pub const CHANNELS: u16 = 1;
pub const CHUNK_DURATION_MS: u64 = 5000;
pub const CHUNK_SIZE: usize =
    (SAMPLE_RATE as usize * CHANNELS as usize) * (CHUNK_DURATION_MS as usize) / 1000;

/// AudioBuffer stores audio samples and manages the recording state and chunking.
///
/// It provides thread-safe access to the buffer through Arc<Mutex<>>.
/// The buffer only stores samples when recording is active.
#[derive(Debug)]
pub struct AudioBuffer {
    /// The actual audio samples
    samples: Vec<f32>,
    /// Current recording state (active/inactive)
    recording_state: RecordingState,
    /// Number of samples needed for a complete chunk
    required_samples: usize,
}

impl AudioBuffer {
    /// Create a new AudioBuffer with the given recording state and sample rate
    pub fn new(
        recording_state: RecordingState,
        sample_rate: u32,
        min_chunk_duration: Duration,
    ) -> Self {
        let required_samples = (sample_rate as f32 * min_chunk_duration.as_secs_f32()) as usize;
        Self {
            samples: Vec::with_capacity(required_samples * 2),
            recording_state,
            required_samples,
        }
    }

    // pub fn is_active(&self) -> bool {
    //     self.recording_state.is_active()
    // }

    // /// Add samples to the buffer if recording is active
    // pub fn append_samples(&mut self, samples: &[f32]) {
    //     if !self.is_active() {
    //         return;
    //     }
    //     self.samples.extend_from_slice(samples);
    // }

    // /// Get a complete chunk if available
    // pub fn get_complete_chunk(&mut self) -> Option<Vec<f32>> {
    //     if self.samples.len() >= self.required_samples {
    //         Some(self.samples.drain(0..self.required_samples).collect())
    //     } else {
    //         None
    //     }
    // }

    // /// Take any remaining samples that didn't fill a complete chunk
    // pub fn take_remaining(&mut self) -> Vec<f32> {
    //     self.samples.drain(..).collect()
    // }

    // /// Check if buffer contains enough samples for at least one complete chunk
    // pub fn has_complete_chunk(&self) -> bool {
    //     self.samples.len() >= self.required_samples
    // }

    // /// Get current buffered duration in seconds
    // pub fn current_duration(&self) -> f32 {
    //     self.samples.len() as f32 / SAMPLE_RATE as f32
    // }

    // /// Stop recording and don't add more samples
    // pub fn stop_recording(&mut self) {
    //     self.recording_state.set_active(false);
    // }

    // /// Start recording and clear existing samples
    // pub fn start_recording(&mut self) {
    //     self.recording_state.set_active(true);
    //     self.samples.clear();
    // }

    // /// Clear all samples from the buffer
    // pub fn clear(&mut self) {
    //     self.samples.clear();
    // }

    // /// Get a reference to the current samples
    // pub fn samples(&self) -> &[f32] {
    //     &self.samples
    // }

    // /// Get the current recording state
    // pub fn recording_state(&self) -> &RecordingState {
    //     &self.recording_state
    // }
}

// pub fn create_shared_buffer(
//     recording_state: RecordingState,
//     sample_rate: u32,
//     min_chunk_duration: Duration
// ) -> Arc<Mutex<AudioBuffer>> {
//     Arc::new(Mutex::new(AudioBuffer::new(recording_state, sample_rate, min_chunk_duration)))
// }

// pub fn try_lock_buffer(
//     buffer: &Arc<Mutex<AudioBuffer>>,
// ) -> Option<std::sync::MutexGuard<'_, AudioBuffer>> {
//     match buffer.lock() {
//         Ok(guard) => Some(guard),
//         Err(poisoned) => {
//             error!("Audio buffer mutex poisoned! Recovering.");
//             Some(poisoned.into_inner())
//         }
//     }
// }

/// Resamples audio to the target sample rate using rubato crate
pub fn resample_audio(input: &[f32], source_rate: u32, target_rate: u32) -> Vec<f32> {
    use rubato::{Resampler, SincFixedIn};

    let mut resampler = SincFixedIn::<f32>::new(
        target_rate as f64 / source_rate as f64,
        2.0,
        rubato::SincInterpolationParameters {
            sinc_len: 256,
            f_cutoff: 0.95,
            oversampling_factor: 128,
            window: rubato::WindowFunction::BlackmanHarris2,
            interpolation: rubato::SincInterpolationType::Linear,
        },
        input.len(),
        1,
    )
    .expect("Failed to create resampler");

    let output = resampler
        .process(&[input.to_vec()], None)
        .expect("Resampling failed");
    output.into_iter().flatten().collect()
}

/// Converts multi-channel audio data to mono by averaging samples across channels.
///
/// * `audio`: A slice containing interleaved audio samples (e.g., [L, R, L, R, ...]).
/// * `channels`: The number of channels in the input audio.
///
/// Returns a `Vec<f32>` containing the mono audio samples.
pub fn audio_to_mono(audio: &[f32], channels: u16) -> Vec<f32> {
    if channels == 0 {
        return Vec::new(); // Avoid division by zero
    }
    if channels == 1 {
        return audio.to_vec(); // Already mono
    }

    let frame_count = audio.len() / channels as usize;
    let mut mono_samples = Vec::with_capacity(frame_count);

    for frame_index in 0..frame_count {
        let start = frame_index * channels as usize;
        let end = start + channels as usize;
        // Ensure we don't go out of bounds, though chunks exact should prevent this
        let chunk = &audio[start..end.min(audio.len())];

        let sum: f32 = chunk.iter().sum();
        let mono_sample = sum / (channels as f32);
        mono_samples.push(mono_sample);
    }

    mono_samples
}