From a5da4897de02910669a2bc828397b0f57605b976 Mon Sep 17 00:00:00 2001 From: Henrik Friedrichsen Date: Sat, 11 Jan 2020 18:11:41 +0100 Subject: [PATCH] implement software volume mixing closes #115 --- README.md | 1 + src/command.rs | 2 ++ src/commands.rs | 15 +++++++++++++- src/spotify.rs | 48 ++++++++++++++++++++++++++++++++++++++------- src/ui/statusbar.rs | 16 ++++++++++++--- 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dc10bff..0b7bb74 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ have them configurable. * `Shift-u` updates the library cache (tracks, artists, albums, playlists) * `<` and `>` play the previous or next track * `,` and `.` to rewind or skip forward +* `-` and `+` decrease or increase the volume * `r` to toggle repeat mode * `z` to toggle shuffle playback * `q` quits ncspot diff --git a/src/command.rs b/src/command.rs index f5e4ce4..5f43886 100644 --- a/src/command.rs +++ b/src/command.rs @@ -57,6 +57,8 @@ pub enum Command { Delete, Focus(String), Seek(SeekDirection), + VolumeUp, + VolumeDown, Repeat(Option), Shuffle(Option), Share(TargetMode), diff --git a/src/commands.rs b/src/commands.rs index dca751d..4cfebd0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -9,7 +9,7 @@ use cursive::views::ViewRef; use cursive::Cursive; use library::Library; use queue::{Queue, RepeatSetting}; -use spotify::Spotify; +use spotify::{Spotify, VOLUME_PERCENT}; use traits::ViewExt; use ui::layout::Layout; @@ -110,6 +110,17 @@ impl CommandManager { } Ok(None) } + Command::VolumeUp => { + let volume = self.spotify.volume().saturating_add(VOLUME_PERCENT); + self.spotify.set_volume(volume); + Ok(None) + } + Command::VolumeDown => { + let volume = self.spotify.volume().saturating_sub(VOLUME_PERCENT); + debug!("vol {}", volume); + self.spotify.set_volume(volume); + Ok(None) + } Command::Search(_) | Command::Move(_, _) | Command::Shift(_, _) @@ -202,6 +213,8 @@ impl CommandManager { kb.insert("/".into(), Command::Focus("search".into())); kb.insert(".".into(), Command::Seek(SeekDirection::Relative(500))); kb.insert(",".into(), Command::Seek(SeekDirection::Relative(-500))); + kb.insert("+".into(), Command::VolumeUp); + kb.insert("-".into(), Command::VolumeDown); kb.insert("r".into(), Command::Repeat(None)); kb.insert("z".into(), Command::Shuffle(None)); kb.insert("x".into(), Command::Share(TargetMode::Current)); diff --git a/src/spotify.rs b/src/spotify.rs index 47b07da..4c2ce57 100644 --- a/src/spotify.rs +++ b/src/spotify.rs @@ -10,6 +10,7 @@ use librespot_playback::config::PlayerConfig; use librespot_playback::audio_backend; use librespot_playback::config::Bitrate; +use librespot_playback::mixer::Mixer; use librespot_playback::player::Player; use rspotify::spotify::client::ApiError; @@ -36,6 +37,7 @@ use tokio_core::reactor::Core; use tokio_timer; use url::Url; +use std::sync::atomic::{AtomicU16, Ordering}; use std::sync::RwLock; use std::thread; use std::time::{Duration, SystemTime}; @@ -45,12 +47,15 @@ use config; use events::{Event, EventManager}; use track::Track; +pub const VOLUME_PERCENT: u16 = ((u16::max_value() as f64) * 1.0 / 100.0) as u16; + enum WorkerCommand { Load(Box), Play, Pause, Stop, Seek(u32), + SetVolume(u16), RequestToken(oneshot::Sender), } @@ -70,6 +75,7 @@ pub struct Spotify { token_issued: RwLock>, channel: mpsc::UnboundedSender, user: String, + volume: AtomicU16, } struct Worker { @@ -81,6 +87,7 @@ struct Worker { refresh_task: Box>, token_task: Box>, active: bool, + mixer: Box, } impl Worker { @@ -89,6 +96,7 @@ impl Worker { commands: mpsc::UnboundedReceiver, session: Session, player: Player, + mixer: Box, ) -> Worker { Worker { events, @@ -99,6 +107,7 @@ impl Worker { refresh_task: Box::new(futures::stream::empty()), token_task: Box::new(futures::empty()), active: false, + mixer, } } } @@ -154,6 +163,9 @@ impl futures::Future for Worker { WorkerCommand::Seek(pos) => { self.player.seek(pos); } + WorkerCommand::SetVolume(volume) => { + self.mixer.set_volume(volume); + } WorkerCommand::RequestToken(sender) => { self.token_task = Spotify::get_token(&self.session, sender); progress = true; @@ -206,12 +218,13 @@ impl Spotify { normalisation_pregain: 0.0, }; let (user_tx, user_rx) = oneshot::channel(); + let volume = 0xFFFF; let (tx, rx) = mpsc::unbounded(); { let events = events.clone(); thread::spawn(move || { - Self::worker(cfg, events, rx, player_config, credentials, user_tx) + Self::worker(cfg, events, rx, player_config, credentials, user_tx, volume) }); } @@ -223,6 +236,7 @@ impl Spotify { token_issued: RwLock::new(None), channel: tx, user: user_rx.wait().expect("error retrieving userid from worker"), + volume: AtomicU16::new(volume), }; // acquire token for web api usage @@ -297,6 +311,7 @@ impl Spotify { player_config: PlayerConfig, credentials: Credentials, user_tx: oneshot::Sender, + volume: u16, ) { let mut core = Core::new().unwrap(); @@ -305,13 +320,20 @@ impl Spotify { .send(session.username()) .expect("could not pass username back to Spotify::new"); - let backend = audio_backend::find(None).unwrap(); - let (player, _eventchannel) = - Player::new(player_config, session.clone(), None, move || { - (backend)(None) - }); + let create_mixer = librespot_playback::mixer::find(Some("softvol".to_owned())) + .expect("could not create softvol mixer"); + let mixer = create_mixer(None); + mixer.set_volume(volume); - let worker = Worker::new(events, commands, session, player); + let backend = audio_backend::find(None).unwrap(); + let (player, _eventchannel) = Player::new( + player_config, + session.clone(), + mixer.get_audio_filter(), + move || (backend)(None), + ); + + let worker = Worker::new(events, commands, session, player, mixer); debug!("worker thread ready."); core.run(worker).unwrap(); debug!("worker thread finished."); @@ -701,6 +723,18 @@ impl Spotify { let new = (progress.as_secs() * 1000) as i32 + progress.subsec_millis() as i32 + delta; self.seek(std::cmp::max(0, new) as u32); } + + pub fn volume(&self) -> u16 { + self.volume.load(Ordering::Relaxed) as u16 + } + + pub fn set_volume(&self, volume: u16) { + info!("setting volume to {}", volume); + self.volume.store(volume, Ordering::Relaxed); + self.channel + .unbounded_send(WorkerCommand::SetVolume(volume)) + .unwrap(); + } } pub enum URIType { diff --git a/src/ui/statusbar.rs b/src/ui/statusbar.rs index 1330c2d..ff1732e 100644 --- a/src/ui/statusbar.rs +++ b/src/ui/statusbar.rs @@ -90,7 +90,11 @@ impl View for StatusBar { }); let updating = if !*self.library.is_done.read().unwrap() { - if self.use_nerdfont { "\u{f9e5} " } else { "[U] " } + if self.use_nerdfont { + "\u{f9e5} " + } else { + "[U] " + } } else { "" }; @@ -119,6 +123,11 @@ impl View for StatusBar { "" }; + let volume = format!( + " [{}%]", + (self.spotify.volume() as f64 / 0xffff as f64 * 100.0) as u16 + ); + printer.with_color(style_bar_bg, |printer| { printer.print((0, 0), &"┉".repeat(printer.size.x)); }); @@ -147,7 +156,8 @@ impl View for StatusBar { + repeat + shuffle + saved - + &format!("{} / {} ", formatted_elapsed, t.duration_str()); + + &format!("{} / {}", formatted_elapsed, t.duration_str()) + + &volume; let offset = HAlign::Right.get_offset(right.width(), printer.size.x); printer.with_color(style, |printer| { @@ -160,7 +170,7 @@ impl View for StatusBar { printer.print((0, 0), &"━".repeat(duration_width + 1)); }); } else { - let right = updating.to_string() + repeat + shuffle; + let right = updating.to_string() + repeat + shuffle + &volume; let offset = HAlign::Right.get_offset(right.width(), printer.size.x); printer.with_color(style, |printer| {