pulsedeck 0.1.7

A cyber-synthwave internet radio player and smart tape recorder for your terminal
use super::AudioStatus;
use std::sync::{mpsc, Mutex};
use std::time::Instant;

const EWMA_ALPHA: f32 = 0.15;
const DEFAULT_FALLBACK_BYTES_PER_SEC: f32 = 16_000.0;
const MIN_MEASURED_SECONDS: f32 = 0.001;

#[derive(Debug)]
pub(super) struct BufferStatusMeter {
    fallback_velocity: f32,
    state: Mutex<BufferStatusState>,
}

#[derive(Debug)]
struct BufferStatusState {
    historical_velocity: f32,
    last_consumed_at: Option<Instant>,
}

impl BufferStatusMeter {
    pub(super) fn new(fallback_bytes_per_sec: usize) -> Self {
        let fallback_velocity = (fallback_bytes_per_sec as f32).max(DEFAULT_FALLBACK_BYTES_PER_SEC);

        Self {
            fallback_velocity,
            state: Mutex::new(BufferStatusState {
                historical_velocity: fallback_velocity,
                last_consumed_at: None,
            }),
        }
    }

    pub(super) fn report_fill_level(
        &self,
        len: usize,
        capacity: usize,
        status_tx: &mpsc::Sender<AudioStatus>,
    ) {
        let (percent, seconds) = {
            let state = self.state.lock().unwrap();
            buffer_status_from_velocity(
                len,
                capacity,
                state.historical_velocity,
                self.fallback_velocity,
            )
        };

        send_buffer_status(status_tx, percent, seconds);
    }

    pub(super) fn record_consumed(
        &self,
        bytes_read: usize,
        len: usize,
        capacity: usize,
        status_tx: &mpsc::Sender<AudioStatus>,
    ) {
        if bytes_read == 0 {
            return;
        }

        let (percent, seconds) = {
            let mut state = self.state.lock().unwrap();
            let now = Instant::now();
            let delta_t = state
                .last_consumed_at
                .replace(now)
                .map(|last| now.saturating_duration_since(last).as_secs_f32())
                .unwrap_or(0.0);

            if delta_t >= MIN_MEASURED_SECONDS {
                buffer_level_status_adaptive_with_fallback(
                    len,
                    capacity,
                    &mut state.historical_velocity,
                    bytes_read,
                    delta_t,
                    self.fallback_velocity,
                )
            } else {
                buffer_status_from_velocity(
                    len,
                    capacity,
                    state.historical_velocity,
                    self.fallback_velocity,
                )
            }
        };

        send_buffer_status(status_tx, percent, seconds);
    }
}

fn send_buffer_status(status_tx: &mpsc::Sender<AudioStatus>, percent: u8, seconds: u32) {
    let _ = status_tx.send(AudioStatus::BufferLevel { percent, seconds });
}

fn buffer_level_status_adaptive_with_fallback(
    len: usize,
    capacity: usize,
    historical_velocity: &mut f32,
    instant_bytes_read: usize,
    delta_t: f32,
    fallback_velocity: f32,
) -> (u8, u32) {
    if instant_bytes_read > 0 && delta_t >= MIN_MEASURED_SECONDS {
        let current_velocity = instant_bytes_read as f32 / delta_t;
        *historical_velocity = ewma_velocity(*historical_velocity, current_velocity);
    }

    buffer_status_from_velocity(len, capacity, *historical_velocity, fallback_velocity)
}

fn ewma_velocity(previous: f32, current: f32) -> f32 {
    EWMA_ALPHA.mul_add(current, (1.0 - EWMA_ALPHA) * previous)
}

fn buffer_status_from_velocity(
    len: usize,
    capacity: usize,
    historical_velocity: f32,
    fallback_velocity: f32,
) -> (u8, u32) {
    if capacity == 0 {
        return (0, 0);
    }

    let percent = len.saturating_mul(100).checked_div(capacity).unwrap_or(0);
    let percent = percent.min(100) as u8;

    let safe_velocity = if historical_velocity > 1.0 {
        historical_velocity
    } else {
        fallback_velocity.max(DEFAULT_FALLBACK_BYTES_PER_SEC)
    };
    let seconds = (len as f32 / safe_velocity).round() as u32;

    (percent, seconds)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn adaptive_status_smooths_consumption_velocity_spikes() {
        let mut velocity = 16_000.0;
        let (_, seconds) = buffer_level_status_adaptive_with_fallback(
            160_000,
            1_000_000,
            &mut velocity,
            64_000,
            1.0,
            16_000.0,
        );

        assert!((velocity - 23_200.0).abs() < 0.1);
        assert_eq!(seconds, 7);
    }

    #[test]
    fn adaptive_status_uses_fallback_when_history_is_not_ready() {
        let mut velocity = 0.0;
        let (percent, seconds) = buffer_level_status_adaptive_with_fallback(
            160_000,
            1_000_000,
            &mut velocity,
            0,
            0.0,
            16_000.0,
        );

        assert_eq!(percent, 16);
        assert_eq!(seconds, 10);
    }

    #[test]
    fn adaptive_status_clamps_percent_to_capacity() {
        let mut velocity = 16_000.0;
        let (percent, seconds) = buffer_level_status_adaptive_with_fallback(
            2_000_000,
            1_000_000,
            &mut velocity,
            16_000,
            1.0,
            16_000.0,
        );

        assert_eq!(percent, 100);
        assert_eq!(seconds, 125);
    }

    #[test]
    fn adaptive_status_handles_zero_capacity() {
        let mut velocity = 16_000.0;
        let (percent, seconds) = buffer_level_status_adaptive_with_fallback(
            160_000,
            0,
            &mut velocity,
            16_000,
            1.0,
            16_000.0,
        );

        assert_eq!(percent, 0);
        assert_eq!(seconds, 0);
    }

    #[test]
    fn ewma_velocity_damps_burst_variability() {
        let first = ewma_velocity(16_000.0, 64_000.0);
        let second = ewma_velocity(first, 8_000.0);

        assert!(first < 64_000.0);
        assert!(second > 8_000.0);
        assert!(second < first);
    }
}