Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,4 @@ press `?` inside the player to see all keybindings

rexamp reads its settings from `~/.config/rexamp/config.toml` (or `$XDG_CONFIG_HOME/rexamp/config.toml`). Every key is optional, and `config.example.toml` describes each one.

All rexamp state can be found in `~/.local/state/rexamp/state.json`. Coming from 1.5 or earlier, rexamp copies the login and server keys from `config.toml` there on first start; after that you can delete them from `config.toml`.
All rexamp state can be found in `~/.local/state/rexamp/state.json`.
3 changes: 0 additions & 3 deletions doc/rexamp.1
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,6 @@ feature.
.B ?
Show help.
.TP
.B F12
Show memory usage.
.TP
.B q
Quit.
.SH CASTING
Expand Down
5 changes: 0 additions & 5 deletions src/app/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ pub enum Action {
MoveUp,
Clear,
Help,
DebugLine,
Quit,
TogglePlayer,
SearchSubmit,
Expand Down Expand Up @@ -91,8 +90,6 @@ pub const QUEUE_KEYS: &[Action] = &[
Action::MoveUp,
];

pub const MAXIMIZED_PLAYER_KEYS: &[Action] = &[];

pub const SEARCH_INPUT_KEYS: &[Action] = &[Action::SearchSubmit, Action::SearchCancel];

pub const HELP_KEYS: &[Action] = &[Action::Help, Action::Quit, Action::Back];
Expand Down Expand Up @@ -141,7 +138,6 @@ impl Action {
Action::MoveUp => "queue: move up",
Action::Clear => "queue: clear",
Action::Help => "help",
Action::DebugLine => "memory usage",
Action::Quit => "quit",
Action::Play => "play now (replace queue)",
Action::PlayShuffle => "play shuffled (replace queue)",
Expand Down Expand Up @@ -245,7 +241,6 @@ pub const BINDINGS: &[Binding] = &[
help_bar_only(bind(Action::MoveUp, &[key('K')])),
help_bar_only(bind(Action::Clear, &[key('D')])),
help_bar_only(bind(Action::Help, &[key('?')])),
bind(Action::DebugLine, &[code(KeyCode::F(12))]),
bind(Action::Quit, &[key('q')]),
help_bar_only(bind(Action::Play, &[key('P')])),
help_bar_only(bind(Action::PlayShuffle, &[key('S')])),
Expand Down
43 changes: 30 additions & 13 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ pub mod queue;
mod reporting;
pub mod state;

use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, sync_channel};
use std::sync::mpsc::{Receiver, RecvTimeoutError, SyncSender, channel, sync_channel};
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
Expand Down Expand Up @@ -66,7 +66,6 @@ pub enum ApiRequest {
view: u64,
query: String,
},
Report(Vec<Report>),
Cover {
thumb: String,
},
Expand Down Expand Up @@ -126,6 +125,10 @@ pub fn run(
let (player_tx, player_rx) = player::spawn(client.clone(), cache, saved.volume)?;

spawn_api_workers(client.clone(), section, saved, api_rx, tx.clone())?;

let (report_tx, report_rx) = channel();
let reports_done = spawn_reporter(client.clone(), report_rx)?;

spawn_forwarder(player_rx, tx.clone(), 0)?;

#[cfg(unix)]
Expand All @@ -138,6 +141,7 @@ pub fn run(

let mut app = AppState::new(
api_tx,
report_tx,
player_tx,
saved.volume,
saved.queue.take(),
Expand All @@ -148,15 +152,15 @@ pub fn run(
);
app.set_accent(accent);

spawn_tui(rx, app, media_controls, &client, saved)
spawn_tui(rx, app, media_controls, reports_done, saved)
}

/// runs the ui on a scoped thread while the main thread runs the platform run loop
fn spawn_tui(
rx: Receiver<AppEvent>,
mut state: AppState,
mut media_controls: Option<MediaControls>,
client: &PlexClient,
reports_done: Receiver<()>,
saved: &mut SavedState,
) -> Result<()> {
thread::scope(|s| {
Expand All @@ -168,7 +172,12 @@ fn spawn_tui(
let result = event_loop(&mut terminal, &mut state, &rx, media_controls.as_mut());
ratatui::restore();

shutdown(&mut state, client, saved)?;
let closed = shutdown(&mut state, saved);

drop(state);
let _ = reports_done.recv_timeout(REPORT_TIMEOUT);
closed?;

result
})?;
run_main_loop(&tui);
Expand All @@ -177,13 +186,12 @@ fn spawn_tui(
}

/// stops playback, sends the final playback reports and saves what the session changed
fn shutdown(state: &mut AppState, client: &PlexClient, saved: &mut SavedState) -> Result<()> {
fn shutdown(state: &mut AppState, saved: &mut SavedState) -> Result<()> {
state.playback.stop_player();
#[cfg(feature = "cast")]
let cast = state.close_cast();
for r in state.playback.reporter.stopped() {
send_report(client, &r);
}
let stopped = state.playback.reporter.stopped();
state.playback.report(stopped);
let result = save_session(state, saved);
#[cfg(feature = "cast")]
cast::wait_closed(cast);
Expand Down Expand Up @@ -332,6 +340,19 @@ fn send_report(c: &PlexClient, r: &Report) {
}
}

fn spawn_reporter(client: Arc<PlexClient>, rx: Receiver<Vec<Report>>) -> Result<Receiver<()>> {
let (done_tx, done_rx) = channel::<()>();
thread::Builder::new()
.name("reporter".into())
.spawn(move || {
let _done = done_tx;
for r in rx.iter().flatten() {
send_report(&client, &r);
}
})?;
Ok(done_rx)
}

fn spawn_forwarder(rx: Receiver<PlayerEvent>, tx: SyncSender<AppEvent>, id: u64) -> Result<()> {
thread::Builder::new()
.name("player-events".into())
Expand Down Expand Up @@ -431,10 +452,6 @@ impl ApiContext {
mode,
result: self.call(|| fetch_tracks(c, &source)).map_err(err),
},
ApiRequest::Report(reports) => {
reports.iter().for_each(|r| send_report(c, r));
return None;
}
ApiRequest::Cover { thumb } => AppEvent::Cover {
result: self.call(|| fetch_cover(c, &thumb)).map_err(err),
thumb,
Expand Down
4 changes: 2 additions & 2 deletions src/app/now_playing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ pub struct NowPlaying {
}

impl NowPlaying {
pub fn artist_playing_now(&self, artist_title: &str) -> bool {
pub fn artist_playing_now(&self, artist_key: &str) -> bool {
self.track
.as_ref()
.is_some_and(|t| t.grandparent_title == *artist_title)
.is_some_and(|t| t.grandparent_rating_key == *artist_key)
}

pub fn album_playing_now(&self, album_key: &str) -> bool {
Expand Down
25 changes: 12 additions & 13 deletions src/app/playback.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use std::sync::Arc;
use std::sync::mpsc::{SyncSender, TrySendError};
use std::sync::mpsc::{Sender, SyncSender, TrySendError};
use std::time::Instant;

use image::RgbImage;
Expand Down Expand Up @@ -28,6 +28,7 @@ pub struct Cover {
/// the queue, what is playing, and the player thread that plays it
pub struct Playback {
api: SyncSender<ApiRequest>,
reports: Sender<Vec<Report>>,
player: SyncSender<PlayerCmd>,
pub queue: Queue,
pub now: NowPlaying,
Expand All @@ -49,12 +50,14 @@ pub struct Playback {
impl Playback {
pub fn new(
api: SyncSender<ApiRequest>,
reports: Sender<Vec<Report>>,
player: SyncSender<PlayerCmd>,
volume: f32,
saved: Option<SavedQueue>,
) -> Self {
let mut playback = Self {
api,
reports,
player,
queue: Queue::default(),
now: NowPlaying {
Expand Down Expand Up @@ -140,25 +143,21 @@ impl Playback {
}
}

/// one request per event so a worker sends them in order (e.g. stopped before playing)
fn report(&self, reports: Vec<Report>) {
if !reports.is_empty() && self.api.try_send(ApiRequest::Report(reports)).is_err() {
warn!("dropping playback reports, API queue full");
pub(super) fn report(&self, reports: Vec<Report>) {
if !reports.is_empty() && self.reports.send(reports).is_err() {
warn!("playback reporter is gone");
}
}

pub fn play_index(&mut self, i: usize) {
if i >= self.queue.len() {
let Some(e) = self.queue.entries.get(i) else {
return;
}
};
let (id, track) = (e.id, e.track.clone());
let stopped = self.reporter.stopped();
self.report(stopped);
self.media_controls_dirty = true;
let Some(e) = self.queue.entries.get(i) else {
return;
};
let track = e.track.clone();
self.loading = Some((e.id, track.rating_key.clone()));
self.loading = Some((id, track.rating_key.clone()));
self.queue.current = Some(i);
self.enqueued = None;
self.resume_ms = None;
Expand Down Expand Up @@ -451,7 +450,7 @@ impl Playback {
return self.play_index(0);
}
AddMode::PlayShuffled => {
self.queue.set_shuffle(true);
self.queue.shuffle = true;
self.queue.replace(tracks, None);
return self.play_index(0);
}
Expand Down
5 changes: 5 additions & 0 deletions src/app/queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,11 @@ impl Queue {
return None;
}
self.entries.swap(i, j);

if !self.shuffle {
self.unshuffled.swap(i, j);
}

self.current = self.current.map(|c| match c {
c if c == i => j,
c if c == j => i,
Expand Down
2 changes: 1 addition & 1 deletion src/app/reporting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ impl PlayState {
}
}

#[derive(Debug, PartialEq)]
#[derive(Debug)]
pub enum Report {
Timeline {
rating_key: String,
Expand Down
14 changes: 6 additions & 8 deletions src/app/state.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use std::sync::mpsc::SyncSender;
use std::sync::mpsc::{Sender, SyncSender};
use std::time::{Duration, Instant};

use ratatui::crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::style::Color;

use super::browser::{Browser, ListKind, Selected, View};
use super::keys::{
self, Action, GLOBAL_PLAYER_KEYS, HELP_KEYS, LIST_KEYS, MAXIMIZED_PLAYER_KEYS, NAV_KEYS,
QUEUE_KEYS, SEARCH_INPUT_KEYS,
self, Action, GLOBAL_PLAYER_KEYS, HELP_KEYS, LIST_KEYS, NAV_KEYS, QUEUE_KEYS, SEARCH_INPUT_KEYS,
};
use super::playback::Playback;
use super::queue::SavedQueue;
use super::reporting::Report;
use super::{AddMode, ApiRequest, TrackSource};
use crate::backend::PlayerCmd;
#[cfg(feature = "cast")]
Expand Down Expand Up @@ -72,7 +72,6 @@ pub struct AppState {
quit_armed: Option<Instant>,
/// new server address found by rediscovery, saved to the state on exit
pub server_url: Option<String>,
pub debug_line: bool,
/// colour for focus, selection and highlights, from `accent_color` in the config
pub accent: Color,
#[cfg(feature = "cast")]
Expand All @@ -87,6 +86,7 @@ pub struct AppState {
impl AppState {
pub fn new(
api: SyncSender<ApiRequest>,
reports: Sender<Vec<Report>>,
player: SyncSender<PlayerCmd>,
volume: f32,
saved: Option<SavedQueue>,
Expand All @@ -102,10 +102,9 @@ impl AppState {
#[cfg(feature = "cast")]
static_cast_devices,
backend: 0,
playback: Playback::new(api, player, volume, saved),
playback: Playback::new(api, reports, player, volume, saved),
quit_armed: None,
server_url: None,
debug_line: false,
accent: Color::Cyan,
message: None,
quit: false,
Expand Down Expand Up @@ -158,7 +157,7 @@ impl AppState {
(Some(Modal::Help), _) => &[HELP_KEYS],
#[cfg(feature = "cast")]
(Some(Modal::Cast(_)), _) => &[keys::CAST_KEYS],
(None, Screen::Player) => &[MAXIMIZED_PLAYER_KEYS, GLOBAL_PLAYER_KEYS],
(None, Screen::Player) => &[GLOBAL_PLAYER_KEYS],
(None, Screen::Browse(Focus::Sidebar)) => &[NAV_KEYS, GLOBAL_PLAYER_KEYS],
(None, Screen::Browse(Focus::List)) => match self.browser.view().map(|v| &v.kind) {
Some(ListKind::Queue) => &[QUEUE_KEYS, NAV_KEYS, GLOBAL_PLAYER_KEYS],
Expand Down Expand Up @@ -240,7 +239,6 @@ impl AppState {
self.modal = Some(Modal::Search(String::new()));
}
Action::Help => self.modal = Some(Modal::Help),
Action::DebugLine => self.debug_line = !self.debug_line,
Action::Quit => self.request_quit(),
// Play is always shuffled when playing on Artist level
Action::Play => self.add_selected(AddMode::PlayFromStart),
Expand Down
14 changes: 13 additions & 1 deletion src/audio/decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ impl Decoder {
buf.copy_to_vec_interleaved(&mut self.scratch);
let skip = (self.skip_frames * self.channels).min(self.scratch.len());
self.skip_frames -= skip / self.channels.max(1);
super::to_stereo(&self.scratch[skip..], self.channels, out);
to_stereo(&self.scratch[skip..], self.channels, out);
return Ok(true);
}
}
Expand All @@ -114,3 +114,15 @@ impl Decoder {
Ok(required.as_millis().max(0) as u64)
}
}

fn to_stereo(src: &[f32], channels: usize, out: &mut Vec<f32>) {
match channels {
0 => {}
1 => out.extend(src.iter().flat_map(|&s| [s, s])),
2 => out.extend_from_slice(src),
n => out.extend(src.chunks_exact(n).flat_map(|f| {
let m = f.iter().sum::<f32>() / n as f32;
[m, m]
})),
}
}
12 changes: 0 additions & 12 deletions src/audio/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,3 @@ pub mod output;
pub mod player;
pub mod resample;
pub mod source;

pub fn to_stereo(src: &[f32], channels: usize, out: &mut Vec<f32>) {
match channels {
0 => {}
1 => out.extend(src.iter().flat_map(|&s| [s, s])),
2 => out.extend_from_slice(src),
n => out.extend(src.chunks_exact(n).flat_map(|f| {
let m = f.iter().sum::<f32>() / n as f32;
[m, m]
})),
}
}
7 changes: 5 additions & 2 deletions src/audio/output.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@ use cpal::{FromSample, SampleFormat, SizedSample, Stream, StreamConfig};
use rtrb::{Consumer, Producer, RingBuffer};
use tracing::{debug, error, info};

use crate::backend::perceptual_gain;

#[derive(Default)]
struct Shared {
played: AtomicU64,
Expand Down Expand Up @@ -193,3 +191,8 @@ where
)?;
Ok(stream)
}

fn perceptual_gain(volume: f32) -> f32 {
let v = volume.clamp(0.0, 1.0);
v * v * v
}
Loading
Loading