use anyhow::{Context, Result};
use rodio::Source;
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player};
use std::{
fs::File,
path::PathBuf,
sync::mpsc::{self, Receiver, Sender},
thread,
time::Duration,
};
#[derive(Debug)]
pub enum PlayerCommand {
Play {
path: PathBuf,
speed: f32,
},
Stop,
Pause,
Resume,
Seek {
path: PathBuf,
position: Duration,
paused: bool,
speed: f32,
},
SetVolume(f32),
SetSpeed(f32),
Quit,
}
#[derive(Debug)]
pub enum PlayerEvent {
TrackFinished,
Error(String),
}
const VOLUME_STEP: f32 = 0.01;
const VOLUME_MIN: f32 = 0.0;
const VOLUME_MAX: f32 = 1.0;
pub struct PlayerHandle {
cmd_tx: Sender<PlayerCommand>,
event_rx: Receiver<PlayerEvent>,
pub volume: f32,
pub is_paused: bool,
pub playback_speed: f32,
}
impl PlayerHandle {
pub fn send(&self, cmd: PlayerCommand) {
let _ = self.cmd_tx.send(cmd);
}
pub fn play(&mut self, path: PathBuf) {
self.is_paused = false;
self.send(PlayerCommand::Play {
path,
speed: self.playback_speed,
});
}
pub fn stop(&mut self) {
self.is_paused = false;
self.send(PlayerCommand::Stop);
}
pub fn pause(&mut self) {
self.is_paused = true;
self.send(PlayerCommand::Pause);
}
pub fn resume(&mut self) {
self.is_paused = false;
self.send(PlayerCommand::Resume);
}
pub fn seek(&self, path: PathBuf, position: Duration, paused: bool) {
self.send(PlayerCommand::Seek {
path,
position,
paused,
speed: self.playback_speed,
});
}
pub fn set_volume(&mut self, v: f32) {
self.volume = v.clamp(VOLUME_MIN, VOLUME_MAX);
self.send(PlayerCommand::SetVolume(self.volume));
}
pub fn set_speed(&mut self, speed: f32) {
self.playback_speed = speed;
self.send(PlayerCommand::SetSpeed(speed));
}
pub fn volume_up(&mut self) {
self.set_volume(self.volume + VOLUME_STEP);
}
pub fn volume_down(&mut self) {
self.set_volume(self.volume - VOLUME_STEP);
}
pub fn drain_events(&self) -> Vec<PlayerEvent> {
let mut out = Vec::new();
while let Ok(ev) = self.event_rx.try_recv() {
out.push(ev);
}
out
}
}
impl Drop for PlayerHandle {
fn drop(&mut self) {
self.send(PlayerCommand::Quit);
}
}
pub fn spawn_audio_thread(default_volume: f32) -> Result<PlayerHandle> {
let volume = default_volume.clamp(VOLUME_MIN, VOLUME_MAX);
let (cmd_tx, cmd_rx) = mpsc::channel::<PlayerCommand>();
let (event_tx, event_rx) = mpsc::channel::<PlayerEvent>();
let mut sink = DeviceSinkBuilder::open_default_sink()
.context("could not open default audio output sink")?;
sink.log_on_drop(false);
thread::Builder::new()
.name("audium-audio".into())
.spawn(move || audio_thread_main(&sink, &cmd_rx, &event_tx, volume))?;
Ok(PlayerHandle {
cmd_tx,
event_rx,
volume,
is_paused: false,
playback_speed: 1.0,
})
}
fn audio_thread_main(
sink: &MixerDeviceSink,
cmd_rx: &Receiver<PlayerCommand>,
event_tx: &Sender<PlayerEvent>,
default_volume: f32,
) {
let player = Player::connect_new(sink.mixer());
player.set_volume(default_volume);
player.set_speed(1.0);
let mut stopped_explicitly = true;
loop {
loop {
match cmd_rx.try_recv() {
Ok(cmd) => {
handle_command(&player, cmd, &mut stopped_explicitly, event_tx);
}
Err(mpsc::TryRecvError::Empty) => break,
Err(mpsc::TryRecvError::Disconnected) => return,
}
}
if !stopped_explicitly && player.empty() {
stopped_explicitly = true;
let _ = event_tx.send(PlayerEvent::TrackFinished);
}
thread::sleep(Duration::from_millis(20));
}
}
fn open_source(path: &std::path::Path) -> Result<Decoder<std::io::BufReader<File>>> {
let file =
File::open(path).with_context(|| format!("could not open \"{}\"", path.display()))?;
Decoder::try_from(file)
.map_err(|e| anyhow::Error::msg(format!("could not decode \"{}\": {e}", path.display())))
}
fn handle_command(
player: &Player,
cmd: PlayerCommand,
stopped: &mut bool,
event_tx: &Sender<PlayerEvent>,
) {
match cmd {
PlayerCommand::Play { path, speed } => {
player.stop();
match open_source(&path) {
Ok(source) => {
*stopped = false;
player.set_speed(speed);
player.append(source);
player.play();
}
Err(e) => {
*stopped = true;
let _ = event_tx.send(PlayerEvent::Error(e.to_string()));
}
}
}
PlayerCommand::Seek {
path,
position,
paused,
speed,
} => {
player.stop();
match open_source(&path) {
Ok(mut source) => {
*stopped = false;
let _ = source.try_seek(position);
player.set_speed(speed);
player.append(source);
if paused {
player.pause();
} else {
player.play();
}
}
Err(e) => {
*stopped = true;
let _ = event_tx.send(PlayerEvent::Error(format!("Seek failed: {e}")));
}
}
}
PlayerCommand::Stop => {
*stopped = true;
player.stop();
}
PlayerCommand::Pause => {
player.pause();
}
PlayerCommand::Resume => {
player.play();
}
PlayerCommand::SetVolume(v) => {
player.set_volume(v.clamp(VOLUME_MIN, VOLUME_MAX));
}
PlayerCommand::SetSpeed(s) => {
player.set_speed(s);
}
PlayerCommand::Quit => {
player.stop();
}
}
}
pub fn validate_decodable(path: &std::path::Path) -> Result<()> {
open_source(path).map(|_| ())
}
pub fn resolve_duration(path: &std::path::Path) -> Option<Duration> {
let file = File::open(path).ok()?;
let source = Decoder::try_from(file).ok()?;
source.total_duration()
}