melors 0.2.2

Keyboard-first terminal MP3 player with queue, search, and tag editing
use std::collections::{HashMap, HashSet, VecDeque};
use std::fs::File;
use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Receiver, Sender};
use std::time::{Duration, Instant};

use anyhow::{Context, Result};
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player as RodioPlayer, Source};

mod analysis;
mod control;

const ANALYSIS_BANDS: usize = 36;
const MAX_ANALYSIS_CACHE_ITEMS: usize = 24;
const MAX_CONCURRENT_ANALYSIS_JOBS: usize = 2;
const MAX_QUEUED_ANALYSIS_JOBS: usize = 32;

pub struct Player {
    backend: Backend,
    current_path: Option<PathBuf>,
    current_mtime: Option<i64>,
    base_position_secs: i64,
    started_at: Option<Instant>,
    paused: bool,
    volume: f32,
    analysis: VisualizerAnalysis,
    analysis_cache: HashMap<AnalysisCacheKey, VisualizerAnalysis>,
    analysis_cache_order: VecDeque<AnalysisCacheKey>,
    analysis_pending: HashSet<AnalysisCacheKey>,
    analysis_queue: VecDeque<(PathBuf, AnalysisCacheKey)>,
    analysis_active_jobs: usize,
    analysis_tx: Sender<(AnalysisCacheKey, Option<VisualizerAnalysis>)>,
    analysis_rx: Receiver<(AnalysisCacheKey, Option<VisualizerAnalysis>)>,
    current_analysis_key: Option<AnalysisCacheKey>,
}

#[derive(Clone, Default)]
struct VisualizerAnalysis {
    frames: Vec<Vec<f32>>,
    frame_duration_secs: f32,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct AnalysisCacheKey {
    path: PathBuf,
    mtime: i64,
}

struct Backend {
    stream: MixerDeviceSink,
    sink: Option<RodioPlayer>,
}

impl Player {
    pub fn new() -> Result<Self> {
        let stream = DeviceSinkBuilder::open_default_sink().context("failed to init audio output")?;
        let (analysis_tx, analysis_rx) = mpsc::channel();
        Ok(Self {
            backend: Backend {
                stream,
                sink: None,
            },
            current_path: None,
            current_mtime: None,
            base_position_secs: 0,
            started_at: None,
            paused: true,
            volume: 1.0,
            analysis: VisualizerAnalysis::default(),
            analysis_cache: HashMap::new(),
            analysis_cache_order: VecDeque::new(),
            analysis_pending: HashSet::new(),
            analysis_queue: VecDeque::new(),
            analysis_active_jobs: 0,
            analysis_tx,
            analysis_rx,
            current_analysis_key: None,
        })
    }
}