diff --git a/Cargo.toml b/Cargo.toml
index 007750d..3372a40 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -8,7 +8,7 @@ edition = "2021"
[dependencies]
# general
-itertools = "0.11.0"
+itertools = "0.12.0"
log = "0.4.19"
simplelog = "0.12.1"
serde = { version = "1.0.181", features = ["derive", "rc"] }
diff --git a/src/cache.rs b/src/cache.rs
index f837813..054f83f 100644
--- a/src/cache.rs
+++ b/src/cache.rs
@@ -1,4 +1,4 @@
-use crate::{config::Config, decoder, song::Song};
+use crate::{config::Config, song::Song};
use std::{
collections::HashMap,
fs::Metadata,
@@ -64,7 +64,7 @@ impl Cache {
trace!("Found file {}", e.path().display());
})
.filter_map(|(e, m)| {
- decoder::song_from_file(e.path())
+ Song::load(e.path())
.map(|s| (e.path().to_path_buf(), m, s))
.map_err(|e| {
warn!("Failed to read song from {:?}: {}", e, e);
diff --git a/src/decoder.rs b/src/decoder.rs
deleted file mode 100644
index 8c43e27..0000000
--- a/src/decoder.rs
+++ /dev/null
@@ -1,206 +0,0 @@
-use std::{collections::HashMap, thread::JoinHandle};
-
-use log::{info, warn};
-use replaygain::ReplayGain;
-use symphonia::core::{
- audio::{SampleBuffer, SignalSpec},
- codecs::{DecoderOptions, CODEC_TYPE_NULL},
- formats::FormatOptions,
- io::{MediaSourceStream, MediaSourceStreamOptions},
- meta::{MetadataOptions, MetadataRevision},
- probe::Hint,
-};
-
-use crate::song::{Song, StandardTagKey, Value};
-
-pub fn read_audio
(
- path: P,
- mut f: F,
-) -> anyhow::Result<(Option, SignalSpec, JoinHandle<()>)>
-where
- P: AsRef,
- F: FnMut(&[f32]) -> anyhow::Result<()> + Send + 'static,
-{
- let src = std::fs::File::open(path)?;
-
- let mss = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default());
- let mut probed = symphonia::default::get_probe().format(
- &Hint::new(),
- mss,
- &FormatOptions::default(),
- &MetadataOptions::default(),
- )?;
-
- let metadata = {
- let mut meta = probed.format.metadata();
- meta.skip_to_latest().cloned()
- };
-
- let mut format_reader = probed.format;
-
- let track = format_reader
- .tracks()
- .into_iter()
- .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
- .ok_or(anyhow::anyhow!("No audio tracks found"))?;
-
- let codec_params = track.codec_params.clone();
- let track_id = track.id;
-
- let mut decoder =
- symphonia::default::get_codecs().make(&track.codec_params, &DecoderOptions::default())?;
-
- let handle = std::thread::spawn(move || loop {
- match format_reader.next_packet() {
- Ok(packet) => {
- if packet.track_id() == track_id {
- let data = match decoder.decode(&packet) {
- Ok(d) => d,
- Err(e) => {
- warn!("Failed to decode packet {:?}", e);
- break;
- }
- };
-
- let mut sample_buffer = SampleBuffer::new(
- data.capacity() as u64,
- SignalSpec::new(
- codec_params.sample_rate.unwrap(),
- codec_params.channels.unwrap(),
- ),
- );
- sample_buffer.copy_interleaved_ref(data);
-
- if f(sample_buffer.samples()).is_err() {
- break;
- }
- }
- }
- Err(symphonia::core::errors::Error::IoError(e)) => match e.kind() {
- std::io::ErrorKind::UnexpectedEof => break,
- _ => {
- warn!("Failed to read packet {:?}", e);
- break;
- }
- },
- Err(e) => {
- warn!("Failed to read packet {:?}", e);
- break;
- }
- };
- });
-
- Ok((
- metadata,
- SignalSpec::new(
- codec_params.sample_rate.unwrap(),
- codec_params.channels.unwrap(),
- ),
- handle,
- ))
-}
-
-pub fn song_from_file(path: P) -> anyhow::Result
-where
- P: AsRef,
-{
- let src = std::fs::File::open(&path)?;
-
- let mss = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default());
- let mut probed = symphonia::default::get_probe().format(
- &Hint::new().with_extension(path.as_ref().extension().unwrap().to_str().unwrap()),
- mss,
- &FormatOptions {
- prebuild_seek_index: false,
- seek_index_fill_rate: 0,
- enable_gapless: true,
- },
- &MetadataOptions::default(),
- )?;
-
- let metadata = {
- let mut meta = probed.format.metadata();
- meta.skip_to_latest().cloned()
- };
-
- let track = probed
- .format
- .tracks()
- .into_iter()
- .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
- .ok_or(anyhow::anyhow!("No audio tracks found"))?;
-
- let duration = track
- .codec_params
- .time_base
- .ok_or(anyhow::anyhow!(
- "No time base found for track {:?}",
- track.id
- ))?
- .calc_time(track.codec_params.n_frames.ok_or(anyhow::anyhow!(
- "No frame count found for track {:?}",
- track.id
- ))?);
-
- let duration = std::time::Duration::from_secs_f64(duration.seconds as f64 + duration.frac);
-
- let (mut standard_tags, other_tags) = metadata
- .map(|m| {
- let s = m
- .tags()
- .into_iter()
- .filter_map(|t| t.std_key.map(|k| (k.into(), t.value.clone().into())))
- .collect::>();
-
- let o = m
- .tags()
- .into_iter()
- .filter(|t| t.std_key == None)
- .map(|t| (t.key.clone(), t.value.clone().into()))
- .collect::>();
-
- (s, o)
- })
- .unwrap_or_default();
-
- if !standard_tags.contains_key(&StandardTagKey::ReplayGainTrackGain) {
- info!(
- "File {} is missing ReplayGain, calculating",
- path.as_ref().display()
- );
-
- let mut rg = ReplayGain::new(
- track
- .codec_params
- .sample_rate
- .expect("No sample rate found") as usize,
- )
- .expect("Failed to create ReplayGain");
-
- let rg_ref =
- unsafe { std::mem::transmute::<&'_ mut ReplayGain, &'static mut ReplayGain>(&mut rg) };
- let (_, _, handle) = read_audio(&path, |data| {
- rg_ref.process_samples(data);
-
- Ok(())
- })?;
- handle.join().expect("Failed to join thread");
-
- let (gain, peak) = rg.finish();
-
- standard_tags.insert(
- StandardTagKey::ReplayGainTrackGain,
- Value::Float(gain as f64),
- );
- standard_tags.insert(
- StandardTagKey::ReplayGainTrackPeak,
- Value::Float(peak as f64),
- );
- }
-
- Ok(Song {
- standard_tags,
- other_tags,
- duration,
- })
-}
diff --git a/src/main.rs b/src/main.rs
index 8f3b312..a26d3b8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,24 +1,19 @@
-use std::{
- fs::File,
- io::{Read, Write},
- sync::{atomic::Ordering, Arc},
-};
+use std::{fs::File, sync::Arc};
+use anyhow::Context;
use cache::Cache;
use log::{info, trace, warn, LevelFilter};
-use player::Player;
use simplelog::{CombinedLogger, WriteLogger};
-use crate::{config::Config, tui::tui};
+use crate::{config::Config, player::Player, tui::tui};
mod cache;
mod config;
-mod decoder;
mod player;
mod song;
mod tui;
-fn main() {
+fn main() -> anyhow::Result<()> {
let config_dir = dirs::config_dir()
.expect("Unable to find config directory")
.join("ramp");
@@ -42,7 +37,7 @@ fn main() {
}),
);
- let _logger = CombinedLogger::init(vec![WriteLogger::new(
+ CombinedLogger::init(vec![WriteLogger::new(
#[cfg(debug_assertions)]
LevelFilter::Trace,
#[cfg(not(debug_assertions))]
@@ -55,19 +50,8 @@ fn main() {
.build(),
File::create(&config.log_path).expect("Failed to create log file"),
)])
- .expect("Failed to initialize logger");
-
- let quit = Arc::new(std::sync::atomic::AtomicBool::new(false));
- let mut f = File::open(&config.log_path).expect("Failed to open log file");
- let _quit = quit.clone();
- let handle = std::thread::spawn(move || {
- while !_quit.load(Ordering::SeqCst) {
- let mut buf = Vec::new();
- f.read_to_end(&mut buf).unwrap();
- std::io::stdout().write_all(&buf).unwrap();
- std::thread::sleep(std::time::Duration::from_millis(100));
- }
- });
+ .context("Failed to initialize logger")?;
+ info!("Logger initialized");
trace!("loading cache");
let (cache, old_config) = Cache::load(&config).unwrap_or_else(|e| {
@@ -99,25 +83,11 @@ fn main() {
let cache = Arc::new(cache);
trace!("initializing player");
- let player = Player::new(cache.clone()).expect("Failed to initialize player");
-
- {
- trace!("attaching media controls");
- player
- .lock()
- .unwrap()
- .attach_media_controls()
- .unwrap_or_else(|e| {
- warn!("Failed to attach media controls: {e:?}");
- });
- }
-
- quit.store(true, Ordering::SeqCst);
- handle.join().expect("Failed to join log thread");
+ let (cmd, player) = Player::run(cache.clone()).context("Failed to initialize player")?;
trace!("entering tui");
- tui(config.clone(), cache.clone(), player.clone()).expect("Failed to run tui");
+ tui(config.clone(), cache.clone(), cmd, player).context("Error in tui")?;
trace!("tui exited");
- trace!("quitting");
+ Ok(())
}
diff --git a/src/player.rs b/src/player.rs
deleted file mode 100644
index 6883ceb..0000000
--- a/src/player.rs
+++ /dev/null
@@ -1,496 +0,0 @@
-use cpal::{
- traits::{DeviceTrait, HostTrait, StreamTrait},
- Device, OutputCallbackInfo, StreamConfig,
-};
-use log::{debug, error, info, trace, warn};
-use souvlaki::{MediaControlEvent, MediaControls, MediaMetadata, MediaPlayback, PlatformConfig};
-use symphonia::core::{
- audio::SignalSpec,
- meta::{MetadataRevision, StandardVisualKey},
-};
-use tempfile::NamedTempFile;
-
-use std::{
- collections::VecDeque,
- io::Write,
- path::{Path, PathBuf},
- sync::{
- mpsc::{Receiver, SyncSender},
- Arc, Mutex, Weak,
- },
- time::Duration,
-};
-
-use crate::{
- cache::Cache,
- decoder,
- song::{Song, StandardTagKey},
-};
-
-enum StreamCommand {
- Play,
- Pause,
-}
-
-struct QueuedSong {
- song: Song,
- path: PathBuf,
- metadata: Option,
- signal_spec: SignalSpec,
- receiver: Receiver>,
-}
-
-fn front_cover<'a>(metadata: &'a Option) -> Option<&[u8]> {
- metadata
- .as_ref()
- .and_then(|m| {
- m.visuals()
- .iter()
- .find(|v| v.usage == Some(StandardVisualKey::FrontCover))
- })
- .map(|v| v.data.as_ref())
-}
-
-struct CurrentSong {
- song: Song,
- path: PathBuf,
- metadata: Option,
- stream_command_sender: SyncSender,
- elapsed: Duration,
- cover_tempfile: NamedTempFile,
-}
-
-pub struct Player {
- cache: Arc,
- playing: bool,
- current: Option,
- next: VecDeque,
- arc: Weak>,
- device: Device,
- pub media_controls: MediaControls,
- pub tempfile: NamedTempFile,
-}
-
-impl Player {
- pub fn new(cache: Arc) -> anyhow::Result>> {
- let device = cpal::default_host()
- .default_output_device()
- .ok_or(anyhow::anyhow!("Failed to get default output device"))?;
-
- let media_controls = MediaControls::new(PlatformConfig {
- display_name: "rcmp",
- dbus_name: "rcmp",
- hwnd: None,
- })
- .map_err(|e| anyhow::anyhow!("Failed to create media controls: {:?}", e))?;
-
- let player = Player {
- cache,
- playing: false,
- current: None,
- next: VecDeque::new(),
- device,
- media_controls,
- tempfile: NamedTempFile::new().expect("Failed to create tempfile"),
- arc: Weak::new(),
- };
-
- let arc = Arc::new(Mutex::new(player));
-
- arc.lock().unwrap().arc = Arc::downgrade(&arc);
-
- Ok(arc)
- }
-
- pub fn update_media_controls(
- &mut self,
- song: &Song,
- cover_tempfile: &NamedTempFile,
- ) -> anyhow::Result<()> {
- let [title, album, artist] = [
- StandardTagKey::TrackTitle,
- StandardTagKey::Album,
- StandardTagKey::TrackTitle,
- ]
- .map(|k| song.standard_tags.get(&k).map(|x| x.to_string()));
-
- self.media_controls
- .set_playback(MediaPlayback::Playing { progress: None })
- .map_err(|e| anyhow::anyhow!("Failed to set playback: {:?}", e))?;
-
- self.media_controls
- .set_metadata(MediaMetadata {
- title: title.as_ref().map(|x| x.as_str()),
- album: album.as_ref().map(|x| x.as_str()),
- artist: artist.as_ref().map(|x| x.as_str()),
- cover_url: Some(format!("file://{}", cover_tempfile.path().display()).as_str()),
- duration: None,
- })
- .map_err(|e| anyhow::anyhow!("Failed to set metadata: {:?}", e))?;
-
- Ok(())
- }
-
- pub fn play(&mut self) -> anyhow::Result<()> {
- if let Some(CurrentSong {
- song,
- path,
- metadata,
- stream_command_sender,
- elapsed: duration,
- cover_tempfile,
- }) = self.current.take()
- {
- trace!("play: playing current stream");
-
- stream_command_sender.send(StreamCommand::Play)?;
- trace!("play: sent play command");
-
- self.playing = true;
- self.update_media_controls(&song, &cover_tempfile)?;
-
- self.current = Some(CurrentSong {
- song,
- path,
- metadata,
- stream_command_sender,
- elapsed: duration,
- cover_tempfile,
- });
-
- Ok(())
- } else {
- trace!("play: no current stream, trying to get next");
-
- if let Some(QueuedSong {
- song,
- path,
- metadata,
- signal_spec,
- receiver,
- }) = self.next.pop_front()
- {
- let sender = self.spawn_stream_thread(receiver, &signal_spec, &song);
-
- let mut cover_tempfile = NamedTempFile::new().expect("Failed to create tempfile");
- if let Some(data) = front_cover(&metadata) {
- cover_tempfile.write_all(data).unwrap_or_else(|e| {
- warn!("Failed to write cover to tempfile: {:?}", e);
- });
- }
-
- self.current = Some(CurrentSong {
- song,
- path,
- metadata,
- stream_command_sender: sender,
- elapsed: Duration::from_secs(0),
- cover_tempfile,
- });
-
- self.play()
- } else {
- trace!("play: no next stream");
- Ok(())
- }
- }
- }
-
- fn spawn_stream_thread(
- &mut self,
- receiver: Receiver>,
- signal_spec: &SignalSpec,
- song: &Song,
- ) -> SyncSender {
- let gain_factor = song.gain_factor().unwrap_or(1.0);
- trace!("play: gain_factor: {}", gain_factor);
-
- let (command_tx, command_rx) = std::sync::mpsc::sync_channel::(1);
-
- let buffer_size = signal_spec.rate * 2;
- let stream_config = StreamConfig {
- channels: signal_spec.channels.count() as u16,
- sample_rate: cpal::SampleRate(signal_spec.rate),
- buffer_size: cpal::BufferSize::Fixed(buffer_size),
- };
- trace!("play: stream_config: {:?}", stream_config);
-
- let arc = self.arc.upgrade().expect("Failed to upgrade weak player");
- let arc2 = arc.clone();
- let arc3 = arc.clone();
-
- let signal_spec = signal_spec.clone();
- let thread = std::thread::spawn(move || {
- trace!("locking player");
- let player = arc.lock().expect("Failed to lock player");
- let mut n = 0;
- let mut buf = Vec::new();
-
- let stream = player
- .device
- .build_output_stream(
- &stream_config,
- move |data: &mut [f32], _info: &OutputCallbackInfo| {
- let now = std::time::Instant::now();
- while buf.len() < data.len() {
- match receiver.recv() {
- Ok(s) => buf.extend(s.into_iter().map(|x| x * gain_factor)),
- Err(e) => {
- debug!("Failed to receive sample, sender disconnected {:?}", e);
-
- let duration = Duration::from_secs_f32(
- buffer_size as f32
- / (signal_spec.channels.bits() * signal_spec.rate)
- as f32,
- );
- info!("Sleeping for {:?} to finish song", duration);
- std::thread::sleep(duration);
-
- {
- trace!("locking player");
- let mut player =
- arc2.lock().expect("Failed to lock player");
- player.skip().unwrap_or_else(|e| {
- warn!("Failed to skip song: {:?}", e);
- });
- }
-
- return;
- }
- }
- }
- trace!(
- "receiver.recv() got {} frames, took {:?}",
- buf.len(),
- now.elapsed()
- );
-
- {
- trace!("locking player");
- let mut player = arc2.lock().expect("Failed to lock player");
- n += data.len();
- if let Some(CurrentSong {
- elapsed: ref mut duration,
- ..
- }) = player.current.as_mut()
- {
- *duration = Duration::from_secs_f32(
- n as f32
- / (signal_spec.channels.bits() * signal_spec.rate) as f32,
- );
- }
- }
-
- data.copy_from_slice(buf.drain(0..data.len()).as_slice());
- },
- move |e| match e {
- // TODO: figure out why this happens
- cpal::StreamError::BackendSpecific {
- err: cpal::BackendSpecificError { description },
- } if description == "`alsa::poll()` spuriously returned" => {}
- e => {
- warn!("Output stream error {:?}", e);
- let mut player = arc3.lock().expect("Failed to lock player");
- player.stop().unwrap_or_else(|e| {
- warn!("Failed to stop player: {:?}", e);
- });
- }
- },
- Some(Duration::from_secs_f32(1.0)),
- )
- .expect("Failed to build output stream");
-
- drop(player);
-
- loop {
- trace!(
- "thread {:?} waiting for command",
- std::thread::current().id()
- );
- match command_rx.recv() {
- Ok(s) => match s {
- StreamCommand::Play => stream.play().expect("Failed to play output stream"),
- StreamCommand::Pause => {
- stream.pause().expect("Failed to pause output stream")
- }
- },
- Err(e) => {
- warn!("Failed to receive command, sender disconnected {:?}", e);
- break;
- }
- }
- }
-
- trace!(
- "play: stream thread {:?} exiting",
- std::thread::current().id()
- );
- });
-
- trace!("Spawned stream thead {:?}", thread.thread().id());
-
- command_tx
- }
-
- pub fn stop(&mut self) -> anyhow::Result<()> {
- trace!("stopping player");
- self.current = None;
- self.playing = false;
- self.media_controls
- .set_playback(MediaPlayback::Stopped)
- .map_err(|e| anyhow::anyhow!("Failed to set playback: {:?}", e))?;
-
- Ok(())
- }
-
- pub fn pause(&mut self) -> anyhow::Result<()> {
- trace!("pausing player");
- if let Some(CurrentSong {
- stream_command_sender,
- ..
- }) = self.current.as_ref()
- {
- trace!("pause: pausing stream");
- stream_command_sender.send(StreamCommand::Pause)?;
- self.playing = false;
- self.media_controls
- .set_playback(MediaPlayback::Paused { progress: None })
- .map_err(|e| anyhow::anyhow!("Failed to set playback: {:?}", e))?;
- } else {
- trace!("pause: no stream to pause");
- }
-
- trace!("pause: done");
-
- Ok(())
- }
-
- pub fn queue(&mut self, path: P) -> anyhow::Result<()>
- where
- P: AsRef,
- {
- let song = self
- .cache
- .get(path.as_ref().clone())?
- .ok_or(anyhow::anyhow!(
- "Failed to get song with path {:?}",
- path.as_ref().display()
- ))?
- .as_file()?;
-
- // TODO: adaptively choose buffer size based on signal spec and config
- let (tx, rx) = std::sync::mpsc::sync_channel::>(32);
-
- let mut last = std::time::Instant::now();
- let (metadata, signal_spec, _handle) = decoder::read_audio(&path, move |data| {
- trace!(
- "read_audio: got {} frames, took {:?}",
- data.len(),
- last.elapsed()
- );
- tx.send(data.to_vec())?;
- last = std::time::Instant::now();
- Ok(())
- })?;
-
- self.next.push_back(QueuedSong {
- song: song.clone(),
- path: path.as_ref().to_path_buf(),
- metadata,
- signal_spec,
- receiver: rx,
- });
-
- if self.current.is_none() {
- trace!("queue: playing queued song");
- self.play()
- } else {
- Ok(())
- }
- }
-
- pub fn play_pause(&mut self) -> anyhow::Result<()> {
- match self.playing {
- true => self.pause(),
- false => self.play(),
- }
- }
-
- pub fn clear(&mut self) -> anyhow::Result<()> {
- trace!("clearing queue");
- self.next.clear();
- self.stop()
- }
-
- pub fn skip(&mut self) -> anyhow::Result<()> {
- trace!("skipping song");
- trace!("next len: {:?}", self.next.len());
- self.stop()?;
-
- trace!("stopped");
- trace!("next len: {:?}", self.next.len());
- self.play()?;
-
- trace!("played");
- trace!("next len: {:?}", self.next.len());
-
- Ok(())
- }
-
- pub fn current(&self) -> Option<(&Song, &Path)> {
- self.current
- .as_ref()
- .map(|CurrentSong { ref song, path, .. }| (song, path.as_path()))
- }
-
- pub fn current_time(&self) -> Option<&Duration> {
- self.current.as_ref().map(
- |CurrentSong {
- elapsed: ref duration,
- ..
- }| duration,
- )
- }
-
- pub fn current_cover(&self) -> Option<&[u8]> {
- self.current
- .as_ref()
- .and_then(|cs| front_cover(&cs.metadata))
- }
-
- pub fn nexts(&self) -> impl Iterator- {
- self.next.iter().map(|QueuedSong { ref song, .. }| song)
- }
-
- pub fn attach_media_controls(&mut self) -> anyhow::Result<()> {
- let weak = self.arc.clone();
- self.media_controls
- .attach(move |event: MediaControlEvent| {
- trace!("media control event {:?}", event);
-
- let arc = weak.upgrade().expect("Failed to upgrade weak player");
- let mut player = arc.lock().expect("Failed to lock player");
-
- match event {
- MediaControlEvent::Play => player.play(),
- MediaControlEvent::Pause => player.pause(),
- MediaControlEvent::Toggle => player.play_pause(),
- MediaControlEvent::Next => player.skip(),
- MediaControlEvent::Previous => Ok(()),
- MediaControlEvent::Stop => player.stop(),
- MediaControlEvent::Seek(_) => todo!(),
- MediaControlEvent::SeekBy(_, _) => todo!(),
- MediaControlEvent::SetPosition(_) => todo!(),
- MediaControlEvent::OpenUri(_) => Ok(()),
- MediaControlEvent::Raise => Ok(()),
- MediaControlEvent::Quit => Ok(()),
- }
- .unwrap_or_else(|e| {
- error!("Failed to handle media control event: {:?}", e);
- });
- })
- .map_err(|e| anyhow::anyhow!("Failed to attach media controls: {:?}", e))?;
-
- Ok(())
- }
-}
diff --git a/src/player/command.rs b/src/player/command.rs
new file mode 100644
index 0000000..616d138
--- /dev/null
+++ b/src/player/command.rs
@@ -0,0 +1,10 @@
+pub enum Command {
+ Play,
+ Pause,
+ PlayPause,
+ Skip,
+ Stop,
+ Clear,
+ Enqueue(Box),
+ Dequeue(usize),
+}
diff --git a/src/player/facade.rs b/src/player/facade.rs
new file mode 100644
index 0000000..46c9470
--- /dev/null
+++ b/src/player/facade.rs
@@ -0,0 +1,86 @@
+use std::{
+ sync::{atomic::AtomicBool, Arc, RwLock},
+ time::Duration,
+};
+
+use symphonia::core::meta::{MetadataRevision, StandardVisualKey};
+
+use crate::song::Song;
+
+use super::Player;
+
+#[derive(Default)]
+pub enum PlayerStatus {
+ PlayingOrPaused {
+ song: Song,
+ metadata: Option,
+ playing_duration: Arc>,
+ paused: Arc,
+ },
+ #[default]
+ Stopped,
+}
+
+impl PlayerStatus {
+ fn from_internal(player: &Player) -> PlayerStatus {
+ match &player.status {
+ super::InternalPlayerStatus::PlayingOrPaused {
+ song,
+ metadata,
+ playing_duration,
+ stream_paused,
+ ..
+ } => PlayerStatus::PlayingOrPaused {
+ song: song.clone(),
+ metadata: metadata.clone(),
+ playing_duration: playing_duration.clone(),
+ paused: stream_paused.clone(),
+ },
+ super::InternalPlayerStatus::Stopped => PlayerStatus::Stopped,
+ }
+ }
+}
+
+#[derive(Default)]
+pub struct PlayerFacade {
+ pub status: PlayerStatus,
+ pub queue: Box<[Box]>,
+}
+
+impl PlayerFacade {
+ pub(super) fn from_player(player: &Player) -> PlayerFacade {
+ PlayerFacade {
+ status: PlayerStatus::from_internal(player),
+ queue: player.queue.clone().into_iter().collect(),
+ }
+ }
+
+ pub fn current_song(&self) -> Option<&Song> {
+ match &self.status {
+ PlayerStatus::PlayingOrPaused { song, .. } => Some(song),
+ _ => None,
+ }
+ }
+
+ pub fn playing_duration(&self) -> Option {
+ match &self.status {
+ PlayerStatus::PlayingOrPaused {
+ playing_duration, ..
+ } => Some(*playing_duration.read().unwrap()),
+ _ => None,
+ }
+ }
+
+ pub fn current_cover(&self) -> Option<&[u8]> {
+ match &self.status {
+ PlayerStatus::PlayingOrPaused { metadata, .. } => metadata.as_ref(),
+ PlayerStatus::Stopped => None,
+ }
+ .and_then(|m| {
+ m.visuals()
+ .iter()
+ .find(|v| v.usage == Some(StandardVisualKey::FrontCover))
+ })
+ .map(|v| v.data.as_ref())
+ }
+}
diff --git a/src/player/loader.rs b/src/player/loader.rs
new file mode 100644
index 0000000..8d99bd4
--- /dev/null
+++ b/src/player/loader.rs
@@ -0,0 +1,97 @@
+use anyhow::Context;
+
+use symphonia::core::{
+ audio::{SampleBuffer, SignalSpec},
+ codecs::{DecoderOptions, CODEC_TYPE_NULL},
+ errors::Error,
+ formats::FormatOptions,
+ io::{MediaSourceStream, MediaSourceStreamOptions},
+ meta::{MetadataOptions, MetadataRevision},
+ probe::Hint,
+};
+
+use crate::song::Song;
+
+pub struct LoadedSong {
+ pub song: Song,
+ pub metadata: Option,
+ pub signal_spec: SignalSpec,
+ pub decoder: Box anyhow::Result<(Option>, bool)> + Send>,
+}
+
+impl LoadedSong {
+ pub fn load(song: Song) -> anyhow::Result {
+ let src = std::fs::File::open(song.path.as_ref()).context(format!(
+ "Failed to open file {}",
+ song.path.to_string_lossy()
+ ))?;
+
+ let mss = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default());
+ let mut probed = symphonia::default::get_probe().format(
+ &Hint::new(),
+ mss,
+ &FormatOptions::default(),
+ &MetadataOptions::default(),
+ )?;
+
+ let metadata = {
+ let mut meta = probed.format.metadata();
+ meta.skip_to_latest().cloned()
+ };
+
+ let mut format_reader = probed.format;
+
+ let track = format_reader
+ .tracks()
+ .into_iter()
+ .find(|t| t.codec_params.codec != CODEC_TYPE_NULL)
+ .ok_or(anyhow::anyhow!("No audio tracks found"))?;
+
+ let codec_params = track.codec_params.clone();
+ let track_id = track.id;
+
+ let mut decoder = symphonia::default::get_codecs()
+ .make(&track.codec_params, &DecoderOptions::default())?;
+
+ let signal_spec = SignalSpec::new(
+ codec_params
+ .sample_rate
+ .ok_or(anyhow::anyhow!("No sample rate"))?,
+ codec_params
+ .channels
+ .ok_or(anyhow::anyhow!("No channels"))?,
+ );
+
+ let signal_spec2 = signal_spec.clone();
+ let decoder = move || match format_reader.next_packet() {
+ Ok(packet) => {
+ if packet.track_id() == track_id {
+ let data = match decoder.decode(&packet) {
+ Ok(d) => d,
+ Err(e) => {
+ anyhow::bail!("Failed to decode packet {:?}", e);
+ }
+ };
+
+ let mut sample_buffer = SampleBuffer::new(data.capacity() as u64, signal_spec2);
+ sample_buffer.copy_interleaved_ref(data);
+
+ Ok((Some(sample_buffer), false))
+ } else {
+ Ok((None, false))
+ }
+ }
+ Err(Error::IoError(e)) if e.to_string() == "end of stream" => Ok((None, true)),
+ Err(e) => {
+ anyhow::bail!("Failed to read packet {:?}", e);
+ }
+ };
+
+ Ok(Self {
+ song,
+ metadata,
+ signal_spec,
+ decoder: Box::new(decoder),
+ })
+ }
+}
diff --git a/src/player/mod.rs b/src/player/mod.rs
new file mode 100644
index 0000000..314d811
--- /dev/null
+++ b/src/player/mod.rs
@@ -0,0 +1,381 @@
+use crate::{
+ cache::Cache,
+ song::{Song, StandardTagKey},
+};
+use anyhow::Context;
+use cpal::{
+ traits::{DeviceTrait, HostTrait},
+ Stream, StreamConfig,
+};
+use log::warn;
+use souvlaki::{MediaControls, MediaMetadata, MediaPlayback, MediaPosition, PlatformConfig};
+use std::{
+ collections::VecDeque,
+ io::{Seek, Write},
+ sync::{atomic::AtomicBool, mpsc, Arc, RwLock},
+ time::Duration,
+};
+use symphonia::core::meta::MetadataRevision;
+use tempfile::NamedTempFile;
+
+use self::{command::Command, facade::PlayerFacade, loader::LoadedSong};
+
+pub mod command;
+pub mod facade;
+mod loader;
+
+enum InternalPlayerStatus {
+ PlayingOrPaused {
+ song: Song,
+ metadata: Option,
+ playing_duration: Arc>,
+ stream_paused: Arc,
+ _stream: Stream,
+ },
+ Stopped,
+}
+
+pub struct Player {
+ cache: Arc,
+ status: InternalPlayerStatus,
+ queue: VecDeque>,
+ media_controls: MediaControls,
+ command_tx: mpsc::Sender,
+}
+
+impl Player {
+ /// command player to continue playing or start playing the next song
+ fn play(&mut self) -> anyhow::Result<()> {
+ match &self.status {
+ InternalPlayerStatus::PlayingOrPaused {
+ stream_paused: paused,
+ ..
+ } => {
+ if paused.load(std::sync::atomic::Ordering::Relaxed) {
+ paused.store(false, std::sync::atomic::Ordering::Relaxed);
+ }
+ }
+ InternalPlayerStatus::Stopped => {}
+ }
+
+ if matches!(self.status, InternalPlayerStatus::Stopped) {
+ if let Some(path) = self.queue.pop_front() {
+ let song = self
+ .cache
+ .get(path)
+ .context("Failed to get song from cache")?
+ .ok_or(anyhow::anyhow!("Song not found in cache"))?
+ .as_file()
+ .context("Song is not a file")?
+ .clone();
+
+ let loaded_song = LoadedSong::load(song.clone()).context("Failed to load song")?;
+
+ let metadata = loaded_song.metadata.clone();
+ let (stream_paused, playing_duration, _stream) =
+ self.create_playback(loaded_song)?;
+
+ self.status = InternalPlayerStatus::PlayingOrPaused {
+ song,
+ metadata,
+ playing_duration,
+ stream_paused,
+ _stream,
+ }
+ }
+ }
+
+ Ok(())
+ }
+
+ /// command player to pause
+ fn pause(&mut self) -> anyhow::Result<()> {
+ match &self.status {
+ InternalPlayerStatus::PlayingOrPaused {
+ stream_paused: paused,
+ ..
+ } => {
+ paused.store(true, std::sync::atomic::Ordering::Relaxed);
+ }
+ InternalPlayerStatus::Stopped => {}
+ }
+
+ Ok(())
+ }
+
+ /// command player to play if paused or pause if playing
+ fn play_pause(&mut self) -> anyhow::Result<()> {
+ match &self.status {
+ InternalPlayerStatus::PlayingOrPaused {
+ stream_paused: paused,
+ ..
+ } => {
+ paused.fetch_xor(true, std::sync::atomic::Ordering::Relaxed);
+ }
+ InternalPlayerStatus::Stopped => {}
+ }
+
+ Ok(())
+ }
+
+ /// command player to stop
+ fn stop(&mut self) -> anyhow::Result<()> {
+ self.status = InternalPlayerStatus::Stopped;
+
+ Ok(())
+ }
+
+ /// command player to skip to next song
+ fn skip(&mut self) -> anyhow::Result<()> {
+ self.stop()?;
+ self.play()?;
+
+ Ok(())
+ }
+
+ /// add a song to the queue
+ /// if the player is stopped, the song will be played
+ fn enqueue>(&mut self, path: P) -> anyhow::Result<()> {
+ self.queue.push_back(path.as_ref().into());
+
+ if matches!(self.status, InternalPlayerStatus::Stopped) {
+ self.play()?;
+ }
+
+ Ok(())
+ }
+
+ /// remove a song from the queue
+ fn dequeue(&mut self, index: usize) -> anyhow::Result<()> {
+ self.queue
+ .remove(index)
+ .ok_or(anyhow::anyhow!(format!("No song at index {}", index)))?;
+
+ Ok(())
+ }
+
+ /// remove all songs from the queue and stop playing
+ fn clear(&mut self) -> anyhow::Result<()> {
+ self.queue.clear();
+ self.stop()?;
+
+ Ok(())
+ }
+
+ /// create playback stream for song
+ fn create_playback(
+ &mut self,
+ mut song: LoadedSong,
+ ) -> anyhow::Result<(Arc, Arc>, Stream)> {
+ let config = StreamConfig {
+ channels: song.signal_spec.channels.count() as u16,
+ sample_rate: cpal::SampleRate(song.signal_spec.rate),
+ buffer_size: cpal::BufferSize::Default,
+ };
+
+ let mut buffer = VecDeque::::new();
+
+ let pause_stream = Arc::new(AtomicBool::new(false));
+ let playing_duration = Arc::new(RwLock::new(Duration::from_secs(0)));
+
+ let gain_factor = song.song.gain_factor;
+ let pause_stream2 = pause_stream.clone();
+ let playing_duration2 = playing_duration.clone();
+ let command_tx = self.command_tx.clone();
+
+ let stream = cpal::default_host()
+ .default_output_device()
+ .expect("Failed to get default output device")
+ .build_output_stream::(
+ &config,
+ move |dest, _info| {
+ if pause_stream2.load(std::sync::atomic::Ordering::Relaxed) {
+ dest.fill(0.0);
+ return;
+ }
+
+ let mut duration = playing_duration2.write().unwrap();
+
+ let mut n = buffer.len().min(dest.len());
+ buffer.drain(0..n).enumerate().for_each(|(i, s)| {
+ dest[i] = s * gain_factor;
+ });
+
+ *duration += Duration::from_secs_f64(
+ n as f64
+ / song.signal_spec.rate as f64
+ / song.signal_spec.channels.count() as f64,
+ );
+
+ let (samples, eof) = (song.decoder)().expect("Failed to decode packet");
+
+ if let Some(samples) = samples {
+ buffer.extend(samples.samples());
+
+ if n < dest.len() {
+ n = buffer.len().min(dest.len() - n);
+ buffer.drain(0..n).enumerate().for_each(|(i, s)| {
+ dest[i] = s * gain_factor;
+ });
+
+ *duration += Duration::from_secs_f64(
+ n as f64
+ / song.signal_spec.rate as f64
+ / song.signal_spec.channels.count() as f64,
+ );
+ }
+ }
+
+ if eof && buffer.is_empty() {
+ command_tx.send(Command::Skip).unwrap();
+ }
+ },
+ |e| {
+ warn!("Error in playback stream: {:?}", e);
+ },
+ None,
+ )
+ .expect("Failed to build output stream");
+
+ Ok((pause_stream, playing_duration, stream))
+ }
+
+ pub fn run(
+ cache: Arc,
+ ) -> anyhow::Result<(mpsc::Sender, Arc>)> {
+ let media_controls = MediaControls::new(PlatformConfig {
+ display_name: "rcmp",
+ dbus_name: "rcmp",
+ hwnd: None,
+ })
+ .map_err(|e| anyhow::anyhow!(format!("{:?}", e)))
+ .context("Failed to create media controls")?;
+
+ let (tx, rx) = mpsc::channel();
+ let facade = Arc::new(RwLock::new(PlayerFacade::default()));
+
+ let tx2 = tx.clone();
+ let facade2 = facade.clone();
+ std::thread::Builder::new()
+ .name("player thread".to_string())
+ .spawn(move || {
+ let mut player = Player {
+ cache,
+ status: InternalPlayerStatus::Stopped,
+ queue: VecDeque::new(),
+ media_controls,
+ command_tx: tx2.clone(),
+ };
+
+ let tx = tx2.clone();
+ player
+ .media_controls
+ .attach(move |event| match event {
+ souvlaki::MediaControlEvent::Play => {
+ tx.send(Command::Play).unwrap();
+ }
+ souvlaki::MediaControlEvent::Pause => {
+ tx.send(Command::Pause).unwrap();
+ }
+ souvlaki::MediaControlEvent::Toggle => {
+ tx.send(Command::PlayPause).unwrap();
+ }
+ souvlaki::MediaControlEvent::Next => {
+ tx.send(Command::Skip).unwrap();
+ }
+ souvlaki::MediaControlEvent::Previous => warn!("Previous not implemented"),
+ souvlaki::MediaControlEvent::Stop => {
+ tx.send(Command::Stop).unwrap();
+ }
+ souvlaki::MediaControlEvent::Seek(dir) => {
+ warn!("Seek {dir:?} not implemented")
+ }
+ souvlaki::MediaControlEvent::SeekBy(dir, dur) => {
+ warn!("SeekBy {dir:?} {dur:?} not implemented")
+ }
+ souvlaki::MediaControlEvent::SetPosition(mp) => {
+ warn!("SetPosition {mp:?} not implemented")
+ }
+ souvlaki::MediaControlEvent::OpenUri(uri) => {
+ warn!("OpenUri {uri:?} not implemented")
+ }
+ souvlaki::MediaControlEvent::Raise => {}
+ souvlaki::MediaControlEvent::Quit => {
+ warn!("Quit not implemented")
+ }
+ })
+ .expect("Failed to attach media controls");
+
+ let mut cover_tempfile;
+ loop {
+ match rx.recv().expect("Failed to receive Command") {
+ Command::Play => player.play().unwrap(),
+ Command::Pause => player.pause().unwrap(),
+ Command::PlayPause => player.play_pause().unwrap(),
+ Command::Skip => player.skip().unwrap(),
+ Command::Stop => player.stop().unwrap(),
+ Command::Clear => player.clear().unwrap(),
+ Command::Enqueue(path) => player.enqueue(path).unwrap(),
+ Command::Dequeue(index) => player.dequeue(index).unwrap(),
+ }
+
+ *facade2.write().unwrap() = PlayerFacade::from_player(&player);
+
+ let facade = facade2.read().unwrap();
+
+ cover_tempfile = NamedTempFile::new().expect("Failed to create tempfile");
+ cover_tempfile
+ .write_all(facade.current_cover().unwrap_or(&[]))
+ .expect("Failed to write cover to tempfile");
+
+ player
+ .media_controls
+ .set_metadata(MediaMetadata {
+ title: facade
+ .current_song()
+ .and_then(|s| s.tag_string(StandardTagKey::TrackTitle)),
+ album: facade
+ .current_song()
+ .and_then(|s| s.tag_string(StandardTagKey::Album)),
+ artist: facade
+ .current_song()
+ .and_then(|s| s.tag_string(StandardTagKey::Artist)),
+ cover_url: Some(
+ format!("file://{}", cover_tempfile.path().display()).as_str(),
+ ),
+ duration: facade.current_song().map(|s| s.duration),
+ })
+ .expect("Failed to set metadata");
+
+ player
+ .media_controls
+ .set_playback(match &facade.status {
+ facade::PlayerStatus::PlayingOrPaused {
+ playing_duration,
+ paused,
+ ..
+ } => {
+ if paused.load(std::sync::atomic::Ordering::Relaxed) {
+ MediaPlayback::Paused {
+ progress: Some(MediaPosition(
+ *playing_duration.read().unwrap(),
+ )),
+ }
+ } else {
+ MediaPlayback::Playing {
+ progress: Some(MediaPosition(
+ *playing_duration.read().unwrap(),
+ )),
+ }
+ }
+ }
+ facade::PlayerStatus::Stopped => MediaPlayback::Stopped,
+ })
+ .expect("Failed to set playback");
+ }
+ })
+ .context("Failed to create player thread")?;
+
+ Ok((tx, facade))
+ }
+}
diff --git a/src/song.rs b/src/song.rs
index 50ca6cf..8b37ec3 100644
--- a/src/song.rs
+++ b/src/song.rs
@@ -1,5 +1,15 @@
use std::{collections::HashMap, fmt::Debug, num::NonZeroU32, time::Duration};
+use anyhow::Context;
+use log::warn;
+use symphonia::core::{
+ codecs,
+ formats::FormatOptions,
+ io::{MediaSourceStream, MediaSourceStreamOptions},
+ meta::MetadataOptions,
+ probe::Hint,
+};
+
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub enum Value {
Binary(Box<[u8]>),
@@ -467,18 +477,125 @@ impl From for StandardTagKey {
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Song {
+ pub path: Box,
+ pub duration: Duration,
+ pub gain_factor: f32,
pub standard_tags: HashMap,
pub other_tags: HashMap,
- pub duration: Duration,
}
impl Song {
- pub fn gain_factor(&self) -> Option {
- self.standard_tags
+ pub fn tag_string(&self, key: StandardTagKey) -> Option<&str> {
+ self.standard_tags.get(&key).and_then(|v| match v {
+ Value::String(s) => Some(s.as_str()),
+ _ => None,
+ })
+ }
+
+ pub fn load>(path: P) -> anyhow::Result {
+ let src = std::fs::File::open(&path)
+ .context(format!("Failed to open file {}", path.as_ref().display()))?;
+
+ let source = MediaSourceStream::new(Box::new(src), MediaSourceStreamOptions::default());
+
+ let extension = path
+ .as_ref()
+ .extension()
+ .unwrap()
+ .to_str()
+ .ok_or(anyhow::anyhow!(
+ "Failed to get extension for file {}",
+ path.as_ref().display()
+ ))?;
+
+ let mut probed = symphonia::default::get_probe().format(
+ &Hint::new().with_extension(extension),
+ source,
+ &FormatOptions {
+ prebuild_seek_index: false,
+ seek_index_fill_rate: 0,
+ enable_gapless: true,
+ },
+ &MetadataOptions::default(),
+ )?;
+
+ let mut metadata = probed.format.metadata();
+ let metadata = metadata.skip_to_latest().cloned();
+
+ let track = probed
+ .format
+ .tracks()
+ .into_iter()
+ .find(|t| t.codec_params.codec != codecs::CODEC_TYPE_NULL)
+ .ok_or(anyhow::anyhow!("No audio tracks found"))?;
+
+ let duration = track
+ .codec_params
+ .time_base
+ .ok_or(anyhow::anyhow!(
+ "No time base found for track {:?}",
+ track.id
+ ))?
+ .calc_time(track.codec_params.n_frames.ok_or(anyhow::anyhow!(
+ "No frame count found for track {:?}",
+ track.id
+ ))?);
+
+ let duration = std::time::Duration::from_secs_f64(duration.seconds as f64 + duration.frac);
+
+ let (standard_tags, other_tags) = metadata
+ .map(|m| {
+ let s = m
+ .tags()
+ .into_iter()
+ .filter_map(|t| t.std_key.map(|k| (k.into(), t.value.clone().into())))
+ .collect::>();
+
+ let o = m
+ .tags()
+ .into_iter()
+ .filter(|t| t.std_key == None)
+ .map(|t| (t.key.clone(), t.value.clone().into()))
+ .collect::>();
+
+ (s, o)
+ })
+ .unwrap_or_default();
+
+ let replay_gain = standard_tags
.get(&StandardTagKey::ReplayGainTrackGain)
- .map(|x| x.to_string())
- .and_then(|x| x.strip_suffix(" dB").map(|x| x.to_string()))
- .and_then(|x| x.parse::().ok())
- .map(|x| 10.0f32.powf(x / 20.0))
+ .ok_or(anyhow::anyhow!(
+ "No replay gain found for {}",
+ path.as_ref().display()
+ ))
+ .and_then(|v| match v {
+ Value::String(s) => {
+ s.strip_suffix(" dB")
+ .unwrap_or(s)
+ .parse::()
+ .context(format!(
+ "Failed to parse replay gain for {}",
+ path.as_ref().display()
+ ))
+ }
+ v => anyhow::bail!("Expected string, got {:?}", v),
+ })
+ .map(|x| 10_f32.powf(x / 20.0))
+ .unwrap_or_else(|e| {
+ warn!(
+ "Failed to get replay gain for {}: {}",
+ path.as_ref().display(),
+ e
+ );
+ 1.0
+ });
+
+ Ok(Song {
+ path: path.as_ref().into(),
+ duration,
+ standard_tags,
+ other_tags,
+ gain_factor: replay_gain,
+ })
}
}
diff --git a/src/tui/fancy.rs b/src/tui/fancy.rs
index fd9f1af..dc3ef05 100644
--- a/src/tui/fancy.rs
+++ b/src/tui/fancy.rs
@@ -1,4 +1,4 @@
-use std::sync::{Arc, Mutex};
+use std::sync::{Arc, RwLock};
use crossterm::event::Event;
use image::imageops::FilterType;
@@ -11,16 +11,16 @@ use ratatui::{
Frame,
};
-use crate::player::Player;
+use crate::player::facade::PlayerFacade;
use super::Tui;
pub struct Fancy {
- player: Arc>,
+ player: Arc>,
}
impl Fancy {
- pub fn new(player: Arc>) -> Self {
+ pub fn new(player: Arc>) -> Self {
Self { player }
}
}
@@ -28,12 +28,12 @@ impl Fancy {
impl Tui for Fancy {
fn draw(&self, area: Rect, f: &mut Frame) -> anyhow::Result<()> {
trace!("locking player");
- let player = self.player.lock().expect("Failed to lock player");
+ let player = self.player.read().expect("Failed to lock player");
let standard_tags = Table::new(
player
- .current()
- .map(|(s, _)| {
+ .current_song()
+ .map(|s| {
s.standard_tags
.iter()
.map(|(k, v)| (format!("{:?}", k), v))
@@ -54,10 +54,12 @@ impl Tui for Fancy {
.title(format!(
" {} ",
player
- .current()
- .map(|(_, p)| {
- p.to_str()
- .ok_or(anyhow::anyhow!("Failed to convert Path to str: {:?}", p))
+ .current_song()
+ .map(|s| {
+ s.path.to_str().ok_or(anyhow::anyhow!(
+ "Failed to convert Path to str: {:?}",
+ s.path
+ ))
})
.unwrap_or(Ok(""))?,
))
diff --git a/src/tui/files.rs b/src/tui/files.rs
index 3e990ce..4cc66a8 100644
--- a/src/tui/files.rs
+++ b/src/tui/files.rs
@@ -1,7 +1,7 @@
use std::{
cmp::Ordering,
path::PathBuf,
- sync::{Arc, Mutex},
+ sync::{mpsc, Arc},
};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
@@ -17,7 +17,7 @@ use ratatui::{
use crate::{
cache::{Cache, CacheEntry},
- player::Player,
+ player::command::Command,
song::StandardTagKey,
tui::song_table,
};
@@ -34,12 +34,12 @@ pub struct Files {
cache: Arc,
path: PathBuf,
selected: Vec,
- player: Arc>,
+ player_tx: mpsc::Sender,
filter: FilterState,
}
impl Files {
- pub fn new(cache: Arc, player: Arc>) -> Self {
+ pub fn new(cache: Arc, cmd: mpsc::Sender) -> Self {
Self {
path: std::path::Path::new("/")
.canonicalize()
@@ -54,7 +54,7 @@ impl Files {
.collect(),
selected: vec![0],
cache,
- player,
+ player_tx: cmd,
filter: FilterState::Disabled,
}
}
@@ -64,9 +64,6 @@ impl Files {
let l = self.items()?.count();
- trace!("lock player");
- let mut player = self.player.lock().expect("Failed to lock player");
-
if let Event::Key(KeyEvent {
code, modifiers, ..
}) = event
@@ -79,11 +76,25 @@ impl Files {
};
}
KeyCode::Char(' ') => {
- player.play_pause().expect("Failed to play/pause");
+ self.player_tx
+ .send(Command::PlayPause)
+ .expect("Failed to send play/pause");
+ }
+ KeyCode::Char('n') => {
+ self.player_tx
+ .send(Command::Skip)
+ .expect("Failed to send skip");
+ }
+ KeyCode::Char('s') => {
+ self.player_tx
+ .send(Command::Stop)
+ .expect("Failed to send stop");
+ }
+ KeyCode::Char('c') => {
+ self.player_tx
+ .send(Command::Clear)
+ .expect("Failed to send clear");
}
- KeyCode::Char('n') => player.skip().expect("Failed to skip"),
- KeyCode::Char('s') => player.stop().expect("Failed to stop"),
- KeyCode::Char('c') => player.clear().expect("Failed to clear"),
KeyCode::Up => {
self.selected
.last_mut()
@@ -117,7 +128,9 @@ impl Files {
match c {
CacheEntry::File { .. } => {
trace!("queueing song: {:?}", self.path);
- player.queue(&self.path.join(f)).expect("Failed to queue");
+ self.player_tx
+ .send(Command::Enqueue(self.path.join(f).as_path().into()))
+ .unwrap();
}
CacheEntry::Directory { .. } => {
self.path.push(f.clone());
diff --git a/src/tui/mod.rs b/src/tui/mod.rs
index 4d07c9d..b2e9c6b 100644
--- a/src/tui/mod.rs
+++ b/src/tui/mod.rs
@@ -7,7 +7,7 @@ mod status;
mod tabs;
use std::{
- sync::{Arc, Mutex},
+ sync::{mpsc, Arc, Mutex, RwLock},
time::Duration,
};
@@ -22,7 +22,11 @@ use ratatui::{
Frame, Terminal,
};
-use crate::{cache::Cache, config::Config, player::Player};
+use crate::{
+ cache::Cache,
+ config::Config,
+ player::{command::Command, facade::PlayerFacade},
+};
use self::{fancy::Fancy, files::Files, queue::Queue, search::Search, status::Status, tabs::Tabs};
@@ -48,7 +52,8 @@ pub trait Tui {
pub fn tui<'a>(
_config: Arc,
cache: Arc,
- player: Arc>,
+ cmd: mpsc::Sender,
+ player: Arc>,
) -> anyhow::Result<()> {
let stdout = std::io::stdout();
let backend = CrosstermBackend::new(stdout);
@@ -62,12 +67,15 @@ pub fn tui<'a>(
vec![
(
" Files 🗃️ ",
- Box::new(Files::new(cache.clone(), player.clone())),
+ Box::new(Files::new(cache.clone(), cmd.clone())),
+ ),
+ (
+ "Queue 🕰️ ",
+ Box::new(Queue::new(cache.clone(), player.clone())),
),
- ("Queue 🕰️ ", Box::new(Queue::new(player.clone()))),
(
- "Search 🔎", /* idk, whatever */
- Box::new(Search::new(cache.clone(), player.clone())),
+ "Search 🔎",
+ Box::new(Search::new(cache.clone(), cmd.clone())),
),
("Fancy stuff ✨ ", Box::new(Fancy::new(player.clone()))),
],
diff --git a/src/tui/queue.rs b/src/tui/queue.rs
index c60c5aa..8a83167 100644
--- a/src/tui/queue.rs
+++ b/src/tui/queue.rs
@@ -1,4 +1,4 @@
-use std::sync::{Arc, Mutex};
+use std::sync::{Arc, RwLock};
use crossterm::event::Event;
use log::trace;
@@ -8,17 +8,18 @@ use ratatui::{
widgets::Table,
};
-use crate::{player::Player, tui::song_table};
+use crate::{cache::Cache, player::facade::PlayerFacade, tui::song_table};
use super::Tui;
pub struct Queue {
- player: Arc>,
+ cache: Arc,
+ player: Arc>,
}
impl Queue {
- pub fn new(player: Arc>) -> Self {
- Queue { player }
+ pub fn new(cache: Arc, player: Arc>) -> Self {
+ Queue { cache, player }
}
}
@@ -27,10 +28,12 @@ impl Tui for Queue {
trace!("drawing queue");
trace!("lock player");
- let player = self.player.lock().unwrap();
+ let player = self.player.read().unwrap();
let items = player
- .nexts()
+ .queue
+ .iter()
+ .map(|p| self.cache.get(p).unwrap().unwrap().as_file().unwrap())
.map(|s| song_table::song_row(s))
.collect::>();
diff --git a/src/tui/search.rs b/src/tui/search.rs
index 11b2ab0..9a74c7a 100644
--- a/src/tui/search.rs
+++ b/src/tui/search.rs
@@ -1,6 +1,6 @@
use std::{
path::PathBuf,
- sync::{Arc, Mutex},
+ sync::{mpsc, Arc},
};
use crossterm::event::{Event, KeyCode, KeyEvent};
@@ -17,7 +17,7 @@ use strsim::jaro_winkler;
use crate::{
cache::{Cache, CacheEntry},
- player::Player,
+ player::command::Command,
song::{Song, StandardTagKey},
};
@@ -27,17 +27,17 @@ pub struct Search {
keyword: String,
cache: Arc,
selected: usize,
- player: Arc>,
+ cmd: mpsc::Sender,
items: Vec<(Song, PathBuf)>,
}
impl Search {
- pub fn new(cache: Arc, player: Arc>) -> Self {
+ pub fn new(cache: Arc, cmd: mpsc::Sender) -> Self {
Self {
keyword: String::new(),
cache,
selected: 0,
- player,
+ cmd,
items: vec![],
}
}
@@ -178,11 +178,7 @@ impl Tui for Search {
.ok_or(anyhow::anyhow!("Failed to get selected Song"))?
.clone();
- self.player
- .lock()
- .map_err(|e| anyhow::anyhow!("Failed to lock player: {:?}", e))?
- .queue(path)
- .expect("Failed to queue song");
+ self.cmd.send(Command::Enqueue(path.as_path().into()))?;
}
_ => {}
},
diff --git a/src/tui/status.rs b/src/tui/status.rs
index c6d0d1b..efddcdb 100644
--- a/src/tui/status.rs
+++ b/src/tui/status.rs
@@ -1,6 +1,4 @@
-use std::{
- sync::{Arc, Mutex},
-};
+use std::sync::{Arc, RwLock};
use itertools::Itertools;
use log::trace;
@@ -12,16 +10,16 @@ use ratatui::{
Frame,
};
-use crate::{player::Player, song::StandardTagKey, tui::format_duration};
+use crate::{player::facade::PlayerFacade, song::StandardTagKey, tui::format_duration};
use super::{Tui, UNKNOWN_STRING};
pub struct Status {
- player: Arc>,
+ player: Arc>,
}
impl Status {
- pub fn new(player: Arc>) -> Self {
+ pub fn new(player: Arc>) -> Self {
Self { player }
}
}
@@ -38,12 +36,13 @@ impl Tui for Status {
trace!("locking player");
let playing = Paragraph::new(
- if let Some((song, path)) = self.player.lock().unwrap().current() {
+ if let Some(song) = self.player.read().unwrap().current_song() {
let title = song
.standard_tags
.get(&StandardTagKey::TrackTitle)
.map(|s| s.to_string())
- .or(path
+ .or(song
+ .path
.components()
.last()
.map(|s| s.as_os_str().to_string_lossy().to_string()))
@@ -84,9 +83,9 @@ impl Tui for Status {
.alignment(ratatui::prelude::Alignment::Center);
trace!("locking player");
- let player = self.player.lock().unwrap();
- let ratio = if let (Some((song, _)), Some(current_time)) =
- (player.current(), player.current_time())
+ let player = self.player.read().unwrap();
+ let ratio = if let (Some(song), Some(current_time)) =
+ (player.current_song(), player.playing_duration())
{
current_time.as_secs_f64() / song.duration.as_secs_f64()
} else {
@@ -100,17 +99,17 @@ impl Tui for Status {
.label("")
.gauge_style(Style::default().fg(Color::LightBlue).bg(Color::DarkGray));
let elapsed = format_duration(
- *player
- .current_time()
- .unwrap_or(&std::time::Duration::from_secs(0)),
+ player
+ .playing_duration()
+ .unwrap_or(std::time::Duration::from_secs(0)),
);
let duration = format!(
" -{}",
format_duration(
- if let (Some((current, _)), Some(current_time)) =
- (player.current(), player.current_time())
+ if let (Some(song), Some(current_time)) =
+ (player.current_song(), player.playing_duration())
{
- current.duration.saturating_sub(*current_time)
+ song.duration.saturating_sub(current_time)
} else {
std::time::Duration::from_secs(0)
},
diff --git a/src/tui/tabs.rs b/src/tui/tabs.rs
index 585c3cd..a2b6531 100644
--- a/src/tui/tabs.rs
+++ b/src/tui/tabs.rs
@@ -1,4 +1,4 @@
-use std::{sync::Mutex};
+use std::sync::Mutex;
use crossterm::event::{Event, KeyCode, KeyEvent};
use log::trace;