Reworked Queue widget to use external state

Update plyaer inner working and added agc
Updated rodio to use git version
This commit is contained in:
2025-03-26 23:02:43 +01:00
parent 1e72235e61
commit 40b9b86c92
6 changed files with 111 additions and 59 deletions

35
Cargo.lock generated
View File

@@ -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",
]

View File

@@ -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"] }

View File

@@ -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,
}

View File

@@ -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 {

View File

@@ -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<Action>,
pub songs_list: Arc<Mutex<Vec<Child>>>,
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();
}
}

View File

@@ -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<Mutex<Vec<Child>>>,
current_playing: usize,
}
impl Queue {
pub fn new(songs_list: Arc<Mutex<Vec<Child>>>) -> Self {
pub fn new(songs_list: Arc<Mutex<Vec<Child>>>, 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<Child> {
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);
}