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(PathBuf),
Stop,
Pause,
Resume,
SetVolume(f32),
Quit,
}
#[derive(Debug)]
#[allow(dead_code)]
pub enum PlayerEvent {
DurationResolved(Duration),
TrackFinished,
Error(String),
}
const VOLUME_STEP: f32 = 0.1;
const VOLUME_MIN: f32 = 0.0;
const VOLUME_MAX: f32 = 1.0;
const VOLUME_INIT: f32 = 0.7;
pub struct PlayerHandle {
cmd_tx: Sender<PlayerCommand>,
event_rx: Receiver<PlayerEvent>,
pub volume: f32,
pub is_paused: bool,
}
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));
}
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 set_volume(&mut self, v: f32) {
self.volume = v.clamp(VOLUME_MIN, VOLUME_MAX);
self.send(PlayerCommand::SetVolume(self.volume));
}
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() -> Result<PlayerHandle> {
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))?;
Ok(PlayerHandle {
cmd_tx,
event_rx,
volume: VOLUME_INIT,
is_paused: false,
})
}
fn audio_thread_main(
sink: MixerDeviceSink,
cmd_rx: Receiver<PlayerCommand>,
event_tx: Sender<PlayerEvent>,
) {
let player = Player::connect_new(sink.mixer());
player.set_volume(VOLUME_INIT);
let mut stopped_explicitly = false;
loop {
loop {
match cmd_rx.try_recv() {
Ok(cmd) => {
handle_command(&player, cmd, &mut stopped_explicitly);
}
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 handle_command(player: &Player, cmd: PlayerCommand, stopped: &mut bool) {
match cmd {
PlayerCommand::Play(path) => {
player.stop();
*stopped = false;
match File::open(&path) {
Err(e) => {
eprintln!("audium-audio: failed to open {:?}: {e}", path);
}
Ok(file) => match Decoder::try_from(file) {
Err(e) => {
eprintln!("audium-audio: failed to decode {:?}: {e}", path);
}
Ok(source) => {
player.append(source);
player.play();
}
},
}
}
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::Quit => {
player.stop();
}
}
}
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()
}