use std::io::BufReader;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{
mpsc::{Receiver, Sender},
Arc,
};
use std::time::{Duration, Instant};
use camino::Utf8Path;
use crate::player::decoder;
use crate::player::history;
use crate::player::queue::{Queue, RepeatMode, TrackInfo};
#[derive(Debug)]
pub enum PlayerCommand {
LoadQueue(Vec<TrackInfo>),
PlayQueue(Vec<TrackInfo>),
Enqueue(TrackInfo),
JumpTo(usize),
Pause,
Resume,
Stop,
Next,
Prev,
ToggleShuffle,
CycleRepeat,
Seek(Duration),
SeekRelative(i64),
Volume(f32),
SetSleepTimer(Option<Duration>),
}
#[derive(Debug, Clone)]
pub enum PlayerEvent {
TrackStarted(TrackInfo),
Position(Duration),
TrackEnded,
QueueEmpty,
QueueUpdated {
tracks: Vec<TrackInfo>,
cursor: usize,
},
Stopped,
DecodeError {
path: String,
err: String,
},
TrackMetadata {
idx: usize,
track: TrackInfo,
},
SleepTimerSet(Option<Duration>),
SleepTimerFired,
}
#[derive(Debug, Clone)]
pub struct PlayerStatus {
pub current: Option<TrackInfo>,
pub position: Duration,
pub paused: bool,
pub queue_cursor: usize,
pub queue_tracks: Vec<TrackInfo>,
pub shuffle: bool,
pub repeat: RepeatMode,
pub volume: f32,
}
impl Default for PlayerStatus {
fn default() -> Self {
Self {
current: None,
position: Duration::ZERO,
paused: false,
queue_cursor: 0,
queue_tracks: Vec::new(),
shuffle: false,
repeat: RepeatMode::Off,
volume: 1.0,
}
}
}
#[derive(Clone)]
pub struct PlayerHandle {
tx: Sender<PlayerCommand>,
}
impl PlayerHandle {
pub fn send(&self, cmd: PlayerCommand) {
let _ = self.tx.send(cmd);
}
}
struct TrackDoneNotifier<S> {
inner: S,
done: Arc<AtomicBool>,
}
impl<S> Iterator for TrackDoneNotifier<S>
where
S: rodio::Source,
S::Item: rodio::Sample,
{
type Item = S::Item;
fn next(&mut self) -> Option<S::Item> {
let s = self.inner.next();
if s.is_none() {
self.done.store(true, Ordering::Release);
}
s
}
}
impl<S> rodio::Source for TrackDoneNotifier<S>
where
S: rodio::Source,
S::Item: rodio::Sample,
{
fn current_frame_len(&self) -> Option<usize> {
self.inner.current_frame_len()
}
fn channels(&self) -> u16 {
self.inner.channels()
}
fn sample_rate(&self) -> u32 {
self.inner.sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner.total_duration()
}
}
struct Engine {
cmd_rx: Receiver<PlayerCommand>,
event_tx: Sender<PlayerEvent>,
queue: Queue,
sink: rodio::Sink,
paused: bool,
current_done: Option<Arc<AtomicBool>>,
preloaded: Option<(TrackInfo, Arc<AtomicBool>)>,
current_track: Option<TrackInfo>,
sleep_until: Option<Instant>,
}
impl Engine {
fn new(
cmd_rx: Receiver<PlayerCommand>,
event_tx: Sender<PlayerEvent>,
sink: rodio::Sink,
) -> Self {
Self {
cmd_rx,
event_tx,
queue: Queue::new(),
sink,
paused: false,
current_done: None,
preloaded: None,
current_track: None,
sleep_until: None,
}
}
fn run(mut self) {
loop {
match self.cmd_rx.recv_timeout(Duration::from_millis(50)) {
Ok(cmd) => {
if !self.handle_command(cmd) {
break; }
}
Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => break,
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {}
}
if !self.sink.empty() && !self.paused {
let pos = self.sink.get_pos();
let _ = self.event_tx.send(PlayerEvent::Position(pos));
}
if let Some(deadline) = self.sleep_until {
if Instant::now() >= deadline {
self.sleep_until = None;
self.sink.pause();
self.paused = true;
let _ = self.event_tx.send(PlayerEvent::SleepTimerFired);
}
}
let current_fired = self
.current_done
.as_ref()
.map(|d| d.load(Ordering::Acquire))
.unwrap_or(false);
if current_fired {
self.finish_current(true);
let _ = self.event_tx.send(PlayerEvent::TrackEnded);
if let Some((next_track, next_done)) = self.preloaded.take() {
self.current_done = Some(next_done);
self.queue.advance();
let _ = self.event_tx.send(PlayerEvent::TrackStarted(next_track));
self.emit_queue_updated();
self.try_preload_next();
} else if self.queue.advance() {
self.current_done = None;
self.play_current();
} else {
self.current_done = None;
let _ = self.event_tx.send(PlayerEvent::QueueEmpty);
}
}
}
self.finish_current(false);
}
fn handle_command(&mut self, cmd: PlayerCommand) -> bool {
match cmd {
PlayerCommand::LoadQueue(tracks) => {
self.finish_current(false);
self.sink.stop();
self.current_done = None;
self.preloaded = None;
self.queue.set(tracks.clone());
self.paused = false;
self.emit_queue_updated();
let _ = self.event_tx.send(PlayerEvent::Stopped);
self.spawn_tag_scan(tracks);
}
PlayerCommand::PlayQueue(tracks) => {
self.sink.stop();
self.current_done = None;
self.preloaded = None;
self.queue.set(tracks.clone());
self.paused = false;
self.emit_queue_updated();
self.play_current();
self.spawn_tag_scan(tracks);
}
PlayerCommand::Enqueue(track) => {
self.queue.push(track);
self.emit_queue_updated();
}
PlayerCommand::JumpTo(idx) => {
self.sink.stop();
self.current_done = None;
self.preloaded = None;
self.queue.jump_to(idx);
self.paused = false;
self.play_current();
}
PlayerCommand::Pause => {
self.sink.pause();
self.paused = true;
}
PlayerCommand::Resume => {
self.sink.play();
self.paused = false;
}
PlayerCommand::Stop => {
self.finish_current(false);
self.sink.stop();
self.current_done = None;
self.preloaded = None;
self.paused = false;
let _ = self.event_tx.send(PlayerEvent::Stopped);
return true;
}
PlayerCommand::Next => {
self.sink.stop();
self.current_done = None;
self.preloaded = None;
if self.queue.advance() {
self.paused = false;
self.play_current();
} else {
let _ = self.event_tx.send(PlayerEvent::QueueEmpty);
}
}
PlayerCommand::Prev => {
self.sink.stop();
self.current_done = None;
self.preloaded = None;
self.queue.prev();
self.paused = false;
self.play_current();
}
PlayerCommand::ToggleShuffle => {
self.queue.toggle_shuffle();
}
PlayerCommand::CycleRepeat => {
self.queue.repeat = self.queue.repeat.next();
}
PlayerCommand::Seek(pos) => {
let _ = self.sink.try_seek(pos);
self.preloaded = None;
self.try_preload_next();
}
PlayerCommand::SeekRelative(delta_secs) => {
let pos = self.sink.get_pos();
let new_secs = (pos.as_secs_f64() + delta_secs as f64).max(0.0);
let _ = self.sink.try_seek(Duration::from_secs_f64(new_secs));
self.preloaded = None;
self.try_preload_next();
}
PlayerCommand::Volume(v) => {
self.sink.set_volume(v.clamp(0.0, 2.0));
}
PlayerCommand::SetSleepTimer(dur) => {
self.sleep_until = dur.map(|d| Instant::now() + d);
let _ = self.event_tx.send(PlayerEvent::SleepTimerSet(dur));
}
}
true
}
fn finish_current(&mut self, completed: bool) {
if let Some(track) = self.current_track.take() {
let pos = if completed {
track
.duration_secs
.unwrap_or_else(|| self.sink.get_pos().as_secs_f64())
} else {
self.sink.get_pos().as_secs_f64()
};
history::append(&history::HistoryEntry::from_track(&track, pos));
}
}
fn spawn_tag_scan(&self, tracks: Vec<TrackInfo>) {
let tx = self.event_tx.clone();
std::thread::spawn(move || {
for (idx, track) in tracks.into_iter().enumerate() {
let tagged = track.with_tags();
if tx
.send(PlayerEvent::TrackMetadata { idx, track: tagged })
.is_err()
{
break;
}
}
});
}
fn emit_queue_updated(&self) {
let _ = self.event_tx.send(PlayerEvent::QueueUpdated {
tracks: self.queue.tracks().to_vec(),
cursor: self.queue.cursor(),
});
}
fn play_current(&mut self) {
let Some(track) = self.queue.current().cloned() else {
return;
};
self.finish_current(false);
self.sink.stop();
self.current_done = None;
self.preloaded = None;
let track = track.with_tags();
if let Some(phys_idx) = self.queue.current_phys_idx() {
self.queue.update_at(phys_idx, track.clone());
}
match append_path_notified(&self.sink, &track.path) {
Ok(done) => {
self.sink.play();
self.current_done = Some(done);
self.current_track = Some(track.clone());
self.emit_queue_updated();
let _ = self.event_tx.send(PlayerEvent::TrackStarted(track));
self.try_preload_next();
}
Err(e) => {
let _ = self.event_tx.send(PlayerEvent::DecodeError {
path: track.path.to_string(),
err: e.to_string(),
});
if self.queue.advance() {
self.play_current();
}
}
}
}
fn try_preload_next(&mut self) {
if self.preloaded.is_some() {
return;
}
let Some(next) = self.queue.peek_next().cloned() else {
return;
};
if let Ok(done) = append_path_notified(&self.sink, &next.path) {
self.preloaded = Some((next, done));
}
}
}
fn append_path_notified(sink: &rodio::Sink, path: &Utf8Path) -> anyhow::Result<Arc<AtomicBool>> {
let done = Arc::new(AtomicBool::new(false));
if decoder::is_dsd(path) {
match crate::transcode::ffmpeg::detect() {
None => anyhow::bail!(
"⚠ DSD playback requires ffmpeg in PATH — \
install ffmpeg to play {}",
path.file_name().unwrap_or(path.as_str())
),
Some(_) => {
let src = decoder::DsdSource::open(path)?;
sink.append(TrackDoneNotifier {
inner: src,
done: done.clone(),
});
}
}
} else {
let file = std::fs::File::open(path.as_std_path())
.map_err(|e| anyhow::anyhow!("cannot open {path}: {e}"))?;
let src = rodio::Decoder::new(BufReader::new(file))
.map_err(|e| anyhow::anyhow!("cannot decode {path}: {e}"))?;
sink.append(TrackDoneNotifier {
inner: src,
done: done.clone(),
});
}
Ok(done)
}
pub fn spawn() -> Option<(PlayerHandle, Receiver<PlayerEvent>)> {
let (cmd_tx, cmd_rx) = std::sync::mpsc::channel::<PlayerCommand>();
let (event_tx, event_rx) = std::sync::mpsc::channel::<PlayerEvent>();
let (ready_tx, ready_rx) = std::sync::mpsc::sync_channel::<bool>(1);
std::thread::spawn(move || {
let Ok((stream, stream_handle)) = rodio::OutputStream::try_default() else {
let _ = ready_tx.send(false);
return;
};
let Ok(sink) = rodio::Sink::try_new(&stream_handle) else {
let _ = ready_tx.send(false);
return;
};
let _ = ready_tx.send(true);
let _stream = stream;
let engine = Engine::new(cmd_rx, event_tx, sink);
engine.run();
});
match ready_rx.recv_timeout(Duration::from_secs(2)) {
Ok(true) => Some((PlayerHandle { tx: cmd_tx }, event_rx)),
_ => None,
}
}