mod decoder; pub mod output; use std::sync::{ atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering}, Arc, }; use std::time::Duration; use crate::api::{Format, TrackDto}; #[derive(Debug, Clone)] pub enum PlayerCommand { Play(TrackInfo), Pause, Resume, Stop, Seek(u64), SetVolume(u8), } #[derive(Debug, Clone)] pub struct TrackInfo { pub track: TrackDto, pub url: String, pub format: Format, /// ReplayGain track gain in dB, if enabled and available. pub replaygain_db: Option, } #[derive(Debug, Clone, PartialEq)] pub enum PlayerState { Idle, Playing, Paused, Stopped, Error(String), } #[derive(Clone)] pub struct PlayerStatus { pub state: Arc>, pub position_secs: Arc, pub duration_secs: Arc, pub volume: Arc, pub current_track: Arc>>, /// Set to true by the decode thread when a track finishes naturally. pub track_finished: Arc, /// Set by the player loop when a seek command arrives; cleared by the decode thread. pub seek_requested: Arc, pub seek_target_secs: Arc, /// Linear gain factor to apply (1.0 = unity). Updated each time a new track starts. pub replaygain_gain: Arc>, /// When false the audio output is torn down after each track, producing a gap. pub gapless: Arc, } impl PlayerStatus { pub fn new() -> Self { Self { state: Arc::new(std::sync::Mutex::new(PlayerState::Idle)), position_secs: Arc::new(AtomicU64::new(0)), duration_secs: Arc::new(AtomicU64::new(0)), volume: Arc::new(AtomicU8::new(80)), current_track: Arc::new(std::sync::Mutex::new(None)), track_finished: Arc::new(AtomicBool::new(false)), seek_requested: Arc::new(AtomicBool::new(false)), seek_target_secs: Arc::new(AtomicU64::new(0)), replaygain_gain: Arc::new(std::sync::Mutex::new(1.0)), gapless: Arc::new(AtomicBool::new(false)), } } pub fn get_state(&self) -> PlayerState { self.state.lock().unwrap().clone() } pub fn get_position(&self) -> u64 { self.position_secs.load(Ordering::Relaxed) } pub fn get_duration(&self) -> u64 { self.duration_secs.load(Ordering::Relaxed) } pub fn get_volume(&self) -> u8 { self.volume.load(Ordering::Relaxed) } pub fn get_current_track(&self) -> Option { self.current_track.lock().unwrap().clone() } } pub struct Player { pub cmd_tx: std::sync::mpsc::SyncSender, pub status: PlayerStatus, } impl Player { pub fn new() -> Self { let (cmd_tx, cmd_rx) = std::sync::mpsc::sync_channel(32); let status = PlayerStatus::new(); let status_clone = status.clone(); std::thread::spawn(move || { player_loop(cmd_rx, status_clone); }); Self { cmd_tx, status } } pub fn send(&self, cmd: PlayerCommand) { self.cmd_tx.send(cmd).ok(); } pub fn pause(&self) { self.send(PlayerCommand::Pause); } pub fn resume(&self) { self.send(PlayerCommand::Resume); } pub fn stop(&self) { self.send(PlayerCommand::Stop); } pub fn set_volume(&self, vol: u8) { self.status.volume.store(vol, Ordering::Relaxed); self.send(PlayerCommand::SetVolume(vol)); } pub fn seek(&self, secs: u64) { self.status.seek_target_secs.store(secs, Ordering::Relaxed); self.status.seek_requested.store(true, Ordering::SeqCst); } } /// The player loop runs on a single dedicated OS thread. /// It owns the `AudioOutput` locally so there are no Send constraints. /// Decoding is performed inline; the command channel is polled via try_recv /// inside the decode loop to handle Pause/Resume/Seek/Stop/Play without /// tearng down and re-opening the audio device between tracks. fn player_loop(rx: std::sync::mpsc::Receiver, status: PlayerStatus) { use std::sync::mpsc::RecvTimeoutError; let mut audio_output: Option = None; let paused = Arc::new(AtomicBool::new(false)); // pending_info holds a Play command that interrupted an ongoing decode let mut pending_info: Option = None; 'outer: loop { // Wait for a Play command (or use one that was interrupted) let info = if let Some(p) = pending_info.take() { p } else { loop { match rx.recv_timeout(Duration::from_millis(100)) { Ok(PlayerCommand::Play(info)) => break info, Ok(PlayerCommand::Stop) => { audio_output = None; paused.store(false, Ordering::SeqCst); *status.state.lock().unwrap() = PlayerState::Idle; *status.current_track.lock().unwrap() = None; status.position_secs.store(0, Ordering::Relaxed); status.duration_secs.store(0, Ordering::Relaxed); } Ok(PlayerCommand::SetVolume(v)) => { status.volume.store(v, Ordering::Relaxed); } Ok(PlayerCommand::Seek(s)) => { status.seek_target_secs.store(s, Ordering::Relaxed); status.seek_requested.store(true, Ordering::SeqCst); } Ok(_) => {} // Pause/Resume ignored when idle Err(RecvTimeoutError::Timeout) => {} Err(RecvTimeoutError::Disconnected) => break 'outer, } } }; // Compute ReplayGain factor let rg_factor = info.replaygain_db .map(|db| 10f32.powf(db as f32 / 20.0)) .unwrap_or(1.0); *status.replaygain_gain.lock().unwrap() = rg_factor; *status.state.lock().unwrap() = PlayerState::Playing; *status.current_track.lock().unwrap() = Some(info.track.clone()); if let Some(dur) = info.track.duration { status.duration_secs.store(dur as u64, Ordering::Relaxed); } status.position_secs.store(0, Ordering::Relaxed); paused.store(false, Ordering::SeqCst); match decoder::play_track_inline(&info.url, &status, &paused, &mut audio_output, &rx) { Ok(Some(next_info)) => { // Interrupted by a new Play — loop immediately with reused audio output pending_info = Some(next_info); } Ok(None) => { // Track finished naturally — tear down audio output if gapless is off if !status.gapless.load(Ordering::Relaxed) { audio_output = None; } *status.state.lock().unwrap() = PlayerState::Idle; status.track_finished.store(true, Ordering::SeqCst); } Err(e) => { eprintln!("playback error: {e}"); *status.state.lock().unwrap() = PlayerState::Error(e.to_string()); } } } }