From 40b9b86c922bac7b3113ae7c2efd1ac2ddf4d4d0 Mon Sep 17 00:00:00 2001 From: Jika Date: Wed, 26 Mar 2025 23:02:43 +0100 Subject: [PATCH] Reworked Queue widget to use external state Update plyaer inner working and added agc Updated rodio to use git version --- Cargo.lock | 35 ++++++++++++++++++++++++++++++++-- Cargo.toml | 2 +- src/action.rs | 8 +++++--- src/app.rs | 45 ++++++++++++++++++++++++++++++-------------- src/player.rs | 37 ++++++++++++++++++++++++------------ src/widgets/queue.rs | 43 ++++++++++++++++-------------------------- 6 files changed, 111 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d302e2..67be149 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1355,6 +1355,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -1372,6 +1382,26 @@ dependencies = [ "syn 2.0.100", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1810,13 +1840,14 @@ dependencies = [ [[package]] name = "rodio" version = "0.20.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ceb6607dd738c99bc8cb28eff249b7cd5c8ec88b9db96c0608c1480d140fb1" +source = "git+https://github.com/RustAudio/rodio#0d352f5f2678226e843aa9c0ddea080f1e6d80ae" dependencies = [ "claxon", "cpal", + "dasp_sample", "hound", "lewton", + "num-rational", "symphonia", ] diff --git a/Cargo.toml b/Cargo.toml index 4f9090b..74a3857 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,5 +35,5 @@ subsonic-types = "0.2.0" rand = "0.9" md5 = "0.7" -rodio = "0.20" +rodio = { git = "https://github.com/RustAudio/rodio" } cpal = { version = "0.15", features = ["jack"] } diff --git a/src/action.rs b/src/action.rs index 99d4e2b..c3f5ec3 100644 --- a/src/action.rs +++ b/src/action.rs @@ -5,18 +5,20 @@ pub enum Action { Tick, Render, Resize(u16, u16), - TryPlaySong(String), - AddToSink(String), + TryPlaySong(usize), + AddToSink(usize), ScrollUp, ScrollDown, Quit, Init, RandomQueue, ClosePopUp, - ForcePlaySong(String), + ForcePlaySong(usize), VolumeUp, VolumeDown, PlayPause, UpdateQueue, Next, + TrackEnded, + Previous, } diff --git a/src/app.rs b/src/app.rs index 4bd9e3e..2415a48 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,11 +1,12 @@ use color_eyre::eyre::Result; use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; +use rand::seq::index; use ratatui::{ Terminal, buffer::Buffer, layout::{Constraint, Flex, Layout, Rect}, prelude::CrosstermBackend, - widgets::{Block, Clear, Paragraph, Widget, Wrap}, + widgets::{Block, Clear, Paragraph, StatefulWidget, TableState, Widget, Wrap}, }; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -16,7 +17,7 @@ use tracing::{debug, info}; use crate::{ action::Action, event::{Event, Events}, - player::Player, + player::{self, Player}, subsonic_helper, widgets::Queue, }; @@ -47,7 +48,7 @@ pub struct App { client: Client, player: Player, - queue: Queue, + queue_state: TableState, } impl Default for App { @@ -60,11 +61,10 @@ impl App { pub fn new() -> Self { let (tx, rx) = mpsc::unbounded_channel(); let player = Player::new(tx.clone()).expect("Could not create rodio player"); - let songs_list = player.songs_list.clone(); Self { player, client: Client::default(), - queue: Queue::new(songs_list), + queue_state: TableState::new(), rx, tx, mode: Mode::default(), @@ -120,15 +120,13 @@ impl App { match key.code { KeyCode::Esc if self.popup.is_some() => Some(Action::ClosePopUp), KeyCode::Enter => match self.mode { - Mode::Queue => self - .queue - .get_selected() - .map(|song| Action::ForcePlaySong(song.id)), + Mode::Queue => self.queue_state.selected().map(Action::ForcePlaySong), _ => None, }, KeyCode::Up => Some(Action::ScrollUp), KeyCode::Down => Some(Action::ScrollDown), KeyCode::Right => Some(Action::Next), + KeyCode::Left => Some(Action::Previous), KeyCode::Char(char) => match char { 'q' | 'Q' => Some(Action::Quit), 'r' | 'R' if self.mode == Mode::Queue => Some(Action::RandomQueue), @@ -166,12 +164,12 @@ impl App { } Action::ScrollUp => { if self.mode == Mode::Queue { - self.queue.scroll_up() + self.queue_state.select_previous(); } } Action::ScrollDown => { if self.mode == Mode::Queue { - self.queue.scroll_down() + self.queue_state.select_next(); } } Action::RandomQueue => subsonic_helper::request_random( @@ -193,8 +191,22 @@ impl App { self.player.pause(); } } - Action::UpdateQueue => self.queue.update(), - Action::Next => self.player.next(), + Action::UpdateQueue => self.queue_state.select_first(), + Action::Next => self.player.skip_next(), + Action::Previous => { + let index = self.player.current_playing; + if index > 0 { + self.tx.send(Action::ForcePlaySong(index - 1))?; + self.player.current_playing -= 1; + } + } + Action::TrackEnded => { + let index = self.player.current_playing + 1; + if index < self.player.songs_list.lock().unwrap().len() { + self.tx.send(Action::TryPlaySong(index))?; + self.player.current_playing += 1; + } + } } Ok(()) } @@ -217,7 +229,12 @@ impl Widget for &mut App { Mode::Help => {} Mode::Popup => {} Mode::Quit => {} - Mode::Queue => self.queue.render(area, buf), + Mode::Queue => StatefulWidget::render( + &Queue::new(self.player.songs_list.clone(), self.player.current_playing), + area, + buf, + &mut self.queue_state, + ), }; if let Some((title, message)) = &mut self.popup { diff --git a/src/player.rs b/src/player.rs index c3425de..a2e50f7 100644 --- a/src/player.rs +++ b/src/player.rs @@ -2,7 +2,7 @@ use color_eyre::eyre::Result; use cpal::traits::HostTrait; use futures::StreamExt; use reqwest::Client; -use rodio::{OutputStream, OutputStreamHandle, Sink}; +use rodio::{OutputStream, Sink, Source}; use std::fs::{self, File}; use std::io::BufReader; use std::sync::{Arc, Mutex}; @@ -18,8 +18,8 @@ use crate::subsonic_helper::make_request_url; pub struct Player { tx: UnboundedSender, pub songs_list: Arc>>, + pub current_playing: usize, sink: Sink, - _handle: OutputStreamHandle, _stream: OutputStream, } @@ -32,20 +32,20 @@ impl Player { "make sure --features jack is specified. only works on OSes where jack is available", )).expect("jack host unavailable").default_output_device().expect("Could not find output jack device"); - let (_stream, _handle) = rodio::OutputStream::try_from_device(&device)?; - let sink = rodio::Sink::try_new(&_handle)?; - sink.pause(); + let _stream = rodio::OutputStreamBuilder::from_device(device)?.open_stream()?; + let sink = rodio::Sink::connect_new(_stream.mixer()); Ok(Self { tx, _stream, - _handle, sink, songs_list: Default::default(), + current_playing: 0, }) } - pub fn try_play(&self, id: String, client: Client) -> Result<()> { + pub fn try_play(&self, index: usize, client: Client) -> Result<()> { + let id = self.songs_list.lock().unwrap()[index].id.clone(); let mut path = config::temp_dir(); path.push(format!("{id}.mp3")); @@ -70,18 +70,28 @@ impl Player { } tokio::time::sleep(Duration::from_millis(100)).await; } - tx.send(Action::AddToSink(id)) + tx.send(Action::AddToSink(index)) }); Ok(()) } - pub fn add_to_sink(&self, id: String) -> Result<()> { + pub fn add_to_sink(&mut self, index: usize) -> Result<()> { + let id = self.songs_list.lock().unwrap()[index].id.clone(); let mut path = config::temp_dir(); path.push(format!("{id}.mp3")); let file = std::fs::File::open(&path)?; let decoder = rodio::Decoder::new(BufReader::new(file))?; - self.sink.append(decoder); + let agc_source = decoder.automatic_gain_control(1.0, 4.0, 0.005, 5.0); + if self.sink.empty() { + self.current_playing = index; + } + self.sink.append(agc_source); + let tx = self.tx.clone(); + self.sink + .append(rodio::source::EmptyCallback::new(Box::new(move || { + let _ = tx.send(Action::TrackEnded); + }))); self.sink.play(); Ok(()) } @@ -104,13 +114,16 @@ impl Player { pub fn volume_down(&self) { self.sink.set_volume(self.sink.volume() - 0.2); + if self.sink.volume() <= 0. { + self.sink.set_volume(0.); + } } pub fn volume_up(&self) { - self.sink.set_volume(self.sink.volume() + 0.5); + self.sink.set_volume(self.sink.volume() + 0.2); } - pub fn next(&self) { + pub fn skip_next(&self) { self.sink.skip_one(); } } diff --git a/src/widgets/queue.rs b/src/widgets/queue.rs index d8dc34b..ee0f290 100644 --- a/src/widgets/queue.rs +++ b/src/widgets/queue.rs @@ -5,46 +5,29 @@ use ratatui::{ widgets::{StatefulWidget, Table, TableState}, }; use std::sync::{Arc, Mutex}; +use std::usize; +use style::Styled; use subsonic_types::response::Child; #[derive(Debug, Clone)] pub struct Queue { - table_state: TableState, songs_list: Arc>>, + current_playing: usize, } impl Queue { - pub fn new(songs_list: Arc>>) -> Self { + pub fn new(songs_list: Arc>>, current_playing: usize) -> Self { Self { - table_state: Default::default(), + current_playing, songs_list, } } - - pub fn scroll_down(&mut self) { - self.table_state.select_next(); - } - pub fn scroll_up(&mut self) { - self.table_state.select_previous(); - } - - pub fn get_selected(&self) -> Option { - if self.songs_list.lock().unwrap().is_empty() { - return None; - } - self.table_state - .selected() - .map(|selected| self.songs_list.lock().unwrap()[selected].clone()) - } - - pub fn update(&mut self) { - self.table_state.select_first(); - } } -impl Widget for &mut Queue { - fn render(self, area: Rect, buf: &mut Buffer) { +impl StatefulWidget for &Queue { + type State = TableState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { if !self.songs_list.lock().unwrap().is_empty() { let vertical_pad = |line| Text::from(vec!["".into(), line, "".into()]); let header_cells = ["#", "Title", "Artist", "Duration"] @@ -58,7 +41,13 @@ impl Widget for &mut Queue { let rows = list .iter() .enumerate() - .map(|(index, song)| row_from_song(index, song)) + .map(|(index, song)| { + let mut row = row_from_song(index, song); + if self.current_playing == index { + row = row.set_style(Style::new().fg(style::Color::Red)); + } + row + }) .collect_vec(); let table = Table::new(rows, column_widths) @@ -70,7 +59,7 @@ impl Widget for &mut Queue { .cell_highlight_style(Style::new().blue()) .highlight_symbol(">>"); - StatefulWidget::render(table, area, buf, &mut self.table_state); + StatefulWidget::render(table, area, buf, state); } else { Text::render(Text::from("Empty queue").centered(), area, buf); }