From 68bec41e08564187e27d8c132b9b7b8274579819 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Tue, 6 Jul 2021 01:37:29 -0500 Subject: [PATCH 01/76] Improve Alsa backend buffer (#811) * Reuse the buffer for the life of the Alsa sink * Don't depend on capacity being exact when sizing the buffer * Always give the PCM a period's worth of audio even when draining the buffer * Refactoring and code cleanup --- playback/src/audio_backend/alsa.rs | 79 ++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index a9a593a39..7b5987a3e 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -152,10 +152,15 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; + trace!("Frames per Buffer: {:?}", frames_per_buffer); + trace!("Frames per Period: {:?}", frames_per_period); + // Let ALSA do the math for us. pcm.frames_to_bytes(frames_per_period) as usize }; + trace!("Period Buffer size in bytes: {:?}", bytes_per_period); + Ok((pcm, bytes_per_period)) } @@ -193,7 +198,22 @@ impl Sink for AlsaSink { match open_device(&self.device, self.format) { Ok((pcm, bytes_per_period)) => { self.pcm = Some(pcm); - self.period_buffer = Vec::with_capacity(bytes_per_period); + // If the capacity is greater than we want shrink it + // to it's current len (which should be zero) before + // setting the capacity with reserve_exact. + if self.period_buffer.capacity() > bytes_per_period { + self.period_buffer.shrink_to_fit(); + } + // This does nothing if the capacity is already sufficient. + // Len should always be zero, but for the sake of being thorough... + self.period_buffer + .reserve_exact(bytes_per_period - self.period_buffer.len()); + + // Should always match the "Period Buffer size in bytes: " trace! message. + trace!( + "Period Buffer capacity: {:?}", + self.period_buffer.capacity() + ); } Err(e) => { return Err(io::Error::new(io::ErrorKind::Other, e)); @@ -205,20 +225,22 @@ impl Sink for AlsaSink { } fn stop(&mut self) -> io::Result<()> { - { - // Write any leftover data in the period buffer - // before draining the actual buffer - self.write_bytes(&[])?; - let pcm = self.pcm.as_mut().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") - })?; - pcm.drain().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("Error stopping AlsaSink {}", e), - ) - })? - } + // Zero fill the remainder of the period buffer and + // write any leftover data before draining the actual PCM buffer. + self.period_buffer.resize(self.period_buffer.capacity(), 0); + self.write_buf()?; + + let pcm = self.pcm.as_mut().ok_or_else(|| { + io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") + })?; + + pcm.drain().map_err(|e| { + io::Error::new( + io::ErrorKind::Other, + format!("Error stopping AlsaSink {}", e), + ) + })?; + self.pcm = None; Ok(()) } @@ -228,22 +250,24 @@ impl Sink for AlsaSink { impl SinkAsBytes for AlsaSink { fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - let mut processed_data = 0; - while processed_data < data.len() { - let data_to_buffer = min( - self.period_buffer.capacity() - self.period_buffer.len(), - data.len() - processed_data, - ); + let mut start_index = 0; + let data_len = data.len(); + let capacity = self.period_buffer.capacity(); + loop { + let data_left = data_len - start_index; + let space_left = capacity - self.period_buffer.len(); + let data_to_buffer = min(data_left, space_left); + let end_index = start_index + data_to_buffer; self.period_buffer - .extend_from_slice(&data[processed_data..processed_data + data_to_buffer]); - processed_data += data_to_buffer; - if self.period_buffer.len() == self.period_buffer.capacity() { + .extend_from_slice(&data[start_index..end_index]); + if self.period_buffer.len() == capacity { self.write_buf()?; - self.period_buffer.clear(); } + if end_index == data_len { + break Ok(()); + } + start_index = end_index; } - - Ok(()) } } @@ -276,6 +300,7 @@ impl AlsaSink { })? } + self.period_buffer.clear(); Ok(()) } } From 4c00b19c29d1c29a528e332113f20a0ce9cdcb34 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 20:12:44 +0200 Subject: [PATCH 02/76] Fix Alsa mixer --- src/main.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index a3687aaae..aa04d0d41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -205,6 +205,7 @@ fn get_setup(args: &[String]) -> Setup { const FORMAT: &str = "format"; const HELP: &str = "h"; const INITIAL_VOLUME: &str = "initial-volume"; + const MIXER_TYPE: &str = "mixer"; const MIXER_CARD: &str = "mixer-card"; const MIXER_INDEX: &str = "mixer-index"; const MIXER_NAME: &str = "mixer-name"; @@ -295,7 +296,7 @@ fn get_setup(args: &[String]) -> Setup { "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) - .optopt("", "mixer", "Mixer to use {alsa|softvol}.", "MIXER") + .optopt("", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") .optopt( "m", MIXER_NAME, @@ -454,8 +455,8 @@ fn get_setup(args: &[String]) -> Setup { exit(0); } - let mixer_name = matches.opt_str(MIXER_NAME); - let mixer = mixer::find(mixer_name.as_deref()).expect("Invalid mixer"); + let mixer_type = matches.opt_str(MIXER_TYPE); + let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); let mixer_config = { let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| { @@ -475,7 +476,7 @@ fn get_setup(args: &[String]) -> Setup { let mut volume_range = matches .opt_str(VOLUME_RANGE) .map(|range| range.parse::().unwrap()) - .unwrap_or_else(|| match mixer_name.as_deref() { + .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, @@ -563,7 +564,7 @@ fn get_setup(args: &[String]) -> Setup { } (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) - .or_else(|| match mixer_name.as_deref() { + .or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] Some(AlsaMixer::NAME) => None, _ => cache.as_ref().and_then(Cache::volume), From 2541f123bcbb69454de901f48785988df61a7d68 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 21:02:48 +0200 Subject: [PATCH 03/76] Update documentation --- COMPILING.md | 25 +++++++++++-------------- CONTRIBUTING.md | 21 ++++++++++++++------- README.md | 12 +++++++----- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/COMPILING.md b/COMPILING.md index 8748cd0c2..39ae20cc7 100644 --- a/COMPILING.md +++ b/COMPILING.md @@ -5,20 +5,15 @@ In order to compile librespot, you will first need to set up a suitable Rust build environment, with the necessary dependencies installed. You will need to have a C compiler, Rust, and the development libraries for the audio backend(s) you want installed. These instructions will walk you through setting up a simple build environment. ### Install Rust -The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). On Unix/MacOS You can install `rustup` with this command: - -```bash -curl https://sh.rustup.rs -sSf | sh -``` - -Follow any prompts it gives you to install Rust. Once that’s done, Rust's standard tools should be setup and ready to use. +The easiest, and recommended way to get Rust is to use [rustup](https://rustup.rs). Once that’s installed, Rust's standard tools should be set up and ready to use. *Note: The current minimum required Rust version at the time of writing is 1.48, you can find the current minimum version specified in the `.github/workflow/test.yml` file.* #### Additional Rust tools - `rustfmt` -To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt), which is installed by default with `rustup` these days, else it can be installed manually with: +To ensure a consistent codebase, we utilise [`rustfmt`](https://github.com/rust-lang/rustfmt) and [`clippy`](https://github.com/rust-lang/rust-clippy), which are installed by default with `rustup` these days, else they can be installed manually with: ```bash rustup component add rustfmt +rustup component add clippy ``` Using `rustfmt` is not optional, as our CI checks against this repo's rules. @@ -43,12 +38,13 @@ Depending on the chosen backend, specific development libraries are required. |--------------------|------------------------------|-----------------------------------|-------------| |Rodio (default) | `libasound2-dev` | `alsa-lib-devel` | | |ALSA | `libasound2-dev, pkg-config` | `alsa-lib-devel` | | +|GStreamer | `gstreamer1.0-plugins-base libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-good libgstreamer-plugins-good1.0-dev` | `gstreamer1 gstreamer1-devel gstreamer1-plugins-base-devel gstreamer1-plugins-good` | `gstreamer gst-devtools gst-plugins-base gst-plugins-good` | |PortAudio | `portaudio19-dev` | `portaudio-devel` | `portaudio` | |PulseAudio | `libpulse-dev` | `pulseaudio-libs-devel` | | -|JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | | -|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | - | -|SDL | `libsdl2-dev` | `SDL2-devel` | | -|Pipe | - | - | - | +|JACK | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` | +|JACK over Rodio | `libjack-dev` | `jack-audio-connection-kit-devel` | `jack` | +|SDL | `libsdl2-dev` | `SDL2-devel` | `sdl2` | +|Pipe & subprocess | - | - | - | ###### For example, to build an ALSA based backend, you would need to run the following to install the required dependencies: @@ -68,7 +64,6 @@ The recommended method is to first fork the repo, so that you have a copy that y ```bash git clone git@github.com:YOURUSERNAME/librespot.git -cd librespot ``` ## Compiling & Running @@ -109,7 +104,9 @@ cargo build --no-default-features --features "alsa-backend" Assuming you just compiled a ```debug``` build, you can run librespot with the following command: ```bash -./target/debug/librespot -n Librespot +./target/debug/librespot ``` There are various runtime options, documented in the wiki, and visible by running librespot with the ```-h``` argument. + +Note that debug builds may cause buffer underruns and choppy audio when dithering is enabled (which it is by default). You can disable dithering with ```--dither none```. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3395529cc..907a7c040 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,10 +8,12 @@ If you have encountered a bug, please report it, as we rely on user reports to f Please also make sure that your issues are helpful. To ensure that your issue is helpful, please read over this brief checklist to avoid the more common pitfalls: - - Please take a moment to search/read previous similar issues to ensure you aren’t posting a duplicate. Duplicates will be closed immediately. - - Please include a clear description of what the issue is. Issues with descriptions such as ‘It hangs after 40 minutes’ will be closed immediately. - - Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues. - - Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run ‘export RUST_BACKTRACE=full’ before running librespot to enable backtraces. +- Please take a moment to search/read previous similar issues to ensure you aren’t posting a duplicate. Duplicates will be closed immediately. +- Please include a clear description of what the issue is. Issues with descriptions such as ‘It hangs after 40 minutes’ will be closed immediately. +- Please include, where possible, steps to reproduce the bug, along with any other material that is related to the bug. For example, if librespot consistently crashes when you try to play a song, please include the Spotify URI of that song. This can be immensely helpful in quickly pinpointing and resolving issues. +- Please be alert and respond to questions asked by any project members. Stale issues will be closed. +- When your issue concerns audio playback, please first make sure that your audio system is set up correctly and can play audio from other applications. This project aims to provide correct audio backends, not to provide Linux support to end users. +- Lastly, and perhaps most importantly, please include a backtrace where possible. Recent versions of librespot should produce these automatically when it crashes, and print them to the console, but in some cases, you may need to run ‘export RUST_BACKTRACE=full’ before running librespot to enable backtraces. ## Contributing Code @@ -33,16 +35,21 @@ Unless your changes are negligible, please add an entry in the "Unreleased" sect Make sure that the code is correctly formatted by running: ```bash -cargo +stable fmt --all +cargo fmt --all ``` -This command runs the previously installed stable version of ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project: +This command runs ```rustfmt```, a code formatting tool that will automatically correct any formatting that you have used that does not conform with the librespot code style. Once that command has run, you will need to rebuild the project: ```bash cargo build ``` -Once it has built, and you have confirmed there are no warnings or errors, you should commit your changes. +Once it has built, check for common code mistakes by running: +```bash +cargo clippy +``` + +Once you have confirmed there are no warnings or errors, you should commit your changes. ```bash git commit -a -m "My fancy fix" diff --git a/README.md b/README.md index bcf73cac0..f557cbc45 100644 --- a/README.md +++ b/README.md @@ -2,17 +2,17 @@ [![Gitter chat](https://badges.gitter.im/librespot-org/librespot.png)](https://gitter.im/librespot-org/spotify-connect-resources) [![Crates.io](https://img.shields.io/crates/v/librespot.svg)](https://crates.io/crates/librespot) -Current maintainer is [@awiouy](https://github.com/awiouy) folks. +Current maintainers are [listed on GitHub](https://github.com/orgs/librespot-org/people). # librespot *librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library. -_Note: librespot only works with Spotify Premium. This will remain the case for the foreseeable future, as we are unlikely to work on implementing the features such as limited skips and adverts that would be required to make librespot compliant with free accounts._ +_Note: librespot only works with Spotify Premium. This will remain the case. We will not any support features to make librespot compatible with free accounts, such as limited skips and adverts._ ## Quick start We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options). -After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160kbps audio. +After installation, you can run librespot from the CLI using a command such as `librespot -n "Librespot Speaker" -b 160` to create a speaker called _Librespot Speaker_ serving 160 kbps audio. ## This fork As the origin by [plietar](https://github.com/plietar/) is no longer actively maintained, this organisation and repository have been set up so that the project may be maintained and upgraded in the future. @@ -53,12 +53,14 @@ librespot currently offers the following selection of [audio backends](https://g ``` Rodio (default) ALSA +GStreamer PortAudio PulseAudio JACK JACK over Rodio SDL Pipe +Subprocess ``` Please check the corresponding [compiling entry](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) for backend specific dependencies. @@ -84,9 +86,9 @@ The above is a minimal example. Here is a more fully fledged one: ```shell target/release/librespot -n "Librespot" -b 320 -c ./cache --enable-volume-normalisation --initial-volume 75 --device-type avr ``` -The above command will create a receiver named ```Librespot```, with bitrate set to 320kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials. +The above command will create a receiver named ```Librespot```, with bitrate set to 320 kbps, initial volume at 75%, with volume normalisation enabled, and the device displayed in the app as an Audio/Video Receiver. A folder named ```cache``` will be created/used in the current directory, and be used to cache audio data and credentials. -A full list of runtime options are available [here](https://github.com/librespot-org/librespot/wiki/Options) +A full list of runtime options is available [here](https://github.com/librespot-org/librespot/wiki/Options). _Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._ From 43a8b91a3dd8715d777520810642503385e19a8f Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 22:17:29 +0200 Subject: [PATCH 04/76] Revert name to softvol --- playback/src/mixer/softmixer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback/src/mixer/softmixer.rs b/playback/src/mixer/softmixer.rs index 27448237b..cefc2de54 100644 --- a/playback/src/mixer/softmixer.rs +++ b/playback/src/mixer/softmixer.rs @@ -43,7 +43,7 @@ impl Mixer for SoftMixer { } impl SoftMixer { - pub const NAME: &'static str = "softmixer"; + pub const NAME: &'static str = "softvol"; } struct SoftVolumeApplier { From bd350c5aa0ebbea0769e11be79258d7087c9220b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 9 Jul 2021 22:30:49 +0200 Subject: [PATCH 05/76] Remove non-working Facebook authentication --- docs/authentication.md | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/docs/authentication.md b/docs/authentication.md index 864701610..2eeb5645e 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -57,20 +57,4 @@ login_data = AES192-DECRYPT(key, data) ``` ## Facebook based Authentication -The client starts an HTTPS server, and makes the user visit -`https://login.spotify.com/login-facebook-sso/?csrf=CSRF&port=PORT` -in their browser, where CSRF is a random token, and PORT is the HTTPS server's port. - -This will redirect to Facebook, where the user must login and authorize Spotify, and -finally make a GET request to -`https://login.spotilocal.com:PORT/login/facebook_login_sso.json?csrf=CSRF&access_token=TOKEN`, -where PORT and CSRF are the same as sent earlier, and TOKEN is the facebook authentication token. - -Since `login.spotilocal.com` resolves the 127.0.0.1, the request is received by the client. - -The client must then contact Facebook's API at -`https://graph.facebook.com/me?fields=id&access_token=TOKEN` -in order to retrieve the user's Facebook ID. - -The Facebook ID is the `username`, the TOKEN the `auth_data`, and `auth_type` is set to `AUTHENTICATION_FACEBOOK_TOKEN`. - +Facebook authentication is currently broken due to Spotify changing the authentication flow. The details of how the new flow works are detailed in https://github.com/librespot-org/librespot/issues/244 and will be implemented at some point in the future. From efd4a02896389735f541b27353558eb2fe0a1134 Mon Sep 17 00:00:00 2001 From: sigaloid <69441971+sigaloid@users.noreply.github.com> Date: Fri, 20 Aug 2021 16:13:39 -0400 Subject: [PATCH 06/76] Cargo update --- Cargo.lock | 342 +++++++++++++++++++++++++++++------------------------ 1 file changed, 187 insertions(+), 155 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 646957232..6d631c831 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,15 +78,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.41" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61" +checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" [[package]] name = "async-trait" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b98e84bbb4cbcdd97da190ba0c58a1bb0de2c1fdf67d159e192ed766aeca722" +checksum = "44318e776df68115a881de9a8fd1b9e53368d7a4a5ce4cc48517da3393233a5e" dependencies = [ "proc-macro2", "quote", @@ -170,9 +170,9 @@ checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" [[package]] name = "cc" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" +checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" dependencies = [ "jobserver", ] @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.6.2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ca8a5221364ef15ce201e8ed2f609fc312682a8f4e0e3d4aa5879764e0fa3b" +checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" [[package]] name = "coreaudio-rs" @@ -285,21 +285,21 @@ dependencies = [ [[package]] name = "cpal" -version = "0.13.3" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8351ddf2aaa3c583fa388029f8b3d26f3c7035a20911fdd5f2e2ed7ab57dad25" +checksum = "98f45f0a21f617cd2c788889ef710b63f075c949259593ea09c826f1e47a2418" dependencies = [ "alsa", "core-foundation-sys", "coreaudio-rs", - "jack 0.6.6", + "jack", "jni", "js-sys", "lazy_static", "libc", "mach", - "ndk", - "ndk-glue", + "ndk 0.3.0", + "ndk-glue 0.3.0", "nix", "oboe", "parking_lot", @@ -320,9 +320,9 @@ dependencies = [ [[package]] name = "crypto-mac" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25fab6889090c8133f3deb8f73ba3c65a7f456f66436fc012a1b1e272b1e103e" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" dependencies = [ "generic-array", "subtle", @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e7e43a803dae2fa37c1f6a8fe121e1f7bf9548b4dfc0522a42f34145dadfc27" +checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" dependencies = [ "futures-channel", "futures-core", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e682a68b29a882df0545c143dc3646daefe80ba479bcdede94d5a703de2871e2" +checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" dependencies = [ "futures-core", "futures-sink", @@ -464,15 +464,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" +checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" [[package]] name = "futures-executor" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" +checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" dependencies = [ "futures-core", "futures-task", @@ -481,15 +481,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc499defb3b348f8d8f3f66415835a9131856ff7714bf10dadfc4ec4bdb29a1" +checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" [[package]] name = "futures-macro" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4c40298486cdf52cc00cd6d6987892ba502c7656a16a4192a9992b1ccedd121" +checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" dependencies = [ "autocfg", "proc-macro-hack", @@ -500,21 +500,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a57bead0ceff0d6dde8f465ecd96c9338121bb7717d3e7b108059531870c4282" +checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" [[package]] name = "futures-task" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" +checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" [[package]] name = "futures-util" -version = "0.3.15" +version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" +checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" dependencies = [ "autocfg", "futures-channel", @@ -589,7 +589,7 @@ dependencies = [ "anyhow", "heck", "itertools", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro-error", "proc-macro2", "quote", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60daa14be0e0786db0f03a9e57cb404c9d756eed2b6c62b9ea98ec5743ec75a9" +checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" dependencies = [ "bytes", "http", @@ -821,9 +821,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.4.1" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a87b616e37e93c22fb19bcd386f02f3af5ea98a25670ad0fce773de23c5e68" +checksum = "acd94fdbe1d4ff688b67b04eee2e17bd50995534a61539e45adfefb45e5e5503" [[package]] name = "httpdate" @@ -839,9 +839,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.9" +version = "0.14.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d6baa1b441335f3ce5098ac421fb6547c46dda735ca1bc6d0153c838f9dd83" +checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" dependencies = [ "bytes", "futures-channel", @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" +checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" dependencies = [ "cfg-if 1.0.0", ] @@ -947,18 +947,6 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" -[[package]] -name = "jack" -version = "0.6.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2deb4974bd7e6b2fb7784f27fa13d819d11292b3b004dce0185ec08163cf686a" -dependencies = [ - "bitflags", - "jack-sys", - "lazy_static", - "libc", -] - [[package]] name = "jack" version = "0.7.1" @@ -984,9 +972,9 @@ dependencies = [ [[package]] name = "jni" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24967112a1e4301ca5342ea339763613a37592b8a6ce6cf2e4494537c7a42faf" +checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" dependencies = [ "cesu8", "combine", @@ -1004,18 +992,18 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "jobserver" -version = "0.1.22" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83bdfbace3a0e81a4253f73b49e960b053e396a11012cbd49b9b74d6a2b67062" +checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" dependencies = [ "wasm-bindgen", ] @@ -1045,9 +1033,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.97" +version = "0.2.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6" +checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" [[package]] name = "libloading" @@ -1095,9 +1083,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.23.1" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db951f37898e19a6785208e3a290261e0f1a8e086716be596aaad684882ca8e3" +checksum = "04b4154b9bc606019cb15125f96e08e1e9c4f53d55315f1ef69ae229e30d1765" dependencies = [ "bitflags", "libc", @@ -1109,9 +1097,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.23.0" +version = "2.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a574975292db859087c3957b9182f7d53278553f06bddaa2099c90e4ac3a0ee0" +checksum = "1165af13c42b9c325582b1a75eaa4a0f176c9094bb3a13877826e9be24881231" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1120,9 +1108,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.16.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "468cf582b7b022c0d1b266fefc7fc8fa7b1ddcb61214224f2f105c95a9c2d5c1" +checksum = "83346d68605e656afdefa9a8a2f1968fa05ab9369b55f2e26f7bf2a11b7e8444" dependencies = [ "libpulse-sys", "pkg-config", @@ -1130,9 +1118,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.18.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf17e9832643c4f320c42b7d78b2c0510f45aa5e823af094413b94e45076ba82" +checksum = "9ebed2cc92c38cac12307892ce6fb17e2e950bfda1ed17b3e1d47fd5184c8f2b" dependencies = [ "libc", "num-derive", @@ -1288,7 +1276,7 @@ dependencies = [ "glib", "gstreamer", "gstreamer-app", - "jack 0.7.1", + "jack", "lewton", "libpulse-binding", "libpulse-simple-binding", @@ -1352,15 +1340,24 @@ checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" [[package]] name = "matches" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" [[package]] name = "memchr" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "memoffset" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" +dependencies = [ + "autocfg", +] [[package]] name = "mime" @@ -1417,6 +1414,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "ndk" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64d6af06fde0e527b1ba5c7b79a6cc89cfc46325b0b2887dffe8f70197e0c3c" +dependencies = [ + "bitflags", + "jni-sys", + "ndk-sys", + "num_enum", + "thiserror", +] + [[package]] name = "ndk-glue" version = "0.3.0" @@ -1426,7 +1436,21 @@ dependencies = [ "lazy_static", "libc", "log", - "ndk", + "ndk 0.3.0", + "ndk-macro", + "ndk-sys", +] + +[[package]] +name = "ndk-glue" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e9e94628f24e7a3cb5b96a2dc5683acd9230bf11991c2a1677b87695138420" +dependencies = [ + "lazy_static", + "libc", + "log", + "ndk 0.4.0", "ndk-macro", "ndk-sys", ] @@ -1438,7 +1462,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "05d1c6307dc424d0f65b9b06e94f88248e6305726b14729fd67a5e47b2dc481d" dependencies = [ "darling", - "proc-macro-crate", + "proc-macro-crate 0.1.5", "proc-macro2", "quote", "syn", @@ -1452,14 +1476,15 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" [[package]] name = "nix" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" +checksum = "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56" dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", "libc", + "memoffset", ] [[package]] @@ -1547,9 +1572,9 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" +checksum = "3f9bd055fb730c4f8f4f57d45d35cd6b3f0980535b056dc7ff119cee6a66ed6f" dependencies = [ "derivative", "num_enum_derive", @@ -1557,11 +1582,11 @@ dependencies = [ [[package]] name = "num_enum_derive" -version = "0.5.1" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" +checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ - "proc-macro-crate", + "proc-macro-crate 1.0.0", "proc-macro2", "quote", "syn", @@ -1569,13 +1594,13 @@ dependencies = [ [[package]] name = "oboe" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfa187b38ae20374617b7ad418034ed3dc90ac980181d211518bd03537ae8f8d" +checksum = "e15e22bc67e047fe342a32ecba55f555e3be6166b04dd157cd0f803dfa9f48e1" dependencies = [ "jni", - "ndk", - "ndk-glue", + "ndk 0.4.0", + "ndk-glue 0.4.0", "num-derive", "num-traits", "oboe-sys", @@ -1583,9 +1608,9 @@ dependencies = [ [[package]] name = "oboe-sys" -version = "0.4.2" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b88e64835aa3f579c08d182526dc34e3907343d5b97e87b71a40ba5bca7aca9e" +checksum = "338142ae5ab0aaedc8275aa8f67f460e43ae0fca76a695a742d56da0a269eadc" dependencies = [ "cc", ] @@ -1734,6 +1759,16 @@ dependencies = [ "toml", ] +[[package]] +name = "proc-macro-crate" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +dependencies = [ + "thiserror", + "toml", +] + [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1772,33 +1807,33 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" +checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" dependencies = [ "unicode-xid", ] [[package]] name = "protobuf" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db50e77ae196458ccd3dc58a31ea1a90b0698ab1b7928d89f644c25d72070267" +checksum = "020f86b07722c5c4291f7c723eac4676b3892d47d9a7708dc2779696407f039b" [[package]] name = "protobuf-codegen" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09321cef9bee9ddd36884f97b7f7cc92a586cdc74205c4b3aeba65b5fc9c6f90" +checksum = "7b8ac7c5128619b0df145d9bace18e8ed057f18aebda1aa837a5525d4422f68c" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.24.1" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1afb68a6d768571da3db86ce55f0f62966e0fc25eaf96acd070ea548a91b0d23" +checksum = "f6d0daa1b61d6e7a128cdca8c8604b3c5ee22c424c15c8d3a92fafffeda18aaf" dependencies = [ "protobuf", "protobuf-codegen", @@ -1865,9 +1900,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.9" +version = "0.2.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee" +checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" dependencies = [ "bitflags", ] @@ -1978,24 +2013,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f3aac57ee7f3272d8395c6e4f502f434f0e289fcd62876f70daa008c20dcabe" +checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" +checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.126" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" +checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" dependencies = [ "proc-macro2", "quote", @@ -2004,9 +2039,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.64" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" +checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" dependencies = [ "itoa", "ryu", @@ -2015,9 +2050,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.6" +version = "0.9.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c4cfa741c5832d0ef7fab46cabed29c2aae926db0b11bb2069edd8db5e64e16" +checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -2058,9 +2093,9 @@ dependencies = [ [[package]] name = "simple_logger" -version = "1.11.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd57f17c093ead1d4a1499dc9acaafdd71240908d64775465543b8d9a9f1d198" +checksum = "b7de33c687404ec3045d4a0d437580455257c0436f858d702f244e7d652f9f07" dependencies = [ "atty", "chrono", @@ -2071,9 +2106,9 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" +checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" [[package]] name = "smallvec" @@ -2083,9 +2118,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" [[package]] name = "socket2" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e3dfc207c526015c632472a77be09cf1b6e46866581aecae5cc38fb4235dea2" +checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" dependencies = [ "libc", "winapi", @@ -2123,15 +2158,15 @@ dependencies = [ [[package]] name = "subtle" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7" +checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" dependencies = [ "proc-macro2", "quote", @@ -2140,9 +2175,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" dependencies = [ "proc-macro2", "quote", @@ -2190,18 +2225,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" +checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.25" +version = "1.0.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" +checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" dependencies = [ "proc-macro2", "quote", @@ -2220,9 +2255,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.2.0" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" dependencies = [ "tinyvec_macros", ] @@ -2235,9 +2270,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.7.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2" +checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" dependencies = [ "autocfg", "bytes", @@ -2254,9 +2289,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" +checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" dependencies = [ "proc-macro2", "quote", @@ -2265,9 +2300,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" +checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" dependencies = [ "futures-core", "pin-project-lite", @@ -2316,9 +2351,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9ff14f98b1a4b289c6248a023c1c2fa1491062964e9fed67ab29c4e4da4a052" +checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" dependencies = [ "lazy_static", ] @@ -2337,12 +2372,9 @@ checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" [[package]] name = "unicode-bidi" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" -dependencies = [ - "matches", -] +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" [[package]] name = "unicode-normalization" @@ -2355,9 +2387,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" +checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" @@ -2444,9 +2476,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54ee1d4ed486f78874278e63e4069fc1ab9f6a18ca492076ffb90c5eb2997fd" +checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2454,9 +2486,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b33f6a0694ccfea53d94db8b2ed1c3a8a4c86dd936b13b9f0a15ec4a451b900" +checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" dependencies = [ "bumpalo", "lazy_static", @@ -2469,9 +2501,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "088169ca61430fe1e58b8096c24975251700e7b1f6fd91cc9d59b04fb9b18bd4" +checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2479,9 +2511,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be2241542ff3d9f241f5e2cb6dd09b37efe786df8851c54957683a49f0987a97" +checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" dependencies = [ "proc-macro2", "quote", @@ -2492,15 +2524,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.74" +version = "0.2.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7cff876b8f18eed75a66cf49b65e7f967cb354a7aa16003fb55dbfd25b44b4f" +checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" [[package]] name = "web-sys" -version = "0.3.51" +version = "0.3.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e828417b379f3df7111d3a2a9e5753706cae29c41f7c4029ee9fd77f3e09e582" +checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" dependencies = [ "js-sys", "wasm-bindgen", From c67e268dc8b9552e215271cce7bb638a3133b3a2 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 26 Aug 2021 22:35:45 +0200 Subject: [PATCH 07/76] Improve Alsa mixer command-line options --- CHANGELOG.md | 5 +- playback/src/mixer/alsamixer.rs | 16 +++--- playback/src/mixer/mod.rs | 4 +- src/main.rs | 98 ++++++++++++++++++++++++--------- 4 files changed, 87 insertions(+), 36 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ecd12f2f..acf4f7353 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,11 +29,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate +- [playback] `alsamixer`: renamed `mixer-card` to `alsa-mixer-device` +- [playback] `alsamixer`: renamed `mixer-name` to `alsa-mixer-control` +- [playback] `alsamixer`: renamed `mixer-index` to `alsa-mixer-index` ### Removed - [connect] Removed no-op mixer started/stopped logic (breaking) - [playback] Removed `with-vorbis` and `with-tremor` features -- [playback] `alsamixer`: removed `--mixer-linear-volume` option; use `--volume-ctrl linear` instead +- [playback] `alsamixer`: removed `--mixer-linear-volume` option, now that `--volume-ctrl {linear|log}` work as expected on Alsa ### Fixed - [connect] Fix step size on volume up/down events diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 8bee9e0d9..81d0436f4 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -31,14 +31,14 @@ const ZERO_DB: MilliBel = MilliBel(0); impl Mixer for AlsaMixer { fn open(config: MixerConfig) -> Self { info!( - "Mixing with alsa and volume control: {:?} for card: {} with mixer control: {},{}", - config.volume_ctrl, config.card, config.control, config.index, + "Mixing with Alsa and volume control: {:?} for device: {} with mixer control: {},{}", + config.volume_ctrl, config.device, config.control, config.index, ); let mut config = config; // clone let mixer = - alsa::mixer::Mixer::new(&config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&config.control, config.index)) .expect("Could not find Alsa mixer control"); @@ -56,8 +56,8 @@ impl Mixer for AlsaMixer { // Query dB volume range -- note that Alsa exposes a different // API for hardware and software mixers let (min_millibel, max_millibel) = if is_softvol { - let control = - Ctl::new(&config.card, false).expect("Could not open Alsa softvol with that card"); + let control = Ctl::new(&config.device, false) + .expect("Could not open Alsa softvol with that device"); let mut element_id = ElemId::new(ElemIface::Mixer); element_id.set_name( &CString::new(config.control.as_str()) @@ -144,7 +144,7 @@ impl Mixer for AlsaMixer { fn volume(&self) -> u16 { let mixer = - alsa::mixer::Mixer::new(&self.config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&self.config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&self.config.control, self.config.index)) .expect("Could not find Alsa mixer control"); @@ -184,7 +184,7 @@ impl Mixer for AlsaMixer { fn set_volume(&self, volume: u16) { let mixer = - alsa::mixer::Mixer::new(&self.config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&self.config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&self.config.control, self.config.index)) .expect("Could not find Alsa mixer control"); @@ -249,7 +249,7 @@ impl AlsaMixer { } let mixer = - alsa::mixer::Mixer::new(&self.config.card, false).expect("Could not open Alsa mixer"); + alsa::mixer::Mixer::new(&self.config.device, false).expect("Could not open Alsa mixer"); let simple_element = mixer .find_selem(&SelemId::new(&self.config.control, self.config.index)) .expect("Could not find Alsa mixer control"); diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index ed39582ea..5397598fa 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -30,7 +30,7 @@ use self::alsamixer::AlsaMixer; #[derive(Debug, Clone)] pub struct MixerConfig { - pub card: String, + pub device: String, pub control: String, pub index: u32, pub volume_ctrl: VolumeCtrl, @@ -39,7 +39,7 @@ pub struct MixerConfig { impl Default for MixerConfig { fn default() -> MixerConfig { MixerConfig { - card: String::from("default"), + device: String::from("default"), control: String::from("PCM"), index: 0, volume_ctrl: VolumeCtrl::default(), diff --git a/src/main.rs b/src/main.rs index aa04d0d41..d240e2243 100644 --- a/src/main.rs +++ b/src/main.rs @@ -206,9 +206,9 @@ fn get_setup(args: &[String]) -> Setup { const HELP: &str = "h"; const INITIAL_VOLUME: &str = "initial-volume"; const MIXER_TYPE: &str = "mixer"; - const MIXER_CARD: &str = "mixer-card"; - const MIXER_INDEX: &str = "mixer-index"; - const MIXER_NAME: &str = "mixer-name"; + const ALSA_MIXER_DEVICE: &str = "alsa-mixer-device"; + const ALSA_MIXER_INDEX: &str = "alsa-mixer-index"; + const ALSA_MIXER_CONTROL: &str = "alsa-mixer-control"; const NAME: &str = "name"; const NORMALISATION_ATTACK: &str = "normalisation-attack"; const NORMALISATION_GAIN_TYPE: &str = "normalisation-gain-type"; @@ -296,24 +296,42 @@ fn get_setup(args: &[String]) -> Setup { "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) - .optopt("", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") + .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") .optopt( - "m", - MIXER_NAME, + "", + "mixer-name", // deprecated + "", + "", + ) + .optopt( + "", + ALSA_MIXER_CONTROL, "Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.", "NAME", ) .optopt( "", - MIXER_CARD, - "Alsa mixer card, e.g 'hw:0' or similar from `aplay -l`. Defaults to DEVICE if specified, 'default' otherwise.", - "MIXER_CARD", + "mixer-card", // deprecated + "", + "", + ) + .optopt( + "", + ALSA_MIXER_DEVICE, + "Alsa mixer device, e.g 'hw:0' or similar from `aplay -l`. Defaults to `--device` if specified, 'default' otherwise.", + "DEVICE", + ) + .optopt( + "", + "mixer-index", // deprecated + "", + "", ) .optopt( "", - MIXER_INDEX, + ALSA_MIXER_INDEX, "Alsa index of the cards mixer. Defaults to 0.", - "INDEX", + "NUMBER", ) .optopt( "", @@ -459,20 +477,50 @@ fn get_setup(args: &[String]) -> Setup { let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); let mixer_config = { - let card = matches.opt_str(MIXER_CARD).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - MixerConfig::default().card + let mixer_device = match matches.opt_str("mixer-card") { + Some(card) => { + warn!("--mixer-card is deprecated and will be removed in a future release."); + warn!("Please use --alsa-mixer-device instead."); + card } - }); - let index = matches - .opt_str(MIXER_INDEX) - .map(|index| index.parse::().unwrap()) - .unwrap_or(0); - let control = matches - .opt_str(MIXER_NAME) - .unwrap_or_else(|| MixerConfig::default().control); + None => matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + if let Some(ref device_name) = device { + device_name.to_string() + } else { + MixerConfig::default().device + } + }), + }; + + let index = match matches.opt_str("mixer-index") { + Some(index) => { + warn!("--mixer-index is deprecated and will be removed in a future release."); + warn!("Please use --alsa-mixer-index instead."); + index + .parse::() + .expect("Mixer index is not a valid number") + } + None => matches + .opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index + .parse::() + .expect("Alsa mixer index is not a valid number") + }) + .unwrap_or(0), + }; + + let control = match matches.opt_str("mixer-name") { + Some(name) => { + warn!("--mixer-name is deprecated and will be removed in a future release."); + warn!("Please use --alsa-mixer-control instead."); + name + } + None => matches + .opt_str(ALSA_MIXER_CONTROL) + .unwrap_or_else(|| MixerConfig::default().control), + }; + let mut volume_range = matches .opt_str(VOLUME_RANGE) .map(|range| range.parse::().unwrap()) @@ -503,7 +551,7 @@ fn get_setup(args: &[String]) -> Setup { }); MixerConfig { - card, + device: mixer_device, control, index, volume_ctrl, From 7da4d0e4730ecdc8fcf82c97f1bc466ddf28e8b3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 1 Sep 2021 20:54:47 +0200 Subject: [PATCH 08/76] Attenuate after normalisation --- playback/src/player.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a6e71aada..21afdbbec 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1186,10 +1186,6 @@ impl PlayerInternal { Some(mut packet) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { - if let Some(ref editor) = self.audio_filter { - editor.modify_stream(data) - } - if self.config.normalisation && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && self.config.normalisation_method == NormalisationMethod::Basic) @@ -1302,6 +1298,10 @@ impl PlayerInternal { } } } + + if let Some(ref editor) = self.audio_filter { + editor.modify_stream(data) + } } if let Err(err) = self.sink.write(&packet, &mut self.converter) { From d8e35bf0c4f9ee3909da276665ac1d9df8386e00 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 1 Sep 2021 20:55:28 +0200 Subject: [PATCH 09/76] Remove clamping of float samples --- playback/src/player.rs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index 21afdbbec..361c24a78 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1286,16 +1286,7 @@ impl PlayerInternal { } } } - *sample *= actual_normalisation_factor; - - // Extremely sharp attacks, however unlikely, *may* still clip and provide - // undefined results, so strictly enforce output within [-1.0, 1.0]. - if *sample < -1.0 { - *sample = -1.0; - } else if *sample > 1.0 { - *sample = 1.0; - } } } From b016b697722a09663215122bb7fd16061822e223 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 1 Sep 2021 21:25:32 +0200 Subject: [PATCH 10/76] Fix clippy warnings --- audio/src/decrypt.rs | 4 ++-- connect/src/spirc.rs | 6 +++--- core/src/connection/handshake.rs | 2 +- metadata/src/lib.rs | 2 +- playback/src/audio_backend/alsa.rs | 2 +- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/audio/src/decrypt.rs b/audio/src/decrypt.rs index 616ef4f68..17f4edba3 100644 --- a/audio/src/decrypt.rs +++ b/audio/src/decrypt.rs @@ -18,8 +18,8 @@ pub struct AudioDecrypt { impl AudioDecrypt { pub fn new(key: AudioKey, reader: T) -> AudioDecrypt { let cipher = Aes128Ctr::new( - &GenericArray::from_slice(&key.0), - &GenericArray::from_slice(&AUDIO_AESIV), + GenericArray::from_slice(&key.0), + GenericArray::from_slice(&AUDIO_AESIV), ); AudioDecrypt { cipher, reader } } diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 57dc4cdd8..9c541871e 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1033,7 +1033,7 @@ impl SpircTask { .payload .first() .expect("Empty payload on context uri"); - let response: serde_json::Value = serde_json::from_slice(&data).unwrap(); + let response: serde_json::Value = serde_json::from_slice(data).unwrap(); Ok(response) } @@ -1051,7 +1051,7 @@ impl SpircTask { if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { track_vec.drain(0..head); } - track_vec.extend_from_slice(&new_tracks); + track_vec.extend_from_slice(new_tracks); self.state .set_track(protobuf::RepeatedField::from_vec(track_vec)); @@ -1218,7 +1218,7 @@ impl SpircTask { trace!("Sending status to server: [{}]", status_string); let mut cs = CommandSender::new(self, MessageType::kMessageTypeNotify); if let Some(s) = recipient { - cs = cs.recipient(&s); + cs = cs.recipient(s); } cs.send(); } diff --git a/core/src/connection/handshake.rs b/core/src/connection/handshake.rs index 82ec7672c..eddcd3276 100644 --- a/core/src/connection/handshake.rs +++ b/core/src/connection/handshake.rs @@ -124,7 +124,7 @@ fn compute_keys(shared_secret: &[u8], packets: &[u8]) -> (Vec, Vec, Vec< let mut data = Vec::with_capacity(0x64); for i in 1..6 { let mut mac = - HmacSha1::new_from_slice(&shared_secret).expect("HMAC can take key of any size"); + HmacSha1::new_from_slice(shared_secret).expect("HMAC can take key of any size"); mac.update(packets); mac.update(&[i]); data.extend_from_slice(&mac.finalize().into_bytes()); diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index d328a7d9c..cf663ce6f 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -126,7 +126,7 @@ pub trait Metadata: Send + Sized + 'static { let data = response.payload.first().expect("Empty payload"); let msg = Self::Message::parse_from_bytes(data).unwrap(); - Ok(Self::parse(&msg, &session)) + Ok(Self::parse(&msg, session)) } } diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 7b5987a3e..8b8962fbf 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -59,7 +59,7 @@ fn list_outputs() -> io::Result<()> { println!("Listing available Alsa outputs:"); for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = match HintIter::new_str(None, &t) { + let i = match HintIter::new_str(None, t) { Ok(i) => i, Err(e) => { return Err(io::Error::new(io::ErrorKind::Other, e)); From fe644bc0d7bf8fb92c256965f24a9aa3253840a1 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 2 Sep 2021 22:04:30 +0200 Subject: [PATCH 11/76] Update default normalisation threshold --- CHANGELOG.md | 1 + playback/src/config.rs | 2 +- src/main.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acf4f7353..834b0bbfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` +- [playback] Updated default normalisation threshold to -2 dBFS ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate diff --git a/playback/src/config.rs b/playback/src/config.rs index 7604f59f8..14c9cf38a 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -151,7 +151,7 @@ impl Default for PlayerConfig { normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), normalisation_pregain: 0.0, - normalisation_threshold: db_to_ratio(-1.0), + normalisation_threshold: db_to_ratio(-2.0), normalisation_attack: Duration::from_millis(5), normalisation_release: Duration::from_millis(100), normalisation_knee: 1.0, diff --git a/src/main.rs b/src/main.rs index d240e2243..c896a3030 100644 --- a/src/main.rs +++ b/src/main.rs @@ -371,7 +371,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", NORMALISATION_THRESHOLD, - "Threshold (dBFS) to prevent clipping. Defaults to -1.0.", + "Threshold (dBFS) to prevent clipping. Defaults to -2.0.", "THRESHOLD", ) .optopt( From 9cb98e9e2180f6de928f9c47872de1379adedf8d Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 2 Sep 2021 22:41:12 +0200 Subject: [PATCH 12/76] Fix typos and define what's "breaking" --- CONTRIBUTING.md | 2 +- README.md | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 907a7c040..1ba24393b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,7 @@ In order to prepare for a PR, you will need to do a couple of things first: Make any changes that you are going to make to the code, but do not commit yet. -Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. +Unless your changes are negligible, please add an entry in the "Unreleased" section of `CHANGELOG.md`. Refer to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) for instructions on how this entry should look like. If your changes break the API such that downstream packages that depend on librespot need to update their source to still compile, you should mark your changes as `(breaking)`. Make sure that the code is correctly formatted by running: ```bash diff --git a/README.md b/README.md index f557cbc45..20afc01b2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Current maintainers are [listed on GitHub](https://github.com/orgs/librespot-org # librespot *librespot* is an open source client library for Spotify. It enables applications to use Spotify's service to control and play music via various backends, and to act as a Spotify Connect receiver. It is an alternative to the official and [now deprecated](https://pyspotify.mopidy.com/en/latest/#libspotify-s-deprecation) closed-source `libspotify`. Additionally, it will provide extra features which are not available in the official library. -_Note: librespot only works with Spotify Premium. This will remain the case. We will not any support features to make librespot compatible with free accounts, such as limited skips and adverts._ +_Note: librespot only works with Spotify Premium. This will remain the case. We will not support any features to make librespot compatible with free accounts, such as limited skips and adverts._ ## Quick start We're available on [crates.io](https://crates.io/crates/librespot) as the _librespot_ package. Simply run `cargo install librespot` to install librespot on your system. Check the wiki for more info and possible [usage options](https://github.com/librespot-org/librespot/wiki/Options). @@ -20,7 +20,7 @@ As the origin by [plietar](https://github.com/plietar/) is no longer actively ma # Documentation Documentation is currently a work in progress, contributions are welcome! -There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder, +There is some brief documentation on how the protocol works in the [docs](https://github.com/librespot-org/librespot/tree/master/docs) folder. [COMPILING.md](https://github.com/librespot-org/librespot/blob/master/COMPILING.md) contains detailed instructions on setting up a development environment, and compiling librespot. More general usage and compilation information is available on the [wiki](https://github.com/librespot-org/librespot/wiki). [CONTRIBUTING.md](https://github.com/librespot-org/librespot/blob/master/CONTRIBUTING.md) also contains our contributing guidelines. @@ -30,26 +30,26 @@ If you wish to learn more about how librespot works overall, the best way is to # Issues & Discussions **We have recently started using Github discussions for general questions and feature requests, as they are a more natural medium for such cases, and allow for upvoting to prioritize feature development. Check them out [here](https://github.com/librespot-org/librespot/discussions). Bugs and issues with the underlying library should still be reported as issues.** -If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, eg. the Spotify URI of the song that caused the crash. +If you run into a bug when using librespot, please search the existing issues before opening a new one. Chances are, we've encountered it before, and have provided a resolution. If not, please open a new one, and where possible, include the backtrace librespot generates on crashing, along with anything we can use to reproduce the issue, e.g. the Spotify URI of the song that caused the crash. # Building -A quick walk through of the build process is outlined here, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). +A quick walkthrough of the build process is outlined below, while a detailed compilation guide can be found [here](https://github.com/librespot-org/librespot/blob/master/COMPILING.md). ## Additional Dependencies We recently switched to using [Rodio](https://github.com/tomaka/rodio) for audio playback by default, hence for macOS and Windows, you should just be able to clone and build librespot (with the command below). For Linux, you will need to run the additional commands below, depending on your distro. -On Debian/Ubuntu, the following command will install these dependencies : +On Debian/Ubuntu, the following command will install these dependencies: ```shell sudo apt-get install build-essential libasound2-dev ``` -On Fedora systems, the following command will install these dependencies : +On Fedora systems, the following command will install these dependencies: ```shell sudo dnf install alsa-lib-devel make gcc ``` -librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends). +librespot currently offers the following selection of [audio backends](https://github.com/librespot-org/librespot/wiki/Audio-Backends): ``` Rodio (default) ALSA @@ -62,7 +62,7 @@ SDL Pipe Subprocess ``` -Please check the corresponding [compiling entry](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) for backend specific dependencies. +Please check the corresponding [Compiling](https://github.com/librespot-org/librespot/wiki/Compiling#general-dependencies) entry on the wiki for backend specific dependencies. Once you've installed the dependencies and cloned this repository you can build *librespot* with the default backend using Cargo. ```shell @@ -93,7 +93,7 @@ A full list of runtime options is available [here](https://github.com/librespot- _Please Note: When using the cache feature, an authentication blob is stored for your account in the cache directory. For security purposes, we recommend that you set directory permissions on the cache directory to `700`._ ## Contact -Come and hang out on gitter if you need help or want to offer some. +Come and hang out on gitter if you need help or want to offer some: https://gitter.im/librespot-org/spotify-connect-resources ## Disclaimer From 7401d6a96eab95950a360d7ea890b828684bb964 Mon Sep 17 00:00:00 2001 From: Matias Date: Mon, 20 Sep 2021 14:20:44 -0300 Subject: [PATCH 13/76] Don't panic on local files (#846) Skip tracks whose Spotify ID can't be found --- CHANGELOG.md | 1 + metadata/src/lib.rs | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 834b0bbfb..83bf64fd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsa`, `gstreamer`, `pulseaudio`: always output in native endianness - [playback] `alsa`: revert buffer size to ~500 ms - [playback] `alsa`, `pipe`, `pulseaudio`: better error handling +- [metadata] Skip tracks whose Spotify ID's can't be found (e.g. local files, which aren't supported) ## [0.2.0] - 2021-05-04 diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index cf663ce6f..2ed9273ec 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -291,10 +291,10 @@ impl Metadata for Playlist { .get_contents() .get_items() .iter() - .map(|item| { + .filter_map(|item| { let uri_split = item.get_uri().split(':'); let uri_parts: Vec<&str> = uri_split.collect(); - SpotifyId::from_base62(uri_parts[2]).unwrap() + SpotifyId::from_base62(uri_parts[2]).ok() }) .collect::>(); From 949ca4fded0ca399f1ec68e090aac4fd83f4ac59 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 20 Sep 2021 19:22:02 +0200 Subject: [PATCH 14/76] Add and default to "auto" normalisation type (#844) --- CHANGELOG.md | 4 +- audio/src/fetch/receive.rs | 3 +- connect/src/spirc.rs | 11 +++++- playback/src/config.rs | 6 ++- playback/src/player.rs | 81 +++++++++++++++++++++++++++++--------- src/main.rs | 2 +- 6 files changed, 82 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83bf64fd1..6235b017e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves - [playback] `alsamixer`: support for querying dB range from Alsa softvol - [playback] Add `--format F64` (supported by Alsa and GStreamer only) +- [playback] Add `--normalisation-type auto` that switches between album and track automatically ### Changed - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) @@ -26,7 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` -- [playback] Updated default normalisation threshold to -2 dBFS +- [playback] `player`: update default normalisation threshold to -2 dBFS +- [playback] `player`: default normalisation type is now `auto` ### Deprecated - [connect] The `discovery` module was deprecated in favor of the `librespot-discovery` crate diff --git a/audio/src/fetch/receive.rs b/audio/src/fetch/receive.rs index 64becc23b..f7574f4ff 100644 --- a/audio/src/fetch/receive.rs +++ b/audio/src/fetch/receive.rs @@ -266,7 +266,8 @@ impl AudioFileFetch { fn handle_file_data(&mut self, data: ReceivedData) -> ControlFlow { match data { ReceivedData::ResponseTime(response_time) => { - trace!("Ping time estimated as: {}ms", response_time.as_millis()); + // chatty + // trace!("Ping time estimated as: {}ms", response_time.as_millis()); // prune old response times. Keep at most two so we can push a third. while self.network_response_times.len() >= 3 { diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 9c541871e..9aa861343 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -902,7 +902,8 @@ impl SpircTask { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } - if self.config.autoplay && new_index == tracks_len - 1 { + let last_track = new_index == tracks_len - 1; + if self.config.autoplay && last_track { // Extend the playlist // Note: This doesn't seem to reflect in the UI // the additional tracks in the frame don't show up as with station view @@ -917,6 +918,11 @@ impl SpircTask { if tracks_len > 0 { self.state.set_playing_track_index(new_index); self.load_track(continue_playing, 0); + if self.config.autoplay && last_track { + // If we're now playing the last track of an album, then + // switch to track normalisation mode for the autoplay to come. + self.player.set_auto_normalise_as_album(false); + } } else { info!("Not playing next track because there are no more tracks left in queue."); self.state.set_playing_track_index(0); @@ -1084,6 +1090,9 @@ impl SpircTask { self.autoplay_fut = self.resolve_autoplay_uri(&context_uri); } + self.player + .set_auto_normalise_as_album(context_uri.starts_with("spotify:album:")); + self.state.set_playing_track_index(index); self.state.set_track(tracks.iter().cloned().collect()); self.state.set_context_uri(context_uri); diff --git a/playback/src/config.rs b/playback/src/config.rs index 14c9cf38a..c442faee7 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -76,10 +76,11 @@ impl AudioFormat { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum NormalisationType { Album, Track, + Auto, } impl FromStr for NormalisationType { @@ -88,6 +89,7 @@ impl FromStr for NormalisationType { match s.to_lowercase().as_ref() { "album" => Ok(Self::Album), "track" => Ok(Self::Track), + "auto" => Ok(Self::Auto), _ => Err(()), } } @@ -95,7 +97,7 @@ impl FromStr for NormalisationType { impl Default for NormalisationType { fn default() -> Self { - Self::Album + Self::Auto } } diff --git a/playback/src/player.rs b/playback/src/player.rs index 361c24a78..d858e333b 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -67,6 +67,8 @@ struct PlayerInternal { limiter_peak_sample: f64, limiter_factor: f64, limiter_strength: f64, + + auto_normalise_as_album: bool, } enum PlayerCommand { @@ -86,6 +88,7 @@ enum PlayerCommand { AddEventSender(mpsc::UnboundedSender), SetSinkEventCallback(Option), EmitVolumeSetEvent(u16), + SetAutoNormaliseAsAlbum(bool), } #[derive(Debug, Clone)] @@ -238,9 +241,10 @@ impl NormalisationData { return 1.0; } - let [gain_db, gain_peak] = match config.normalisation_type { - NormalisationType::Album => [data.album_gain_db, data.album_peak], - NormalisationType::Track => [data.track_gain_db, data.track_peak], + let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album { + [data.album_gain_db, data.album_peak] + } else { + [data.track_gain_db, data.track_peak] }; let normalisation_power = gain_db as f64 + config.normalisation_pregain; @@ -264,7 +268,11 @@ impl NormalisationData { } debug!("Normalisation Data: {:?}", data); - debug!("Normalisation Factor: {:.2}%", normalisation_factor * 100.0); + debug!( + "Calculated Normalisation Factor for {:?}: {:.2}%", + config.normalisation_type, + normalisation_factor * 100.0 + ); normalisation_factor as f64 } @@ -327,6 +335,8 @@ impl Player { limiter_peak_sample: 0.0, limiter_factor: 1.0, limiter_strength: 0.0, + + auto_normalise_as_album: false, }; // While PlayerInternal is written as a future, it still contains blocking code. @@ -406,6 +416,10 @@ impl Player { pub fn emit_volume_set_event(&self, volume: u16) { self.command(PlayerCommand::EmitVolumeSetEvent(volume)); } + + pub fn set_auto_normalise_as_album(&self, setting: bool) { + self.command(PlayerCommand::SetAutoNormaliseAsAlbum(setting)); + } } impl Drop for Player { @@ -423,7 +437,7 @@ impl Drop for Player { struct PlayerLoadedTrackData { decoder: Decoder, - normalisation_factor: f64, + normalisation_data: NormalisationData, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, duration_ms: u32, @@ -456,6 +470,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, + normalisation_data: NormalisationData, normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, @@ -467,6 +482,7 @@ enum PlayerState { track_id: SpotifyId, play_request_id: u64, decoder: Decoder, + normalisation_data: NormalisationData, normalisation_factor: f64, stream_loader_controller: StreamLoaderController, bytes_per_second: usize, @@ -543,7 +559,7 @@ impl PlayerState { decoder, duration_ms, bytes_per_second, - normalisation_factor, + normalisation_data, stream_loader_controller, stream_position_pcm, .. @@ -553,7 +569,7 @@ impl PlayerState { play_request_id, loaded_track: PlayerLoadedTrackData { decoder, - normalisation_factor, + normalisation_data, stream_loader_controller, bytes_per_second, duration_ms, @@ -572,6 +588,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -583,6 +600,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -603,6 +621,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -615,6 +634,7 @@ impl PlayerState { track_id, play_request_id, decoder, + normalisation_data, normalisation_factor, stream_loader_controller, duration_ms, @@ -775,14 +795,16 @@ impl PlayerTrackLoader { let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - let normalisation_factor = match NormalisationData::parse_from_file(&mut decrypted_file) - { - Ok(normalisation_data) => { - NormalisationData::get_factor(&self.config, normalisation_data) - } + let normalisation_data = match NormalisationData::parse_from_file(&mut decrypted_file) { + Ok(data) => data, Err(_) => { warn!("Unable to extract normalisation data, using default value."); - 1.0 + NormalisationData { + track_gain_db: 0.0, + track_peak: 1.0, + album_gain_db: 0.0, + album_peak: 1.0, + } } }; @@ -838,7 +860,7 @@ impl PlayerTrackLoader { return Some(PlayerLoadedTrackData { decoder, - normalisation_factor, + normalisation_data, stream_loader_controller, bytes_per_second, duration_ms, @@ -1339,6 +1361,17 @@ impl PlayerInternal { ) { let position_ms = Self::position_pcm_to_ms(loaded_track.stream_position_pcm); + let mut config = self.config.clone(); + if config.normalisation_type == NormalisationType::Auto { + if self.auto_normalise_as_album { + config.normalisation_type = NormalisationType::Album; + } else { + config.normalisation_type = NormalisationType::Track; + } + }; + let normalisation_factor = + NormalisationData::get_factor(&config, loaded_track.normalisation_data); + if start_playback { self.ensure_sink_running(); @@ -1353,7 +1386,8 @@ impl PlayerInternal { track_id, play_request_id, decoder: loaded_track.decoder, - normalisation_factor: loaded_track.normalisation_factor, + normalisation_data: loaded_track.normalisation_data, + normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, @@ -1370,7 +1404,8 @@ impl PlayerInternal { track_id, play_request_id, decoder: loaded_track.decoder, - normalisation_factor: loaded_track.normalisation_factor, + normalisation_data: loaded_track.normalisation_data, + normalisation_factor, stream_loader_controller: loaded_track.stream_loader_controller, duration_ms: loaded_track.duration_ms, bytes_per_second: loaded_track.bytes_per_second, @@ -1497,7 +1532,7 @@ impl PlayerInternal { stream_loader_controller, bytes_per_second, duration_ms, - normalisation_factor, + normalisation_data, .. } | PlayerState::Paused { @@ -1506,13 +1541,13 @@ impl PlayerInternal { stream_loader_controller, bytes_per_second, duration_ms, - normalisation_factor, + normalisation_data, .. } = old_state { let loaded_track = PlayerLoadedTrackData { decoder, - normalisation_factor, + normalisation_data, stream_loader_controller, bytes_per_second, duration_ms, @@ -1750,6 +1785,10 @@ impl PlayerInternal { PlayerCommand::EmitVolumeSetEvent(volume) => { self.send_event(PlayerEvent::VolumeSet { volume }) } + + PlayerCommand::SetAutoNormaliseAsAlbum(setting) => { + self.auto_normalise_as_album = setting + } } } @@ -1855,6 +1894,10 @@ impl ::std::fmt::Debug for PlayerCommand { PlayerCommand::EmitVolumeSetEvent(volume) => { f.debug_tuple("VolumeSet").field(&volume).finish() } + PlayerCommand::SetAutoNormaliseAsAlbum(setting) => f + .debug_tuple("SetAutoNormaliseAsAlbum") + .field(&setting) + .finish(), } } } diff --git a/src/main.rs b/src/main.rs index c896a3030..76e8ba1c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -359,7 +359,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", NORMALISATION_GAIN_TYPE, - "Specify the normalisation gain type to use {track|album}. Defaults to album.", + "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", "TYPE", ) .optopt( From 89577d1fc130805695910b1330af2ce6dc89fa02 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 20 Sep 2021 12:29:12 -0500 Subject: [PATCH 15/76] Improve player (#823) * Improve error handling * Harmonize `Seek`: Make the decoders and player use the same math for converting between samples and milliseconds * Reduce duplicate calls: Make decoder seek in PCM, not ms * Simplify decoder errors with `thiserror` --- CHANGELOG.md | 2 +- playback/Cargo.toml | 10 +- playback/src/audio_backend/jackaudio.rs | 5 +- playback/src/audio_backend/portaudio.rs | 5 +- playback/src/audio_backend/rodio.rs | 4 +- playback/src/audio_backend/sdl.rs | 4 +- playback/src/decoder/lewton_decoder.rs | 58 ++--- playback/src/decoder/mod.rs | 69 +++-- playback/src/decoder/passthrough_decoder.rs | 89 +++---- playback/src/lib.rs | 2 + playback/src/player.rs | 274 +++++++++++++------- 11 files changed, 280 insertions(+), 242 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6235b017e..8a056bc2e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Add `--normalisation-type auto` that switches between album and track automatically ### Changed -- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `AudioError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) +- [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) - [audio, playback] Use `Duration` for time constants and functions (breaking) - [connect, playback] Moved volume controls from `librespot-connect` to `librespot-playback` crate - [connect] Synchronize player volume with mixer volume on playback diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 8211f2bd5..f2fdaf482 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -25,6 +25,7 @@ byteorder = "1.4" shell-words = "1.0.0" tokio = { version = "1", features = ["sync"] } zerocopy = { version = "0.3" } +thiserror = { version = "1" } # Backends alsa = { version = "0.5", optional = true } @@ -40,7 +41,6 @@ glib = { version = "0.10", optional = true } # Rodio dependencies rodio = { version = "0.14", optional = true, default-features = false } cpal = { version = "0.13", optional = true } -thiserror = { version = "1", optional = true } # Decoder lewton = "0.10" @@ -51,11 +51,11 @@ rand = "0.8" rand_distr = "0.4" [features] -alsa-backend = ["alsa", "thiserror"] +alsa-backend = ["alsa"] portaudio-backend = ["portaudio-rs"] -pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding", "thiserror"] +pulseaudio-backend = ["libpulse-binding", "libpulse-simple-binding"] jackaudio-backend = ["jack"] -rodio-backend = ["rodio", "cpal", "thiserror"] -rodiojack-backend = ["rodio", "cpal/jack", "thiserror"] +rodio-backend = ["rodio", "cpal"] +rodiojack-backend = ["rodio", "cpal/jack"] sdl-backend = ["sdl2"] gstreamer-backend = ["gstreamer", "gstreamer-app", "glib"] diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index f55f20a84..a8f37524c 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -71,7 +71,10 @@ impl Open for JackSink { impl Sink for JackSink { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { - let samples_f32: &[f32] = &converter.f64_to_f32(packet.samples()); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let samples_f32: &[f32] = &converter.f64_to_f32(samples); for sample in samples_f32.iter() { let res = self.send.send(*sample); if res.is_err() { diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 378deb485..26355a03c 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -148,7 +148,10 @@ impl<'a> Sink for PortAudioSink<'a> { }; } - let samples = packet.samples(); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + let result = match self { Self::F32(stream, _parameters) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 1e9999382..4d9c65c5c 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -176,7 +176,9 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro impl Sink for RodioSink { fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { - let samples = packet.samples(); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; match self.format { AudioFormat::F32 => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 28d140e80..63a88c225 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -92,7 +92,9 @@ impl Sink for SdlSink { }}; } - let samples = packet.samples(); + let samples = packet + .samples() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; match self { Self::F32(queue) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/decoder/lewton_decoder.rs b/playback/src/decoder/lewton_decoder.rs index adf63e2af..bc90b9926 100644 --- a/playback/src/decoder/lewton_decoder.rs +++ b/playback/src/decoder/lewton_decoder.rs @@ -1,22 +1,23 @@ -use super::{AudioDecoder, AudioError, AudioPacket}; +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; +use lewton::audio::AudioReadError::AudioIsHeader; use lewton::inside_ogg::OggStreamReader; use lewton::samples::InterleavedSamples; +use lewton::OggReadError::NoCapturePatternFound; +use lewton::VorbisError::{BadAudio, OggError}; -use std::error; -use std::fmt; use std::io::{Read, Seek}; -use std::time::Duration; pub struct VorbisDecoder(OggStreamReader); -pub struct VorbisError(lewton::VorbisError); impl VorbisDecoder where R: Read + Seek, { - pub fn new(input: R) -> Result, VorbisError> { - Ok(VorbisDecoder(OggStreamReader::new(input)?)) + pub fn new(input: R) -> DecoderResult> { + let reader = + OggStreamReader::new(input).map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; + Ok(VorbisDecoder(reader)) } } @@ -24,51 +25,22 @@ impl AudioDecoder for VorbisDecoder where R: Read + Seek, { - fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - let absgp = Duration::from_millis(ms as u64 * crate::SAMPLE_RATE as u64).as_secs(); - match self.0.seek_absgp_pg(absgp as u64) { - Ok(_) => Ok(()), - Err(err) => Err(AudioError::VorbisError(err.into())), - } + fn seek(&mut self, absgp: u64) -> DecoderResult<()> { + self.0 + .seek_absgp_pg(absgp) + .map_err(|e| DecoderError::LewtonDecoder(e.to_string()))?; + Ok(()) } - fn next_packet(&mut self) -> Result, AudioError> { - use lewton::audio::AudioReadError::AudioIsHeader; - use lewton::OggReadError::NoCapturePatternFound; - use lewton::VorbisError::{BadAudio, OggError}; + fn next_packet(&mut self) -> DecoderResult> { loop { match self.0.read_dec_packet_generic::>() { Ok(Some(packet)) => return Ok(Some(AudioPacket::samples_from_f32(packet.samples))), Ok(None) => return Ok(None), - Err(BadAudio(AudioIsHeader)) => (), Err(OggError(NoCapturePatternFound)) => (), - Err(err) => return Err(AudioError::VorbisError(err.into())), + Err(e) => return Err(DecoderError::LewtonDecoder(e.to_string())), } } } } - -impl From for VorbisError { - fn from(err: lewton::VorbisError) -> VorbisError { - VorbisError(err) - } -} - -impl fmt::Debug for VorbisError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl fmt::Display for VorbisError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} - -impl error::Error for VorbisError { - fn source(&self) -> Option<&(dyn error::Error + 'static)> { - error::Error::source(&self.0) - } -} diff --git a/playback/src/decoder/mod.rs b/playback/src/decoder/mod.rs index 9641e8b35..087bba4cc 100644 --- a/playback/src/decoder/mod.rs +++ b/playback/src/decoder/mod.rs @@ -1,10 +1,30 @@ -use std::fmt; +use thiserror::Error; mod lewton_decoder; -pub use lewton_decoder::{VorbisDecoder, VorbisError}; +pub use lewton_decoder::VorbisDecoder; mod passthrough_decoder; -pub use passthrough_decoder::{PassthroughDecoder, PassthroughError}; +pub use passthrough_decoder::PassthroughDecoder; + +#[derive(Error, Debug)] +pub enum DecoderError { + #[error("Lewton Decoder Error: {0}")] + LewtonDecoder(String), + #[error("Passthrough Decoder Error: {0}")] + PassthroughDecoder(String), +} + +pub type DecoderResult = Result; + +#[derive(Error, Debug)] +pub enum AudioPacketError { + #[error("Decoder OggData Error: Can't return OggData on Samples")] + OggData, + #[error("Decoder Samples Error: Can't return Samples on OggData")] + Samples, +} + +pub type AudioPacketResult = Result; pub enum AudioPacket { Samples(Vec), @@ -17,17 +37,17 @@ impl AudioPacket { AudioPacket::Samples(f64_samples) } - pub fn samples(&self) -> &[f64] { + pub fn samples(&self) -> AudioPacketResult<&[f64]> { match self { - AudioPacket::Samples(s) => s, - AudioPacket::OggData(_) => panic!("can't return OggData on samples"), + AudioPacket::Samples(s) => Ok(s), + AudioPacket::OggData(_) => Err(AudioPacketError::OggData), } } - pub fn oggdata(&self) -> &[u8] { + pub fn oggdata(&self) -> AudioPacketResult<&[u8]> { match self { - AudioPacket::Samples(_) => panic!("can't return samples on OggData"), - AudioPacket::OggData(d) => d, + AudioPacket::OggData(d) => Ok(d), + AudioPacket::Samples(_) => Err(AudioPacketError::Samples), } } @@ -39,34 +59,7 @@ impl AudioPacket { } } -#[derive(Debug)] -pub enum AudioError { - PassthroughError(PassthroughError), - VorbisError(VorbisError), -} - -impl fmt::Display for AudioError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - match self { - AudioError::PassthroughError(err) => write!(f, "PassthroughError({})", err), - AudioError::VorbisError(err) => write!(f, "VorbisError({})", err), - } - } -} - -impl From for AudioError { - fn from(err: VorbisError) -> AudioError { - AudioError::VorbisError(err) - } -} - -impl From for AudioError { - fn from(err: PassthroughError) -> AudioError { - AudioError::PassthroughError(err) - } -} - pub trait AudioDecoder { - fn seek(&mut self, ms: i64) -> Result<(), AudioError>; - fn next_packet(&mut self) -> Result, AudioError>; + fn seek(&mut self, absgp: u64) -> DecoderResult<()>; + fn next_packet(&mut self) -> DecoderResult>; } diff --git a/playback/src/decoder/passthrough_decoder.rs b/playback/src/decoder/passthrough_decoder.rs index 7c1ad5329..dd8e3b32e 100644 --- a/playback/src/decoder/passthrough_decoder.rs +++ b/playback/src/decoder/passthrough_decoder.rs @@ -1,23 +1,22 @@ // Passthrough decoder for librespot -use super::{AudioDecoder, AudioError, AudioPacket}; -use crate::SAMPLE_RATE; +use super::{AudioDecoder, AudioPacket, DecoderError, DecoderResult}; use ogg::{OggReadError, Packet, PacketReader, PacketWriteEndInfo, PacketWriter}; -use std::fmt; use std::io::{Read, Seek}; -use std::time::Duration; use std::time::{SystemTime, UNIX_EPOCH}; -fn get_header(code: u8, rdr: &mut PacketReader) -> Result, PassthroughError> +fn get_header(code: u8, rdr: &mut PacketReader) -> DecoderResult> where T: Read + Seek, { - let pck: Packet = rdr.read_packet_expected()?; + let pck: Packet = rdr + .read_packet_expected() + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; let pkt_type = pck.data[0]; debug!("Vorbis header type {}", &pkt_type); if pkt_type != code { - return Err(PassthroughError(OggReadError::InvalidData)); + return Err(DecoderError::PassthroughDecoder("Invalid Data".to_string())); } Ok(pck.data.into_boxed_slice()) @@ -35,16 +34,14 @@ pub struct PassthroughDecoder { setup: Box<[u8]>, } -pub struct PassthroughError(ogg::OggReadError); - impl PassthroughDecoder { /// Constructs a new Decoder from a given implementation of `Read + Seek`. - pub fn new(rdr: R) -> Result { + pub fn new(rdr: R) -> DecoderResult { let mut rdr = PacketReader::new(rdr); - let stream_serial = SystemTime::now() + let since_epoch = SystemTime::now() .duration_since(UNIX_EPOCH) - .unwrap() - .as_millis() as u32; + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; + let stream_serial = since_epoch.as_millis() as u32; info!("Starting passthrough track with serial {}", stream_serial); @@ -71,9 +68,7 @@ impl PassthroughDecoder { } impl AudioDecoder for PassthroughDecoder { - fn seek(&mut self, ms: i64) -> Result<(), AudioError> { - info!("Seeking to {}", ms); - + fn seek(&mut self, absgp: u64) -> DecoderResult<()> { // add an eos to previous stream if missing if self.bos && !self.eos { match self.rdr.read_packet() { @@ -86,7 +81,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::EndStream, absgp_page, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; } _ => warn! {"Cannot write EoS after seeking"}, }; @@ -97,23 +92,29 @@ impl AudioDecoder for PassthroughDecoder { self.ofsgp_page = 0; self.stream_serial += 1; - // hard-coded to 44.1 kHz - match self.rdr.seek_absgp( - None, - Duration::from_millis(ms as u64 * SAMPLE_RATE as u64).as_secs(), - ) { + match self.rdr.seek_absgp(None, absgp) { Ok(_) => { // need to set some offset for next_page() - let pck = self.rdr.read_packet().unwrap().unwrap(); - self.ofsgp_page = pck.absgp_page(); - debug!("Seek to offset page {}", self.ofsgp_page); - Ok(()) + let pck = self + .rdr + .read_packet() + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; + match pck { + Some(pck) => { + self.ofsgp_page = pck.absgp_page(); + debug!("Seek to offset page {}", self.ofsgp_page); + Ok(()) + } + None => Err(DecoderError::PassthroughDecoder( + "Packet is None".to_string(), + )), + } } - Err(err) => Err(AudioError::PassthroughError(err.into())), + Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())), } } - fn next_packet(&mut self) -> Result, AudioError> { + fn next_packet(&mut self) -> DecoderResult> { // write headers if we are (re)starting if !self.bos { self.wtr @@ -123,7 +124,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::EndPage, 0, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; self.wtr .write_packet( self.comment.clone(), @@ -131,7 +132,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::NormalPacket, 0, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; self.wtr .write_packet( self.setup.clone(), @@ -139,7 +140,7 @@ impl AudioDecoder for PassthroughDecoder { PacketWriteEndInfo::EndPage, 0, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; self.bos = true; debug!("Wrote Ogg headers"); } @@ -151,7 +152,7 @@ impl AudioDecoder for PassthroughDecoder { info!("end of streaming"); return Ok(None); } - Err(err) => return Err(AudioError::PassthroughError(err.into())), + Err(e) => return Err(DecoderError::PassthroughDecoder(e.to_string())), }; let pckgp_page = pck.absgp_page(); @@ -178,32 +179,14 @@ impl AudioDecoder for PassthroughDecoder { inf, pckgp_page - self.ofsgp_page, ) - .unwrap(); + .map_err(|e| DecoderError::PassthroughDecoder(e.to_string()))?; let data = self.wtr.inner_mut(); if !data.is_empty() { - let result = AudioPacket::OggData(std::mem::take(data)); - return Ok(Some(result)); + let ogg_data = AudioPacket::OggData(std::mem::take(data)); + return Ok(Some(ogg_data)); } } } } - -impl fmt::Debug for PassthroughError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Debug::fmt(&self.0, f) - } -} - -impl From for PassthroughError { - fn from(err: OggReadError) -> PassthroughError { - PassthroughError(err) - } -} - -impl fmt::Display for PassthroughError { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - fmt::Display::fmt(&self.0, f) - } -} diff --git a/playback/src/lib.rs b/playback/src/lib.rs index e39dfc7c4..a52ca2fa4 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -16,3 +16,5 @@ pub mod player; pub const SAMPLE_RATE: u32 = 44100; pub const NUM_CHANNELS: u8 = 2; pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE as u32 * NUM_CHANNELS as u32; +pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0; +pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64; diff --git a/playback/src/player.rs b/playback/src/player.rs index d858e333b..a7ff916d2 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -23,11 +23,11 @@ use crate::convert::Converter; use crate::core::session::Session; use crate::core::spotify_id::SpotifyId; use crate::core::util::SeqGenerator; -use crate::decoder::{AudioDecoder, AudioError, AudioPacket, PassthroughDecoder, VorbisDecoder}; +use crate::decoder::{AudioDecoder, AudioPacket, DecoderError, PassthroughDecoder, VorbisDecoder}; use crate::metadata::{AudioItem, FileFormat}; use crate::mixer::AudioFilter; -use crate::{NUM_CHANNELS, SAMPLES_PER_SECOND}; +use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; @@ -356,7 +356,11 @@ impl Player { } fn command(&self, cmd: PlayerCommand) { - self.commands.as_ref().unwrap().send(cmd).unwrap(); + if let Some(commands) = self.commands.as_ref() { + if let Err(e) = commands.send(cmd) { + error!("Player Commands Error: {}", e); + } + } } pub fn load(&mut self, track_id: SpotifyId, start_playing: bool, position_ms: u32) -> u64 { @@ -429,7 +433,7 @@ impl Drop for Player { if let Some(handle) = self.thread_handle.take() { match handle.join() { Ok(_) => (), - Err(_) => error!("Player thread panicked!"), + Err(e) => error!("Player thread Error: {:?}", e), } } } @@ -505,7 +509,10 @@ impl PlayerState { match *self { Stopped | EndOfTrack { .. } | Paused { .. } | Loading { .. } => false, Playing { .. } => true, - Invalid => panic!("invalid state"), + Invalid => { + error!("PlayerState is_playing: invalid state"); + exit(1); + } } } @@ -530,7 +537,10 @@ impl PlayerState { | Playing { ref mut decoder, .. } => Some(decoder), - Invalid => panic!("invalid state"), + Invalid => { + error!("PlayerState decoder: invalid state"); + exit(1); + } } } @@ -546,7 +556,10 @@ impl PlayerState { ref mut stream_loader_controller, .. } => Some(stream_loader_controller), - Invalid => panic!("invalid state"), + Invalid => { + error!("PlayerState stream_loader_controller: invalid state"); + exit(1); + } } } @@ -577,7 +590,10 @@ impl PlayerState { }, }; } - _ => panic!("Called playing_to_end_of_track in non-playing state."), + _ => { + error!("Called playing_to_end_of_track in non-playing state."); + exit(1); + } } } @@ -610,7 +626,10 @@ impl PlayerState { suggested_to_preload_next_track, }; } - _ => panic!("invalid state"), + _ => { + error!("PlayerState paused_to_playing: invalid state"); + exit(1); + } } } @@ -643,7 +662,10 @@ impl PlayerState { suggested_to_preload_next_track, }; } - _ => panic!("invalid state"), + _ => { + error!("PlayerState playing_to_paused: invalid state"); + exit(1); + } } } } @@ -699,8 +721,8 @@ impl PlayerTrackLoader { ) -> Option { let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await { Ok(audio) => audio, - Err(_) => { - error!("Unable to load audio item."); + Err(e) => { + error!("Unable to load audio item: {:?}", e); return None; } }; @@ -768,8 +790,8 @@ impl PlayerTrackLoader { let encrypted_file = match encrypted_file.await { Ok(encrypted_file) => encrypted_file, - Err(_) => { - error!("Unable to load encrypted file."); + Err(e) => { + error!("Unable to load encrypted file: {:?}", e); return None; } }; @@ -787,8 +809,8 @@ impl PlayerTrackLoader { let key = match self.session.audio_key().request(spotify_id, file_id).await { Ok(key) => key, - Err(_) => { - error!("Unable to load decryption key"); + Err(e) => { + error!("Unable to load decryption key: {:?}", e); return None; } }; @@ -813,12 +835,12 @@ impl PlayerTrackLoader { let result = if self.config.passthrough { match PassthroughDecoder::new(audio_file) { Ok(result) => Ok(Box::new(result) as Decoder), - Err(e) => Err(AudioError::PassthroughError(e)), + Err(e) => Err(DecoderError::PassthroughDecoder(e.to_string())), } } else { match VorbisDecoder::new(audio_file) { Ok(result) => Ok(Box::new(result) as Decoder), - Err(e) => Err(AudioError::VorbisError(e)), + Err(e) => Err(DecoderError::LewtonDecoder(e.to_string())), } }; @@ -830,14 +852,17 @@ impl PlayerTrackLoader { e ); - if self - .session - .cache() - .expect("If the audio file is cached, a cache should exist") - .remove_file(file_id) - .is_err() - { - return None; + match self.session.cache() { + Some(cache) => { + if cache.remove_file(file_id).is_err() { + error!("Error removing file from cache"); + return None; + } + } + None => { + error!("If the audio file is cached, a cache should exist"); + return None; + } } // Just try it again @@ -849,13 +874,15 @@ impl PlayerTrackLoader { } }; - if position_ms != 0 { - if let Err(err) = decoder.seek(position_ms as i64) { - error!("Vorbis error: {}", err); + let position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); + + if position_pcm != 0 { + if let Err(e) = decoder.seek(position_pcm) { + error!("PlayerTrackLoader load_track: {}", e); } stream_loader_controller.set_stream_mode(); } - let stream_position_pcm = PlayerInternal::position_ms_to_pcm(position_ms); + let stream_position_pcm = position_pcm; info!("<{}> ({} ms) loaded", audio.name, audio.duration); return Some(PlayerLoadedTrackData { @@ -912,7 +939,8 @@ impl Future for PlayerInternal { start_playback, ); if let PlayerState::Loading { .. } = self.state { - panic!("The state wasn't changed by start_playback()"); + error!("The state wasn't changed by start_playback()"); + exit(1); } } Poll::Ready(Err(_)) => { @@ -976,47 +1004,67 @@ impl Future for PlayerInternal { .. } = self.state { - let packet = decoder.next_packet().expect("Vorbis error"); - - if !passthrough { - if let Some(ref packet) = packet { - *stream_position_pcm += - (packet.samples().len() / NUM_CHANNELS as usize) as u64; - let stream_position_millis = - Self::position_pcm_to_ms(*stream_position_pcm); - - let notify_about_position = match *reported_nominal_start_time { - None => true, - Some(reported_nominal_start_time) => { - // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we're actually in time. - let lag = (Instant::now() - reported_nominal_start_time) - .as_millis() - as i64 - - stream_position_millis as i64; - lag > Duration::from_secs(1).as_millis() as i64 + match decoder.next_packet() { + Ok(packet) => { + if !passthrough { + if let Some(ref packet) = packet { + match packet.samples() { + Ok(samples) => { + *stream_position_pcm += + (samples.len() / NUM_CHANNELS as usize) as u64; + let stream_position_millis = + Self::position_pcm_to_ms(*stream_position_pcm); + + let notify_about_position = + match *reported_nominal_start_time { + None => true, + Some(reported_nominal_start_time) => { + // only notify if we're behind. If we're ahead it's probably due to a buffer of the backend and we're actually in time. + let lag = (Instant::now() + - reported_nominal_start_time) + .as_millis() + as i64 + - stream_position_millis as i64; + lag > Duration::from_secs(1).as_millis() + as i64 + } + }; + if notify_about_position { + *reported_nominal_start_time = Some( + Instant::now() + - Duration::from_millis( + stream_position_millis as u64, + ), + ); + self.send_event(PlayerEvent::Playing { + track_id, + play_request_id, + position_ms: stream_position_millis as u32, + duration_ms, + }); + } + } + Err(e) => { + error!("PlayerInternal poll: {}", e); + exit(1); + } + } } - }; - if notify_about_position { - *reported_nominal_start_time = Some( - Instant::now() - - Duration::from_millis(stream_position_millis as u64), - ); - self.send_event(PlayerEvent::Playing { - track_id, - play_request_id, - position_ms: stream_position_millis as u32, - duration_ms, - }); + } else { + // position, even if irrelevant, must be set so that seek() is called + *stream_position_pcm = duration_ms.into(); } + + self.handle_packet(packet, normalisation_factor); + } + Err(e) => { + error!("PlayerInternal poll: {}", e); + exit(1); } - } else { - // position, even if irrelevant, must be set so that seek() is called - *stream_position_pcm = duration_ms.into(); } - - self.handle_packet(packet, normalisation_factor); } else { - unreachable!(); + error!("PlayerInternal poll: Invalid PlayerState"); + exit(1); }; } @@ -1065,11 +1113,11 @@ impl Future for PlayerInternal { impl PlayerInternal { fn position_pcm_to_ms(position_pcm: u64) -> u32 { - (position_pcm * 10 / 441) as u32 + (position_pcm as f64 * MS_PER_PAGE) as u32 } fn position_ms_to_pcm(position_ms: u32) -> u64 { - position_ms as u64 * 441 / 10 + (position_ms as f64 * PAGES_PER_MS) as u64 } fn ensure_sink_running(&mut self) { @@ -1080,8 +1128,8 @@ impl PlayerInternal { } match self.sink.start() { Ok(()) => self.sink_status = SinkStatus::Running, - Err(err) => { - error!("Fatal error, could not start audio sink: {}", err); + Err(e) => { + error!("{}", e); exit(1); } } @@ -1103,8 +1151,8 @@ impl PlayerInternal { callback(self.sink_status); } } - Err(err) => { - error!("Fatal error, could not stop audio sink: {}", err); + Err(e) => { + error!("{}", e); exit(1); } } @@ -1151,7 +1199,10 @@ impl PlayerInternal { self.state = PlayerState::Stopped; } PlayerState::Stopped => (), - PlayerState::Invalid => panic!("invalid state"), + PlayerState::Invalid => { + error!("PlayerInternal handle_player_stop: invalid state"); + exit(1); + } } } @@ -1317,8 +1368,8 @@ impl PlayerInternal { } } - if let Err(err) = self.sink.write(&packet, &mut self.converter) { - error!("Fatal error, could not write audio to audio sink: {}", err); + if let Err(e) = self.sink.write(&packet, &mut self.converter) { + error!("{}", e); exit(1); } } @@ -1337,7 +1388,8 @@ impl PlayerInternal { play_request_id, }) } else { - unreachable!(); + error!("PlayerInternal handle_packet: Invalid PlayerState"); + exit(1); } } } @@ -1458,7 +1510,10 @@ impl PlayerInternal { play_request_id, position_ms, }), - PlayerState::Invalid { .. } => panic!("Player is in an invalid state."), + PlayerState::Invalid { .. } => { + error!("PlayerInternal handle_command_load: invalid state"); + exit(1); + } } // Now we check at different positions whether we already have a pre-loaded version @@ -1474,24 +1529,30 @@ impl PlayerInternal { if previous_track_id == track_id { let mut loaded_track = match mem::replace(&mut self.state, PlayerState::Invalid) { PlayerState::EndOfTrack { loaded_track, .. } => loaded_track, - _ => unreachable!(), + _ => { + error!("PlayerInternal handle_command_load: Invalid PlayerState"); + exit(1); + } }; - if Self::position_ms_to_pcm(position_ms) != loaded_track.stream_position_pcm { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + if position_pcm != loaded_track.stream_position_pcm { loaded_track .stream_loader_controller .set_random_access_mode(); - let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking. - // But most likely the track is fully - // loaded already because we played - // to the end of it. + if let Err(e) = loaded_track.decoder.seek(position_pcm) { + // This may be blocking. + error!("PlayerInternal handle_command_load: {}", e); + } loaded_track.stream_loader_controller.set_stream_mode(); - loaded_track.stream_position_pcm = Self::position_ms_to_pcm(position_ms); + loaded_track.stream_position_pcm = position_pcm; } self.preload = PlayerPreload::None; self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - panic!("start_playback() hasn't set a valid player state."); + error!("start_playback() hasn't set a valid player state."); + exit(1); } return; } @@ -1515,11 +1576,16 @@ impl PlayerInternal { { if current_track_id == track_id { // we can use the current decoder. Ensure it's at the correct position. - if Self::position_ms_to_pcm(position_ms) != *stream_position_pcm { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + if position_pcm != *stream_position_pcm { stream_loader_controller.set_random_access_mode(); - let _ = decoder.seek(position_ms as i64); // This may be blocking. + if let Err(e) = decoder.seek(position_pcm) { + // This may be blocking. + error!("PlayerInternal handle_command_load: {}", e); + } stream_loader_controller.set_stream_mode(); - *stream_position_pcm = Self::position_ms_to_pcm(position_ms); + *stream_position_pcm = position_pcm; } // Move the info from the current state into a PlayerLoadedTrackData so we can use @@ -1558,12 +1624,14 @@ impl PlayerInternal { self.start_playback(track_id, play_request_id, loaded_track, play); if let PlayerState::Invalid = self.state { - panic!("start_playback() hasn't set a valid player state."); + error!("start_playback() hasn't set a valid player state."); + exit(1); } return; } else { - unreachable!(); + error!("PlayerInternal handle_command_load: Invalid PlayerState"); + exit(1); } } } @@ -1581,17 +1649,23 @@ impl PlayerInternal { mut loaded_track, } = preload { - if Self::position_ms_to_pcm(position_ms) != loaded_track.stream_position_pcm { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + if position_pcm != loaded_track.stream_position_pcm { loaded_track .stream_loader_controller .set_random_access_mode(); - let _ = loaded_track.decoder.seek(position_ms as i64); // This may be blocking + if let Err(e) = loaded_track.decoder.seek(position_pcm) { + // This may be blocking + error!("PlayerInternal handle_command_load: {}", e); + } loaded_track.stream_loader_controller.set_stream_mode(); } self.start_playback(track_id, play_request_id, *loaded_track, play); return; } else { - unreachable!(); + error!("PlayerInternal handle_command_load: Invalid PlayerState"); + exit(1); } } } @@ -1697,7 +1771,9 @@ impl PlayerInternal { stream_loader_controller.set_random_access_mode(); } if let Some(decoder) = self.state.decoder() { - match decoder.seek(position_ms as i64) { + let position_pcm = Self::position_ms_to_pcm(position_ms); + + match decoder.seek(position_pcm) { Ok(_) => { if let PlayerState::Playing { ref mut stream_position_pcm, @@ -1708,10 +1784,10 @@ impl PlayerInternal { .. } = self.state { - *stream_position_pcm = Self::position_ms_to_pcm(position_ms); + *stream_position_pcm = position_pcm; } } - Err(err) => error!("Vorbis error: {:?}", err), + Err(e) => error!("PlayerInternal handle_command_seek: {}", e), } } else { warn!("Player::seek called from invalid state"); @@ -1954,7 +2030,9 @@ struct Subfile { impl Subfile { pub fn new(mut stream: T, offset: u64) -> Subfile { - stream.seek(SeekFrom::Start(offset)).unwrap(); + if let Err(e) = stream.seek(SeekFrom::Start(offset)) { + error!("Subfile new Error: {}", e); + } Subfile { stream, offset } } } From de177f1260d839b5b49808ba1115b515e3781772 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 20 Sep 2021 20:12:57 +0200 Subject: [PATCH 16/76] Update num-bigint --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6d631c831..e94d21b63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1508,9 +1508,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.0" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e0d047c1062aa51e256408c560894e5251f08925980e53cf1aa5bd00eec6512" +checksum = "74e768dff5fb39a41b3bcd30bb25cf989706c90d028d1ad71971987aa309d535" dependencies = [ "autocfg", "num-integer", From 8d70fd910eda39a7a927ddcf26579d4cbb9188ae Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 27 Sep 2021 13:46:26 -0500 Subject: [PATCH 17/76] Implement common SinkError and SinkResult (#820) * Make error messages more consistent and concise. * `impl From for io::Error` so `AlsaErrors` can be thrown to player as `io::Errors`. This little bit of boilerplate goes a long way to simplifying things further down in the code. And will make any needed future changes easier. * Bonus: handle ALSA backend buffer sizing a little better. --- playback/src/audio_backend/alsa.rs | 201 ++++++++++++----------- playback/src/audio_backend/gstreamer.rs | 6 +- playback/src/audio_backend/jackaudio.rs | 8 +- playback/src/audio_backend/mod.rs | 26 ++- playback/src/audio_backend/pipe.rs | 18 +- playback/src/audio_backend/portaudio.rs | 11 +- playback/src/audio_backend/pulseaudio.rs | 148 +++++++++-------- playback/src/audio_backend/rodio.rs | 32 +++- playback/src/audio_backend/sdl.rs | 12 +- playback/src/audio_backend/subprocess.rs | 43 +++-- 10 files changed, 280 insertions(+), 225 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 8b8962fbf..177988681 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -7,7 +7,6 @@ use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; -use std::io; use std::process::exit; use std::time::Duration; use thiserror::Error; @@ -18,34 +17,67 @@ const BUFFER_TIME: Duration = Duration::from_millis(500); #[derive(Debug, Error)] enum AlsaError { - #[error("AlsaSink, device {device} may be invalid or busy, {err}")] - PcmSetUp { device: String, err: alsa::Error }, - #[error("AlsaSink, device {device} unsupported access type RWInterleaved, {err}")] - UnsupportedAccessType { device: String, err: alsa::Error }, - #[error("AlsaSink, device {device} unsupported format {format:?}, {err}")] + #[error(" Device {device} Unsupported Format {alsa_format:?} ({format:?}), {e}")] UnsupportedFormat { device: String, + alsa_format: Format, format: AudioFormat, - err: alsa::Error, + e: alsa::Error, }, - #[error("AlsaSink, device {device} unsupported sample rate {samplerate}, {err}")] - UnsupportedSampleRate { - device: String, - samplerate: u32, - err: alsa::Error, - }, - #[error("AlsaSink, device {device} unsupported channel count {channel_count}, {err}")] + + #[error(" Device {device} Unsupported Channel Count {channel_count}, {e}")] UnsupportedChannelCount { device: String, channel_count: u8, - err: alsa::Error, + e: alsa::Error, + }, + + #[error(" Device {device} Unsupported Sample Rate {samplerate}, {e}")] + UnsupportedSampleRate { + device: String, + samplerate: u32, + e: alsa::Error, }, - #[error("AlsaSink Hardware Parameters Error, {0}")] + + #[error(" Device {device} Unsupported Access Type RWInterleaved, {e}")] + UnsupportedAccessType { device: String, e: alsa::Error }, + + #[error(" Device {device} May be Invalid, Busy, or Already in Use, {e}")] + PcmSetUp { device: String, e: alsa::Error }, + + #[error(" Failed to Drain PCM Buffer, {0}")] + DrainFailure(alsa::Error), + + #[error(" {0}")] + OnWrite(alsa::Error), + + #[error(" Hardware, {0}")] HwParams(alsa::Error), - #[error("AlsaSink Software Parameters Error, {0}")] + + #[error(" Software, {0}")] SwParams(alsa::Error), - #[error("AlsaSink PCM Error, {0}")] + + #[error(" PCM, {0}")] Pcm(alsa::Error), + + #[error(" Could Not Parse Ouput Name(s) and/or Description(s)")] + Parsing, + + #[error("")] + NotConnected, +} + +impl From for SinkError { + fn from(e: AlsaError) -> SinkError { + use AlsaError::*; + let es = e.to_string(); + match e { + DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), + PcmSetUp { .. } => SinkError::ConnectionRefused(es), + NotConnected => SinkError::NotConnected(es), + _ => SinkError::InvalidParams(es), + } + } } pub struct AlsaSink { @@ -55,25 +87,19 @@ pub struct AlsaSink { period_buffer: Vec, } -fn list_outputs() -> io::Result<()> { +fn list_outputs() -> SinkResult<()> { println!("Listing available Alsa outputs:"); for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = match HintIter::new_str(None, t) { - Ok(i) => i, - Err(e) => { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } - }; + + let i = HintIter::new_str(None, &t).map_err(|_| AlsaError::Parsing)?; + for a in i { if let Some(Direction::Playback) = a.direction { // mimic aplay -L - let name = a - .name - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse name"))?; - let desc = a - .desc - .ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Could not parse desc"))?; + let name = a.name.ok_or(AlsaError::Parsing)?; + let desc = a.desc.ok_or(AlsaError::Parsing)?; + println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); } } @@ -82,10 +108,10 @@ fn list_outputs() -> io::Result<()> { Ok(()) } -fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), AlsaError> { +fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> { let pcm = PCM::new(dev_name, Direction::Playback, false).map_err(|e| AlsaError::PcmSetUp { device: dev_name.to_string(), - err: e, + e, })?; let alsa_format = match format { @@ -103,24 +129,26 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa let bytes_per_period = { let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; + hwp.set_access(Access::RWInterleaved) .map_err(|e| AlsaError::UnsupportedAccessType { device: dev_name.to_string(), - err: e, + e, })?; hwp.set_format(alsa_format) .map_err(|e| AlsaError::UnsupportedFormat { device: dev_name.to_string(), + alsa_format, format, - err: e, + e, })?; hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).map_err(|e| { AlsaError::UnsupportedSampleRate { device: dev_name.to_string(), samplerate: SAMPLE_RATE, - err: e, + e, } })?; @@ -128,7 +156,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa .map_err(|e| AlsaError::UnsupportedChannelCount { device: dev_name.to_string(), channel_count: NUM_CHANNELS, - err: e, + e, })?; hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest) @@ -141,8 +169,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> Result<(PCM, usize), Alsa let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; - // Don't assume we got what we wanted. - // Ask to make sure. + // Don't assume we got what we wanted. Ask to make sure. let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; @@ -171,8 +198,8 @@ impl Open for AlsaSink { Ok(_) => { exit(0); } - Err(err) => { - error!("Error listing Alsa outputs, {}", err); + Err(e) => { + error!("{}", e); exit(1); } }, @@ -193,53 +220,40 @@ impl Open for AlsaSink { } impl Sink for AlsaSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { if self.pcm.is_none() { - match open_device(&self.device, self.format) { - Ok((pcm, bytes_per_period)) => { - self.pcm = Some(pcm); - // If the capacity is greater than we want shrink it - // to it's current len (which should be zero) before - // setting the capacity with reserve_exact. - if self.period_buffer.capacity() > bytes_per_period { - self.period_buffer.shrink_to_fit(); - } - // This does nothing if the capacity is already sufficient. - // Len should always be zero, but for the sake of being thorough... - self.period_buffer - .reserve_exact(bytes_per_period - self.period_buffer.len()); - - // Should always match the "Period Buffer size in bytes: " trace! message. - trace!( - "Period Buffer capacity: {:?}", - self.period_buffer.capacity() - ); - } - Err(e) => { - return Err(io::Error::new(io::ErrorKind::Other, e)); - } + let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; + self.pcm = Some(pcm); + + let current_capacity = self.period_buffer.capacity(); + + if current_capacity > bytes_per_period { + self.period_buffer.truncate(bytes_per_period); + self.period_buffer.shrink_to_fit(); + } else if current_capacity < bytes_per_period { + let extra = bytes_per_period - self.period_buffer.len(); + self.period_buffer.reserve_exact(extra); } + + // Should always match the "Period Buffer size in bytes: " trace! message. + trace!( + "Period Buffer capacity: {:?}", + self.period_buffer.capacity() + ); } Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { // Zero fill the remainder of the period buffer and // write any leftover data before draining the actual PCM buffer. self.period_buffer.resize(self.period_buffer.capacity(), 0); self.write_buf()?; - let pcm = self.pcm.as_mut().ok_or_else(|| { - io::Error::new(io::ErrorKind::Other, "Error stopping AlsaSink, PCM is None") - })?; + let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; - pcm.drain().map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!("Error stopping AlsaSink {}", e), - ) - })?; + pcm.drain().map_err(AlsaError::DrainFailure)?; self.pcm = None; Ok(()) @@ -249,23 +263,28 @@ impl Sink for AlsaSink { } impl SinkAsBytes for AlsaSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { let mut start_index = 0; let data_len = data.len(); let capacity = self.period_buffer.capacity(); + loop { let data_left = data_len - start_index; let space_left = capacity - self.period_buffer.len(); let data_to_buffer = min(data_left, space_left); let end_index = start_index + data_to_buffer; + self.period_buffer .extend_from_slice(&data[start_index..end_index]); + if self.period_buffer.len() == capacity { self.write_buf()?; } + if end_index == data_len { break Ok(()); } + start_index = end_index; } } @@ -274,30 +293,18 @@ impl SinkAsBytes for AlsaSink { impl AlsaSink { pub const NAME: &'static str = "alsa"; - fn write_buf(&mut self) -> io::Result<()> { - let pcm = self.pcm.as_mut().ok_or_else(|| { - io::Error::new( - io::ErrorKind::Other, - "Error writing from AlsaSink buffer to PCM, PCM is None", - ) - })?; - let io = pcm.io_bytes(); - if let Err(err) = io.writei(&self.period_buffer) { + fn write_buf(&mut self) -> SinkResult<()> { + let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; + + if let Err(e) = pcm.io_bytes().writei(&self.period_buffer) { // Capture and log the original error as a warning, and then try to recover. // If recovery fails then forward that error back to player. warn!( - "Error writing from AlsaSink buffer to PCM, trying to recover {}", - err + "Error writing from AlsaSink buffer to PCM, trying to recover, {}", + e ); - pcm.try_recover(err, false).map_err(|e| { - io::Error::new( - io::ErrorKind::Other, - format!( - "Error writing from AlsaSink buffer to PCM, recovery failed {}", - e - ), - ) - })? + + pcm.try_recover(e, false).map_err(AlsaError::OnWrite)? } self.period_buffer.clear(); diff --git a/playback/src/audio_backend/gstreamer.rs b/playback/src/audio_backend/gstreamer.rs index 58f6cbc92..8b9575778 100644 --- a/playback/src/audio_backend/gstreamer.rs +++ b/playback/src/audio_backend/gstreamer.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -11,7 +11,7 @@ use gst::prelude::*; use zerocopy::AsBytes; use std::sync::mpsc::{sync_channel, SyncSender}; -use std::{io, thread}; +use std::thread; #[allow(dead_code)] pub struct GstreamerSink { @@ -131,7 +131,7 @@ impl Sink for GstreamerSink { } impl SinkAsBytes for GstreamerSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { // Copy expensively (in to_vec()) to avoid thread synchronization self.tx .send(data.to_vec()) diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index a8f37524c..5ba7b7ff2 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -6,7 +6,6 @@ use crate::NUM_CHANNELS; use jack::{ AsyncClient, AudioOut, Client, ClientOptions, Control, Port, ProcessHandler, ProcessScope, }; -use std::io; use std::sync::mpsc::{sync_channel, Receiver, SyncSender}; pub struct JackSink { @@ -70,10 +69,11 @@ impl Open for JackSink { } impl Sink for JackSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + let samples_f32: &[f32] = &converter.f64_to_f32(samples); for sample in samples_f32.iter() { let res = self.send.send(*sample); diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 31fb847c7..b89232b7e 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -1,26 +1,40 @@ use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; -use std::io; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum SinkError { + #[error("Audio Sink Error Not Connected: {0}")] + NotConnected(String), + #[error("Audio Sink Error Connection Refused: {0}")] + ConnectionRefused(String), + #[error("Audio Sink Error On Write: {0}")] + OnWrite(String), + #[error("Audio Sink Error Invalid Parameters: {0}")] + InvalidParams(String), +} + +pub type SinkResult = Result; pub trait Open { fn open(_: Option, format: AudioFormat) -> Self; } pub trait Sink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()>; + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; pub trait SinkAsBytes { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()>; + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()>; } fn mk_sink(device: Option, format: AudioFormat) -> Box { @@ -30,7 +44,7 @@ fn mk_sink(device: Option, format: AudioFormat // reuse code for various backends macro_rules! sink_as_bytes { () => { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { use crate::convert::i24; use zerocopy::AsBytes; match packet { diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index 56040384e..fd804a0e5 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -1,4 +1,4 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -23,14 +23,14 @@ impl Open for StdoutSink { } impl Sink for StdoutSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { if self.output.is_none() { let output: Box = match self.path.as_deref() { Some(path) => { let open_op = OpenOptions::new() .write(true) .open(path) - .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; Box::new(open_op) } None => Box::new(io::stdout()), @@ -46,14 +46,18 @@ impl Sink for StdoutSink { } impl SinkAsBytes for StdoutSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { match self.output.as_deref_mut() { Some(output) => { - output.write_all(data)?; - output.flush()?; + output + .write_all(data) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + output + .flush() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; } None => { - return Err(io::Error::new(io::ErrorKind::Other, "Output is None")); + return Err(SinkError::NotConnected("Output is None".to_string())); } } diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 26355a03c..7a0b179f7 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -1,11 +1,10 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use portaudio_rs::device::{get_default_output_index, DeviceIndex, DeviceInfo}; use portaudio_rs::stream::*; -use std::io; use std::process::exit; use std::time::Duration; @@ -96,7 +95,7 @@ impl<'a> Open for PortAudioSink<'a> { } impl<'a> Sink for PortAudioSink<'a> { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { (ref mut $stream: ident, ref $parameters: ident) => {{ if $stream.is_none() { @@ -125,7 +124,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { macro_rules! stop_sink { (ref mut $stream: ident) => {{ $stream.as_mut().unwrap().stop().unwrap(); @@ -141,7 +140,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -150,7 +149,7 @@ impl<'a> Sink for PortAudioSink<'a> { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| SinkError::OnWrite(e.to_string()))?; let result = match self { Self::F32(stream, _parameters) => { diff --git a/playback/src/audio_backend/pulseaudio.rs b/playback/src/audio_backend/pulseaudio.rs index 4ef8317a0..7487517ff 100644 --- a/playback/src/audio_backend/pulseaudio.rs +++ b/playback/src/audio_backend/pulseaudio.rs @@ -1,11 +1,10 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use libpulse_binding::{self as pulse, error::PAErr, stream::Direction}; use libpulse_simple_binding::Simple; -use std::io; use thiserror::Error; const APP_NAME: &str = "librespot"; @@ -13,18 +12,40 @@ const STREAM_NAME: &str = "Spotify endpoint"; #[derive(Debug, Error)] enum PulseError { - #[error("Error starting PulseAudioSink, invalid PulseAudio sample spec")] - InvalidSampleSpec, - #[error("Error starting PulseAudioSink, could not connect to PulseAudio server, {0}")] + #[error(" Unsupported Pulseaudio Sample Spec, Format {pulse_format:?} ({format:?}), Channels {channels}, Rate {rate}")] + InvalidSampleSpec { + pulse_format: pulse::sample::Format, + format: AudioFormat, + channels: u8, + rate: u32, + }, + + #[error(" {0}")] ConnectionRefused(PAErr), - #[error("Error stopping PulseAudioSink, failed to drain PulseAudio server buffer, {0}")] + + #[error(" Failed to Drain Pulseaudio Buffer, {0}")] DrainFailure(PAErr), - #[error("Error in PulseAudioSink, Not connected to PulseAudio server")] + + #[error("")] NotConnected, - #[error("Error writing from PulseAudioSink to PulseAudio server, {0}")] + + #[error(" {0}")] OnWrite(PAErr), } +impl From for SinkError { + fn from(e: PulseError) -> SinkError { + use PulseError::*; + let es = e.to_string(); + match e { + DrainFailure(_) | OnWrite(_) => SinkError::OnWrite(es), + ConnectionRefused(_) => SinkError::ConnectionRefused(es), + NotConnected => SinkError::NotConnected(es), + InvalidSampleSpec { .. } => SinkError::InvalidParams(es), + } + } +} + pub struct PulseAudioSink { s: Option, device: Option, @@ -51,68 +72,57 @@ impl Open for PulseAudioSink { } impl Sink for PulseAudioSink { - fn start(&mut self) -> io::Result<()> { - if self.s.is_some() { - return Ok(()); - } - - // PulseAudio calls S24 and S24_3 different from the rest of the world - let pulse_format = match self.format { - AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, - AudioFormat::S32 => pulse::sample::Format::S32NE, - AudioFormat::S24 => pulse::sample::Format::S24_32NE, - AudioFormat::S24_3 => pulse::sample::Format::S24NE, - AudioFormat::S16 => pulse::sample::Format::S16NE, - _ => unreachable!(), - }; - - let ss = pulse::sample::Spec { - format: pulse_format, - channels: NUM_CHANNELS, - rate: SAMPLE_RATE, - }; - - if !ss.is_valid() { - return Err(io::Error::new( - io::ErrorKind::Other, - PulseError::InvalidSampleSpec, - )); - } - - let result = Simple::new( - None, // Use the default server. - APP_NAME, // Our application's name. - Direction::Playback, // Direction. - self.device.as_deref(), // Our device (sink) name. - STREAM_NAME, // Description of our stream. - &ss, // Our sample format. - None, // Use default channel map. - None, // Use default buffering attributes. - ); - - match result { - Ok(s) => { - self.s = Some(s); - } - Err(e) => { - return Err(io::Error::new( - io::ErrorKind::ConnectionRefused, - PulseError::ConnectionRefused(e), - )); + fn start(&mut self) -> SinkResult<()> { + if self.s.is_none() { + // PulseAudio calls S24 and S24_3 different from the rest of the world + let pulse_format = match self.format { + AudioFormat::F32 => pulse::sample::Format::FLOAT32NE, + AudioFormat::S32 => pulse::sample::Format::S32NE, + AudioFormat::S24 => pulse::sample::Format::S24_32NE, + AudioFormat::S24_3 => pulse::sample::Format::S24NE, + AudioFormat::S16 => pulse::sample::Format::S16NE, + _ => unreachable!(), + }; + + let ss = pulse::sample::Spec { + format: pulse_format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, + }; + + if !ss.is_valid() { + let pulse_error = PulseError::InvalidSampleSpec { + pulse_format, + format: self.format, + channels: NUM_CHANNELS, + rate: SAMPLE_RATE, + }; + + return Err(SinkError::from(pulse_error)); } + + let s = Simple::new( + None, // Use the default server. + APP_NAME, // Our application's name. + Direction::Playback, // Direction. + self.device.as_deref(), // Our device (sink) name. + STREAM_NAME, // Description of our stream. + &ss, // Our sample format. + None, // Use default channel map. + None, // Use default buffering attributes. + ) + .map_err(PulseError::ConnectionRefused)?; + + self.s = Some(s); } Ok(()) } - fn stop(&mut self) -> io::Result<()> { - let s = self - .s - .as_mut() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?; + fn stop(&mut self) -> SinkResult<()> { + let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; - s.drain() - .map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::DrainFailure(e)))?; + s.drain().map_err(PulseError::DrainFailure)?; self.s = None; Ok(()) @@ -122,14 +132,10 @@ impl Sink for PulseAudioSink { } impl SinkAsBytes for PulseAudioSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { - let s = self - .s - .as_mut() - .ok_or_else(|| io::Error::new(io::ErrorKind::NotConnected, PulseError::NotConnected))?; - - s.write(data) - .map_err(|e| io::Error::new(io::ErrorKind::Other, PulseError::OnWrite(e)))?; + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { + let s = self.s.as_mut().ok_or(PulseError::NotConnected)?; + + s.write(data).map_err(PulseError::OnWrite)?; Ok(()) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 4d9c65c5c..200c9fc44 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -1,11 +1,11 @@ use std::process::exit; +use std::thread; use std::time::Duration; -use std::{io, thread}; use cpal::traits::{DeviceTrait, HostTrait}; use thiserror::Error; -use super::Sink; +use super::{Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; @@ -33,16 +33,30 @@ pub fn mk_rodiojack(device: Option, format: AudioFormat) -> Box No Device Available")] NoDeviceAvailable, - #[error("Rodio: device \"{0}\" is not available")] + #[error(" device \"{0}\" is Not Available")] DeviceNotAvailable(String), - #[error("Rodio play error: {0}")] + #[error(" Play Error: {0}")] PlayError(#[from] rodio::PlayError), - #[error("Rodio stream error: {0}")] + #[error(" Stream Error: {0}")] StreamError(#[from] rodio::StreamError), - #[error("Cannot get audio devices: {0}")] + #[error(" Cannot Get Audio Devices: {0}")] DevicesError(#[from] cpal::DevicesError), + #[error(" {0}")] + Samples(String), +} + +impl From for SinkError { + fn from(e: RodioError) -> SinkError { + use RodioError::*; + let es = e.to_string(); + match e { + StreamError(_) | PlayError(_) | Samples(_) => SinkError::OnWrite(es), + NoDeviceAvailable | DeviceNotAvailable(_) => SinkError::ConnectionRefused(es), + DevicesError(_) => SinkError::InvalidParams(es), + } + } } pub struct RodioSink { @@ -175,10 +189,10 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| RodioError::Samples(e.to_string()))?; match self.format { AudioFormat::F32 => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 63a88c225..6272fa323 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -1,11 +1,11 @@ -use super::{Open, Sink}; +use super::{Open, Sink, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use sdl2::audio::{AudioQueue, AudioSpecDesired}; +use std::thread; use std::time::Duration; -use std::{io, thread}; pub enum SdlSink { F32(AudioQueue), @@ -52,7 +52,7 @@ impl Open for SdlSink { } impl Sink for SdlSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { macro_rules! start_sink { ($queue: expr) => {{ $queue.clear(); @@ -67,7 +67,7 @@ impl Sink for SdlSink { Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { macro_rules! stop_sink { ($queue: expr) => {{ $queue.pause(); @@ -82,7 +82,7 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> io::Result<()> { + fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit @@ -94,7 +94,7 @@ impl Sink for SdlSink { let samples = packet .samples() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?; + .map_err(|e| SinkError::OnWrite(e.to_string()))?; match self { Self::F32(queue) => { let samples_f32: &[f32] = &converter.f64_to_f32(samples); diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index 64f04c88e..c501cf83f 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -1,10 +1,10 @@ -use super::{Open, Sink, SinkAsBytes}; +use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult}; use crate::config::AudioFormat; use crate::convert::Converter; use crate::decoder::AudioPacket; use shell_words::split; -use std::io::{self, Write}; +use std::io::Write; use std::process::{Child, Command, Stdio}; pub struct SubprocessSink { @@ -30,21 +30,25 @@ impl Open for SubprocessSink { } impl Sink for SubprocessSink { - fn start(&mut self) -> io::Result<()> { + fn start(&mut self) -> SinkResult<()> { let args = split(&self.shell_command).unwrap(); - self.child = Some( - Command::new(&args[0]) - .args(&args[1..]) - .stdin(Stdio::piped()) - .spawn()?, - ); + let child = Command::new(&args[0]) + .args(&args[1..]) + .stdin(Stdio::piped()) + .spawn() + .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; + self.child = Some(child); Ok(()) } - fn stop(&mut self) -> io::Result<()> { + fn stop(&mut self) -> SinkResult<()> { if let Some(child) = &mut self.child.take() { - child.kill()?; - child.wait()?; + child + .kill() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + child + .wait() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; } Ok(()) } @@ -53,11 +57,18 @@ impl Sink for SubprocessSink { } impl SinkAsBytes for SubprocessSink { - fn write_bytes(&mut self, data: &[u8]) -> io::Result<()> { + fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> { if let Some(child) = &mut self.child { - let child_stdin = child.stdin.as_mut().unwrap(); - child_stdin.write_all(data)?; - child_stdin.flush()?; + let child_stdin = child + .stdin + .as_mut() + .ok_or_else(|| SinkError::NotConnected("Child is None".to_string()))?; + child_stdin + .write_all(data) + .map_err(|e| SinkError::OnWrite(e.to_string()))?; + child_stdin + .flush() + .map_err(|e| SinkError::OnWrite(e.to_string()))?; } Ok(()) } From 4c1b2278abe8c2a83d7f31e56435f735af208891 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 4 Oct 2021 13:59:18 -0500 Subject: [PATCH 18/76] Fix clippy comparison chain warning (#857) --- playback/src/audio_backend/alsa.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 177988681..41c75ed6f 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -7,6 +7,7 @@ use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; +use std::cmp::Ordering; use std::process::exit; use std::time::Duration; use thiserror::Error; @@ -92,7 +93,7 @@ fn list_outputs() -> SinkResult<()> { for t in &["pcm", "ctl", "hwdep"] { println!("{} devices:", t); - let i = HintIter::new_str(None, &t).map_err(|_| AlsaError::Parsing)?; + let i = HintIter::new_str(None, t).map_err(|_| AlsaError::Parsing)?; for a in i { if let Some(Direction::Playback) = a.direction { @@ -225,14 +226,16 @@ impl Sink for AlsaSink { let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; self.pcm = Some(pcm); - let current_capacity = self.period_buffer.capacity(); - - if current_capacity > bytes_per_period { - self.period_buffer.truncate(bytes_per_period); - self.period_buffer.shrink_to_fit(); - } else if current_capacity < bytes_per_period { - let extra = bytes_per_period - self.period_buffer.len(); - self.period_buffer.reserve_exact(extra); + match self.period_buffer.capacity().cmp(&bytes_per_period) { + Ordering::Greater => { + self.period_buffer.truncate(bytes_per_period); + self.period_buffer.shrink_to_fit(); + } + Ordering::Less => { + let extra = bytes_per_period - self.period_buffer.len(); + self.period_buffer.reserve_exact(extra); + } + Ordering::Equal => (), } // Should always match the "Period Buffer size in bytes: " trace! message. @@ -251,11 +254,10 @@ impl Sink for AlsaSink { self.period_buffer.resize(self.period_buffer.capacity(), 0); self.write_buf()?; - let pcm = self.pcm.as_mut().ok_or(AlsaError::NotConnected)?; + let pcm = self.pcm.take().ok_or(AlsaError::NotConnected)?; pcm.drain().map_err(AlsaError::DrainFailure)?; - self.pcm = None; Ok(()) } From 095536f100b1ae428315f94e166373906fd54a91 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Mon, 4 Oct 2021 21:44:03 +0200 Subject: [PATCH 19/76] Prepare for 0.3.0 release --- CHANGELOG.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a056bc2e..0fc3f9cc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,13 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) since v0.2.0. ## [Unreleased] + +## [0.3.0] - YYYY-MM-DD + ### Added - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. - [playback] Add support for dithering with `--dither` for lower requantization error (breaking) - [playback] Add `--volume-range` option to set dB range and control `log` and `cubic` volume control curves - [playback] `alsamixer`: support for querying dB range from Alsa softvol - [playback] Add `--format F64` (supported by Alsa and GStreamer only) -- [playback] Add `--normalisation-type auto` that switches between album and track automatically +- [playback] Add `--normalisation-gain-type auto` that switches between album and track automatically ### Changed - [audio, playback] Moved `VorbisDecoder`, `VorbisError`, `AudioPacket`, `PassthroughDecoder`, `PassthroughError`, `DecoderError`, `AudioDecoder` and the `convert` module from `librespot-audio` to `librespot-playback`. The underlying crates `vorbis`, `librespot-tremor`, `lewton` and `ogg` should be used directly. (breaking) @@ -23,7 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] Make cubic volume control available to all mixers with `--volume-ctrl cubic` - [playback] Normalize volumes to `[0.0..1.0]` instead of `[0..65535]` for greater precision and performance (breaking) - [playback] `alsamixer`: complete rewrite (breaking) -- [playback] `alsamixer`: query card dB range for the `log` volume control unless specified otherwise +- [playback] `alsamixer`: query card dB range for the volume control unless specified otherwise - [playback] `alsamixer`: use `--device` name for `--mixer-card` unless specified otherwise - [playback] `player`: consider errors in `sink.start`, `sink.stop` and `sink.write` fatal and `exit(1)` (breaking) - [playback] `player`: make `convert` and `decoder` public so you can implement your own `Sink` @@ -67,7 +70,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.2.0..HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.0..HEAD +[0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 [0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 [0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6 [0.1.5]: https://github.com/librespot-org/librespot/compare/v0.1.3..v0.1.5 From 289b4f9bcceeee1e046986ba3337f75e0eea320a Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 5 Oct 2021 22:08:26 +0200 Subject: [PATCH 20/76] Fix behavior after last track of an album/playlist * When autoplay is disabled, then loop back to the first track instead of 10 tracks back. Continue or stop playing depending on the state of the repeat button. * When autoplay is enabled, then extend the playlist *after* the last track. #844 broke this such that the last track of an album or playlist was never played. Fixes: #434 --- CHANGELOG.md | 1 + connect/src/spirc.rs | 45 +++++++++++++++----------------------------- 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fc3f9cc4..196b4e88b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [connect] Fix step size on volume up/down events +- [connect] Fix looping back to the first track after the last track of an album or playlist - [playback] Incorrect `PlayerConfig::default().normalisation_threshold` caused distortion when using dynamic volume normalisation downstream - [playback] Fix `log` and `cubic` volume controls to be mute at zero volume - [playback] Fix `S24_3` format on big-endian systems diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 9aa861343..2038c8bd8 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -84,7 +84,6 @@ struct SpircTaskConfig { autoplay: bool, } -const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; const VOLUME_STEPS: i64 = 64; @@ -887,8 +886,8 @@ impl SpircTask { let tracks_len = self.state.get_track().len() as u32; debug!( "At track {:?} of {:?} <{:?}> update [{}]", - new_index, - self.state.get_track().len(), + new_index + 1, + tracks_len, self.state.get_context_uri(), tracks_len - new_index < CONTEXT_FETCH_THRESHOLD ); @@ -902,27 +901,25 @@ impl SpircTask { self.context_fut = self.resolve_station(&context_uri); self.update_tracks_from_context(); } - let last_track = new_index == tracks_len - 1; - if self.config.autoplay && last_track { - // Extend the playlist - // Note: This doesn't seem to reflect in the UI - // the additional tracks in the frame don't show up as with station view - debug!("Extending playlist <{}>", context_uri); - self.update_tracks_from_context(); - } if new_index >= tracks_len { - new_index = 0; // Loop around back to start - continue_playing = self.state.get_repeat(); + if self.config.autoplay { + // Extend the playlist + debug!("Extending playlist <{}>", context_uri); + self.update_tracks_from_context(); + self.player.set_auto_normalise_as_album(false); + } else { + new_index = 0; + continue_playing = self.state.get_repeat(); + debug!( + "Looping around back to start, repeat is {}", + continue_playing + ); + } } if tracks_len > 0 { self.state.set_playing_track_index(new_index); self.load_track(continue_playing, 0); - if self.config.autoplay && last_track { - // If we're now playing the last track of an album, then - // switch to track normalisation mode for the autoplay to come. - self.player.set_auto_normalise_as_album(false); - } } else { info!("Not playing next track because there are no more tracks left in queue."); self.state.set_playing_track_index(0); @@ -1054,21 +1051,9 @@ impl SpircTask { let new_tracks = &context.tracks; debug!("Adding {:?} tracks from context to frame", new_tracks.len()); let mut track_vec = self.state.take_track().into_vec(); - if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { - track_vec.drain(0..head); - } track_vec.extend_from_slice(new_tracks); self.state .set_track(protobuf::RepeatedField::from_vec(track_vec)); - - // Update playing index - if let Some(new_index) = self - .state - .get_playing_track_index() - .checked_sub(CONTEXT_TRACKS_HISTORY as u32) - { - self.state.set_playing_track_index(new_index); - } } else { warn!("No context to update from!"); } From 0f5d610b4bc681ea9c956f5768ea1093620e9812 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 6 Oct 2021 21:21:03 +0200 Subject: [PATCH 21/76] Revert 10 track history window --- connect/src/spirc.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 2038c8bd8..d644e2b0c 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -84,6 +84,7 @@ struct SpircTaskConfig { autoplay: bool, } +const CONTEXT_TRACKS_HISTORY: usize = 10; const CONTEXT_FETCH_THRESHOLD: u32 = 5; const VOLUME_STEPS: i64 = 64; @@ -1051,9 +1052,21 @@ impl SpircTask { let new_tracks = &context.tracks; debug!("Adding {:?} tracks from context to frame", new_tracks.len()); let mut track_vec = self.state.take_track().into_vec(); + if let Some(head) = track_vec.len().checked_sub(CONTEXT_TRACKS_HISTORY) { + track_vec.drain(0..head); + } track_vec.extend_from_slice(new_tracks); self.state .set_track(protobuf::RepeatedField::from_vec(track_vec)); + + // Update playing index + if let Some(new_index) = self + .state + .get_playing_track_index() + .checked_sub(CONTEXT_TRACKS_HISTORY as u32) + { + self.state.set_playing_track_index(new_index); + } } else { warn!("No context to update from!"); } From 9ef53f5ffb2cc3acc134854dffa2ef45e119a170 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 6 Oct 2021 11:20:09 -0500 Subject: [PATCH 22/76] simplify buffer resizing This way is less verbose, much more simple and less brittle. --- playback/src/audio_backend/alsa.rs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 41c75ed6f..9dd3ea0c5 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -7,7 +7,6 @@ use alsa::device_name::HintIter; use alsa::pcm::{Access, Format, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; -use std::cmp::Ordering; use std::process::exit; use std::time::Duration; use thiserror::Error; @@ -226,16 +225,8 @@ impl Sink for AlsaSink { let (pcm, bytes_per_period) = open_device(&self.device, self.format)?; self.pcm = Some(pcm); - match self.period_buffer.capacity().cmp(&bytes_per_period) { - Ordering::Greater => { - self.period_buffer.truncate(bytes_per_period); - self.period_buffer.shrink_to_fit(); - } - Ordering::Less => { - let extra = bytes_per_period - self.period_buffer.len(); - self.period_buffer.reserve_exact(extra); - } - Ordering::Equal => (), + if self.period_buffer.capacity() != bytes_per_period { + self.period_buffer = Vec::with_capacity(bytes_per_period); } // Should always match the "Period Buffer size in bytes: " trace! message. From 6a3377402a5910841968adbc651ded08ae825ad9 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Wed, 13 Oct 2021 15:10:18 +0100 Subject: [PATCH 23/76] Update version numbers to 0.3.0 --- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 10 +++++----- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- publish.sh | 2 +- 9 files changed, 28 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ced7d0f9e..90704c842 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.2.0" +version = "0.3.0" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-connect] path = "connect" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-core] path = "core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-discovery] path = "discovery" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-metadata] path = "metadata" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-playback] path = "playback" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-protocol] path = "protocol" -version = "0.2.0" +version = "0.3.0" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index f4440592a..5c2a43beb 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" license="MIT" @@ -8,7 +8,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index 89d185ab9..dd0848d4c 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-playback] path = "../playback" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-discovery] path = "../discovery" -version = "0.2.0" +version = "0.3.0" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 24e599a63..60c53a3eb 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.2.0" +version = "0.3.0" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index 9ea9df48c..cdf1f342c 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -28,7 +28,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.2.0" +version = "0.3.0" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 6e181a1ac..6ea90033a 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-protocol] path = "../protocol" -version = "0.2.0" +version = "0.3.0" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index f2fdaf482..dfab71684 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.2.0" +version = "0.3.0" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-core] path = "../core" -version = "0.2.0" +version = "0.3.0" [dependencies.librespot-metadata] path = "../metadata" -version = "0.2.0" +version = "0.3.0" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 5c3ae0846..83c3a42b0 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.2.0" +version = "0.3.0" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" diff --git a/publish.sh b/publish.sh index 478741a5b..fb4a475a3 100755 --- a/publish.sh +++ b/publish.sh @@ -6,7 +6,7 @@ DRY_RUN='false' WORKINGDIR="$( cd "$(dirname "$0")" ; pwd -P )" cd $WORKINGDIR -crates=( "protocol" "core" "audio" "metadata" "playback" "connect" "librespot" ) +crates=( "protocol" "core" "discovery" "audio" "metadata" "playback" "connect" "librespot" ) function switchBranch { if [ "$SKIP_MERGE" = 'false' ] ; then From afbdd11f4597375e1cc540e03033d0889b47f220 Mon Sep 17 00:00:00 2001 From: Sasha Hilton Date: Wed, 13 Oct 2021 15:30:13 +0100 Subject: [PATCH 24/76] Update Cargo.lock for 0.3.0 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e94d21b63..76f4e53e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.2.0" +version = "0.3.0" dependencies = [ "base64", "env_logger", @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes-ctr", "byteorder", @@ -1170,7 +1170,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.2.0" +version = "0.3.0" dependencies = [ "form_urlencoded", "futures-util", @@ -1189,7 +1189,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes", "base64", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.2.0" +version = "0.3.0" dependencies = [ "aes-ctr", "base64", @@ -1254,7 +1254,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.2.0" +version = "0.3.0" dependencies = [ "async-trait", "byteorder", @@ -1266,7 +1266,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.2.0" +version = "0.3.0" dependencies = [ "alsa", "byteorder", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.2.0" +version = "0.3.0" dependencies = [ "glob", "protobuf", From d99581aeb774da681910f5bca22b74d654fac83b Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Wed, 13 Oct 2021 20:37:46 +0200 Subject: [PATCH 25/76] Tag 0.3.0 and document #859 --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 196b4e88b..efd59d053 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -## [0.3.0] - YYYY-MM-DD +### Fixed +- [connect] Partly fix behavior after last track of an album/playlist + +## [0.3.0] - 2021-10-13 ### Added - [discovery] The crate `librespot-discovery` for discovery in LAN was created. Its functionality was previously part of `librespot-connect`. From 3b51a5dc23c43b028d1e2ecd19b5fc65c14cf1ae Mon Sep 17 00:00:00 2001 From: Nick Steel Date: Thu, 14 Oct 2021 11:57:33 +0100 Subject: [PATCH 26/76] Include build profile in the displayed version information Example output from -V for a debug build is: librespot 0.3.0 832889b (Built on 2021-10-14, Build ID: ANJrycbG, Profile: debug) --- CHANGELOG.md | 3 +++ src/main.rs | 26 +++++++++++++------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efd59d053..9a3834364 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Include build profile in the displayed version information + ### Fixed - [connect] Partly fix behavior after last track of an album/playlist diff --git a/src/main.rs b/src/main.rs index 76e8ba1c7..01ec460b6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -160,14 +160,20 @@ pub fn parse_file_size(input: &str) -> Result { Ok((num * base.pow(exponent) as f64) as u64) } -fn print_version() { - println!( - "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", +fn get_version_string() -> String { + #[cfg(debug_assertions)] + const BUILD_PROFILE: &str = "debug"; + #[cfg(not(debug_assertions))] + const BUILD_PROFILE: &str = "release"; + + format!( + "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id}, Profile: {build_profile})", semver = version::SEMVER, sha = version::SHA_SHORT, build_date = version::BUILD_DATE, - build_id = version::BUILD_ID - ); + build_id = version::BUILD_ID, + build_profile = BUILD_PROFILE + ) } struct Setup { @@ -438,20 +444,14 @@ fn get_setup(args: &[String]) -> Setup { } if matches.opt_present(VERSION) { - print_version(); + println!("{}", get_version_string()); exit(0); } let verbose = matches.opt_present(VERBOSE); setup_logging(verbose); - info!( - "librespot {semver} {sha} (Built on {build_date}, Build ID: {build_id})", - semver = version::SEMVER, - sha = version::SHA_SHORT, - build_date = version::BUILD_DATE, - build_id = version::BUILD_ID - ); + info!("{}", get_version_string()); let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { From 4c89a721eeed791bdd85120df80e0e83ca9caff3 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 19 Oct 2021 22:33:04 +0200 Subject: [PATCH 27/76] Improve dithering CPU usage (#866) --- CHANGELOG.md | 1 + Cargo.lock | 14 ++++++++++++-- playback/Cargo.toml | 1 + playback/src/dither.rs | 36 ++++++++++++++++++++++++++---------- 4 files changed, 40 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a3834364..fb79169bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Include build profile in the displayed version information +- [playback] Improve dithering CPU usage by about 33% ### Fixed - [connect] Partly fix behavior after last track of an album/playlist diff --git a/Cargo.lock b/Cargo.lock index 76f4e53e6..31018365b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,6 +1288,7 @@ dependencies = [ "portaudio-rs", "rand", "rand_distr", + "rand_xoshiro", "rodio", "sdl2", "shell-words", @@ -1881,9 +1882,9 @@ dependencies = [ [[package]] name = "rand_distr" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "051b398806e42b9cd04ad9ec8f81e355d0a382c543ac6672c62f5a5b452ef142" +checksum = "964d548f8e7d12e102ef183a0de7e98180c9f8729f555897a857b96e48122d2f" dependencies = [ "num-traits", "rand", @@ -1898,6 +1899,15 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + [[package]] name = "redox_syscall" version = "0.2.10" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index dfab71684..b3b395597 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -49,6 +49,7 @@ ogg = "0.8" # Dithering rand = "0.8" rand_distr = "0.4" +rand_xoshiro = "0.6" [features] alsa-backend = ["alsa"] diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 2510b8860..a44acf218 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -1,4 +1,4 @@ -use rand::rngs::ThreadRng; +use rand::SeedableRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; @@ -41,20 +41,36 @@ impl fmt::Display for dyn Ditherer { } } -// Implementation note: we save the handle to ThreadRng so it doesn't require -// a lookup on each call (which is on each sample!). This is ~2.5x as fast. -// Downside is that it is not Send so we cannot move it around player threads. +// `SmallRng` is 33% faster than `ThreadRng`, but we can do even better. +// `SmallRng` defaults to `Xoshiro256PlusPlus` on 64-bit platforms and +// `Xoshiro128PlusPlus` on 32-bit platforms. These are excellent for the +// general case. In our case of just 64-bit floating points, we can make +// some optimizations. Compared to `SmallRng`, these hand-picked generators +// improve performance by another 9% on 64-bit platforms and 2% on 32-bit +// platforms. // +// For reference, see https://prng.di.unimi.it. Note that we do not use +// `Xoroshiro128Plus` or `Xoshiro128Plus` because they display low linear +// complexity in the lower four bits, which is not what we want: +// linearization is the very point of dithering. +#[cfg(target_pointer_width = "64")] +type Rng = rand_xoshiro::Xoshiro256Plus; +#[cfg(not(target_pointer_width = "64"))] +type Rng = rand_xoshiro::Xoshiro128StarStar; + +fn create_rng() -> Rng { + Rng::from_entropy() +} pub struct TriangularDitherer { - cached_rng: ThreadRng, + cached_rng: Rng, distribution: Triangular, } impl Ditherer for TriangularDitherer { fn new() -> Self { Self { - cached_rng: rand::thread_rng(), + cached_rng: create_rng(), // 2 LSB peak-to-peak needed to linearize the response: distribution: Triangular::new(-1.0, 1.0, 0.0).unwrap(), } @@ -74,14 +90,14 @@ impl TriangularDitherer { } pub struct GaussianDitherer { - cached_rng: ThreadRng, + cached_rng: Rng, distribution: Normal, } impl Ditherer for GaussianDitherer { fn new() -> Self { Self { - cached_rng: rand::thread_rng(), + cached_rng: create_rng(), // 1/2 LSB RMS needed to linearize the response: distribution: Normal::new(0.0, 0.5).unwrap(), } @@ -103,7 +119,7 @@ impl GaussianDitherer { pub struct HighPassDitherer { active_channel: usize, previous_noises: [f64; NUM_CHANNELS], - cached_rng: ThreadRng, + cached_rng: Rng, distribution: Uniform, } @@ -112,7 +128,7 @@ impl Ditherer for HighPassDitherer { Self { active_channel: 0, previous_noises: [0.0; NUM_CHANNELS], - cached_rng: rand::thread_rng(), + cached_rng: create_rng(), distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB } } From ff3648434b5a8275a30d1bf1c86191a79b146c66 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 21 Oct 2021 19:31:58 +0200 Subject: [PATCH 28/76] Change hand-picked RNGs back to `SmallRng` While `Xoshiro256+` is faster on 64-bit, it has low linear complexity in the lower three bits, which *are* used when generating dither. Also, while `Xoshiro128StarStar` access one less variable from the heap, multiplication is generally slower than addition in hardware. --- Cargo.lock | 10 ---------- playback/Cargo.toml | 3 +-- playback/src/dither.rs | 28 ++++++---------------------- 3 files changed, 7 insertions(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31018365b..82a9d4607 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1288,7 +1288,6 @@ dependencies = [ "portaudio-rs", "rand", "rand_distr", - "rand_xoshiro", "rodio", "sdl2", "shell-words", @@ -1899,15 +1898,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "rand_xoshiro" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" -dependencies = [ - "rand_core", -] - [[package]] name = "redox_syscall" version = "0.2.10" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index b3b395597..911800db1 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -47,9 +47,8 @@ lewton = "0.10" ogg = "0.8" # Dithering -rand = "0.8" +rand = { version = "0.8", features = ["small_rng"] } rand_distr = "0.4" -rand_xoshiro = "0.6" [features] alsa-backend = ["alsa"] diff --git a/playback/src/dither.rs b/playback/src/dither.rs index a44acf218..0f6679171 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -1,3 +1,4 @@ +use rand::rngs::SmallRng; use rand::SeedableRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; @@ -41,29 +42,12 @@ impl fmt::Display for dyn Ditherer { } } -// `SmallRng` is 33% faster than `ThreadRng`, but we can do even better. -// `SmallRng` defaults to `Xoshiro256PlusPlus` on 64-bit platforms and -// `Xoshiro128PlusPlus` on 32-bit platforms. These are excellent for the -// general case. In our case of just 64-bit floating points, we can make -// some optimizations. Compared to `SmallRng`, these hand-picked generators -// improve performance by another 9% on 64-bit platforms and 2% on 32-bit -// platforms. -// -// For reference, see https://prng.di.unimi.it. Note that we do not use -// `Xoroshiro128Plus` or `Xoshiro128Plus` because they display low linear -// complexity in the lower four bits, which is not what we want: -// linearization is the very point of dithering. -#[cfg(target_pointer_width = "64")] -type Rng = rand_xoshiro::Xoshiro256Plus; -#[cfg(not(target_pointer_width = "64"))] -type Rng = rand_xoshiro::Xoshiro128StarStar; - -fn create_rng() -> Rng { - Rng::from_entropy() +fn create_rng() -> SmallRng { + SmallRng::from_entropy() } pub struct TriangularDitherer { - cached_rng: Rng, + cached_rng: SmallRng, distribution: Triangular, } @@ -90,7 +74,7 @@ impl TriangularDitherer { } pub struct GaussianDitherer { - cached_rng: Rng, + cached_rng: SmallRng, distribution: Normal, } @@ -119,7 +103,7 @@ impl GaussianDitherer { pub struct HighPassDitherer { active_channel: usize, previous_noises: [f64; NUM_CHANNELS], - cached_rng: Rng, + cached_rng: SmallRng, distribution: Uniform, } From a5c7580d4fe928b66e325753d9617f56ef2a7e5d Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 21 Oct 2021 17:24:02 -0500 Subject: [PATCH 29/76] Grammar Police the arg descriptions --- src/main.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/main.rs b/src/main.rs index 01ec460b6..a3522e8c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -248,7 +248,7 @@ fn get_setup(args: &[String]) -> Setup { ).optopt( "", SYSTEM_CACHE, - "Path to a directory where system files (credentials, volume) will be cached. Can be different from cache option value.", + "Path to a directory where system files (credentials, volume) will be cached. May be different from the cache option value.", "PATH", ).optopt( "", @@ -257,7 +257,7 @@ fn get_setup(args: &[String]) -> Setup { "SIZE" ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") .optopt("n", NAME, "Device name.", "NAME") - .optopt("", DEVICE_TYPE, "Displayed device type.", "TYPE") + .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") .optopt( BITRATE, "bitrate", @@ -270,14 +270,14 @@ fn get_setup(args: &[String]) -> Setup { "Run PROGRAM when a playback event occurs.", "PROGRAM", ) - .optflag("", EMIT_SINK_EVENTS, "Run program set by --onevent before sink is opened and after it is closed.") + .optflag("", EMIT_SINK_EVENTS, "Run PROGRAM set by --onevent before sink is opened and after it is closed.") .optflag("v", VERBOSE, "Enable verbose output.") .optflag("V", VERSION, "Display librespot version string.") - .optopt("u", USERNAME, "Username to sign in with.", "USERNAME") - .optopt("p", PASSWORD, "Password", "PASSWORD") + .optopt("u", USERNAME, "Username used to sign in with.", "USERNAME") + .optopt("p", PASSWORD, "Password used to sign in with.", "PASSWORD") .optopt("", PROXY, "HTTP proxy to use when connecting.", "URL") - .optopt("", AP_PORT, "Connect to AP with specified port. If no AP with that port are present fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") - .optflag("", DISABLE_DISCOVERY, "Disable discovery mode.") + .optopt("", AP_PORT, "Connect to an AP with a specified port. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") + .optflag("", DISABLE_DISCOVERY, "Disable zeroconf discovery mode.") .optopt( "", BACKEND, @@ -287,7 +287,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", DEVICE, - "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio.", + "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio. Defaults to the backend's default.", "NAME", ) .optopt( @@ -299,10 +299,10 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", DITHER, - "Specify the dither algorithm to use - [none, gpdf, tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", + "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", "DITHER", ) - .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}.", "MIXER") + .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}. Defaults to softvol", "MIXER") .optopt( "", "mixer-name", // deprecated @@ -312,7 +312,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", ALSA_MIXER_CONTROL, - "Alsa mixer control, e.g. 'PCM' or 'Master'. Defaults to 'PCM'.", + "Alsa mixer control, e.g. 'PCM', 'Master' or similar. Defaults to 'PCM'.", "NAME", ) .optopt( @@ -348,13 +348,13 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", ZEROCONF_PORT, - "The port the internal server advertised over zeroconf uses.", + "The port the internal server advertises over zeroconf.", "PORT", ) .optflag( "", ENABLE_VOLUME_NORMALISATION, - "Play all tracks at the same volume.", + "Play all tracks at approximately the same apparent volume.", ) .optopt( "", @@ -377,19 +377,19 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", NORMALISATION_THRESHOLD, - "Threshold (dBFS) to prevent clipping. Defaults to -2.0.", + "Threshold (dBFS) at which the dynamic limiter engages to prevent clipping. Defaults to -2.0.", "THRESHOLD", ) .optopt( "", NORMALISATION_ATTACK, - "Attack time (ms) in which the dynamic limiter is reducing gain. Defaults to 5.", + "Attack time (ms) in which the dynamic limiter reduces gain. Defaults to 5.", "TIME", ) .optopt( "", NORMALISATION_RELEASE, - "Release or decay time (ms) in which the dynamic limiter is restoring gain. Defaults to 100.", + "Release or decay time (ms) in which the dynamic limiter restores gain. Defaults to 100.", "TIME", ) .optopt( @@ -401,7 +401,7 @@ fn get_setup(args: &[String]) -> Setup { .optopt( "", VOLUME_CTRL, - "Volume control type {cubic|fixed|linear|log}. Defaults to log.", + "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.", "VOLUME_CTRL" ) .optopt( @@ -423,7 +423,7 @@ fn get_setup(args: &[String]) -> Setup { .optflag( "", PASSTHROUGH, - "Pass raw stream to output, only works for pipe and subprocess.", + "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", ); let matches = match opts.parse(&args[1..]) { From 9d19841c0f0b40208d3c0540c4d88cf98fc6356e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Oct 2021 20:07:11 +0200 Subject: [PATCH 30/76] Prepare for 0.3.1 release --- CHANGELOG.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb79169bf..6e362ae63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.3.1] - 2021-10-24 + ### Changed - Include build profile in the displayed version information - [playback] Improve dithering CPU usage by about 33% @@ -78,7 +80,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.1.0] - 2019-11-06 -[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.0..HEAD +[unreleased]: https://github.com/librespot-org/librespot/compare/v0.3.1..HEAD +[0.3.1]: https://github.com/librespot-org/librespot/compare/v0.3.0..v0.3.1 [0.3.0]: https://github.com/librespot-org/librespot/compare/v0.2.0..v0.3.0 [0.2.0]: https://github.com/librespot-org/librespot/compare/v0.1.6..v0.2.0 [0.1.6]: https://github.com/librespot-org/librespot/compare/v0.1.5..v0.1.6 From 0e6b1ba9dc426d1eec19f3ee01f5c899cb731654 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Oct 2021 20:12:33 +0200 Subject: [PATCH 31/76] Update version numbers to 0.3.1 --- Cargo.toml | 16 ++++++++-------- audio/Cargo.toml | 4 ++-- connect/Cargo.toml | 10 +++++----- core/Cargo.toml | 4 ++-- discovery/Cargo.toml | 4 ++-- metadata/Cargo.toml | 6 +++--- playback/Cargo.toml | 8 ++++---- protocol/Cargo.toml | 2 +- 8 files changed, 27 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 90704c842..8429ba2e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot" -version = "0.3.0" +version = "0.3.1" authors = ["Librespot Org"] license = "MIT" description = "An open source client library for Spotify, with support for Spotify Connect" @@ -22,31 +22,31 @@ doc = false [dependencies.librespot-audio] path = "audio" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-connect] path = "connect" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-core] path = "core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-discovery] path = "discovery" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-metadata] path = "metadata" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-playback] path = "playback" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-protocol] path = "protocol" -version = "0.3.0" +version = "0.3.1" [dependencies] base64 = "0.13" diff --git a/audio/Cargo.toml b/audio/Cargo.toml index 5c2a43beb..77855e620 100644 --- a/audio/Cargo.toml +++ b/audio/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-audio" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description="The audio fetching and processing logic for librespot" license="MIT" @@ -8,7 +8,7 @@ edition = "2018" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies] aes-ctr = "0.6" diff --git a/connect/Cargo.toml b/connect/Cargo.toml index dd0848d4c..4daf89f48 100644 --- a/connect/Cargo.toml +++ b/connect/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-connect" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description = "The discovery and Spotify Connect logic for librespot" license = "MIT" @@ -20,19 +20,19 @@ tokio-stream = "0.1.1" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-playback] path = "../playback" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-discovery] path = "../discovery" -version = "0.3.0" +version = "0.3.1" [features] with-dns-sd = ["librespot-discovery/with-dns-sd"] diff --git a/core/Cargo.toml b/core/Cargo.toml index 60c53a3eb..2494a19a9 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-core" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] build = "build.rs" description = "The core functionality provided by librespot" @@ -10,7 +10,7 @@ edition = "2018" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.0" +version = "0.3.1" [dependencies] aes = "0.6" diff --git a/discovery/Cargo.toml b/discovery/Cargo.toml index cdf1f342c..9b4d415e3 100644 --- a/discovery/Cargo.toml +++ b/discovery/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-discovery" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description = "The discovery logic for librespot" license = "MIT" @@ -28,7 +28,7 @@ dns-sd = { version = "0.1.3", optional = true } [dependencies.librespot-core] path = "../core" default_features = false -version = "0.3.0" +version = "0.3.1" [dev-dependencies] futures = "0.3" diff --git a/metadata/Cargo.toml b/metadata/Cargo.toml index 6ea90033a..8eb7be8ce 100644 --- a/metadata/Cargo.toml +++ b/metadata/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-metadata" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Lietar "] description = "The metadata logic for librespot" license = "MIT" @@ -15,7 +15,7 @@ log = "0.4" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-protocol] path = "../protocol" -version = "0.3.0" +version = "0.3.1" diff --git a/playback/Cargo.toml b/playback/Cargo.toml index 911800db1..4e8d19c6a 100644 --- a/playback/Cargo.toml +++ b/playback/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-playback" -version = "0.3.0" +version = "0.3.1" authors = ["Sasha Hilton "] description = "The audio playback logic for librespot" license = "MIT" @@ -9,13 +9,13 @@ edition = "2018" [dependencies.librespot-audio] path = "../audio" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-core] path = "../core" -version = "0.3.0" +version = "0.3.1" [dependencies.librespot-metadata] path = "../metadata" -version = "0.3.0" +version = "0.3.1" [dependencies] futures-executor = "0.3" diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml index 83c3a42b0..38f76371e 100644 --- a/protocol/Cargo.toml +++ b/protocol/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "librespot-protocol" -version = "0.3.0" +version = "0.3.1" authors = ["Paul Liétar "] build = "build.rs" description = "The protobuf logic for communicating with Spotify servers" From c1ac4cbb3ad3bbdaeb6f8582186442c69cdae744 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 24 Oct 2021 20:23:47 +0200 Subject: [PATCH 32/76] Update Cargo.lock for 0.3.1 --- Cargo.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82a9d4607..1651f794b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1131,7 +1131,7 @@ dependencies = [ [[package]] name = "librespot" -version = "0.3.0" +version = "0.3.1" dependencies = [ "base64", "env_logger", @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "librespot-audio" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes-ctr", "byteorder", @@ -1170,7 +1170,7 @@ dependencies = [ [[package]] name = "librespot-connect" -version = "0.3.0" +version = "0.3.1" dependencies = [ "form_urlencoded", "futures-util", @@ -1189,7 +1189,7 @@ dependencies = [ [[package]] name = "librespot-core" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes", "base64", @@ -1229,7 +1229,7 @@ dependencies = [ [[package]] name = "librespot-discovery" -version = "0.3.0" +version = "0.3.1" dependencies = [ "aes-ctr", "base64", @@ -1254,7 +1254,7 @@ dependencies = [ [[package]] name = "librespot-metadata" -version = "0.3.0" +version = "0.3.1" dependencies = [ "async-trait", "byteorder", @@ -1266,7 +1266,7 @@ dependencies = [ [[package]] name = "librespot-playback" -version = "0.3.0" +version = "0.3.1" dependencies = [ "alsa", "byteorder", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "librespot-protocol" -version = "0.3.0" +version = "0.3.1" dependencies = [ "glob", "protobuf", From 72b2c01b3ab8cf0c48cd89d450367148ff8b00c4 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Tue, 26 Oct 2021 20:10:39 +0200 Subject: [PATCH 33/76] Update crates --- Cargo.lock | 312 ++++++++++++++++++++++++++--------------------------- 1 file changed, 151 insertions(+), 161 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1651f794b..07f1e23d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,9 +78,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.43" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28ae2b3dec75a406790005a200b1bd89785afc02517a00ca99ecfe093ee9e6cf" +checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" [[package]] name = "async-trait" @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "block-buffer" @@ -152,9 +152,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c59e7af012c713f529e7a3ee57ce9b31ddd858d4b512923602f74608b009631" +checksum = "8f1e260c3a9040a7c19a12468758f4c16f31a81a1fe087482be9570ec864bb6c" [[package]] name = "byteorder" @@ -164,15 +164,15 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.69" +version = "1.0.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2" +checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" dependencies = [ "jobserver", ] @@ -228,13 +228,13 @@ dependencies = [ [[package]] name = "clang-sys" -version = "1.2.0" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "853eda514c284c2287f4bf20ae614f8781f40a81d32ecda6e91449304dfe077c" +checksum = "10612c0ec0e0a1ff0e97980647cb058a6e7aedb913d01d009c406b8b7d0b26ee" dependencies = [ "glob", "libc", - "libloading 0.7.0", + "libloading 0.7.1", ] [[package]] @@ -250,9 +250,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2d47c1b11006b87e492b53b313bb699ce60e16613c4dddaa91f8f7c220ab2fa" +checksum = "a909e4d93292cd8e9c42e189f61681eff9d67b6541f96b8a1a737f23737bd001" dependencies = [ "bytes", "memchr", @@ -260,9 +260,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea221b5284a47e40033bf9b66f35f984ec0ea2931eb03505246cd27a963f981b" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "coreaudio-rs" @@ -311,9 +311,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.1.5" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66c99696f6c9dd7f35d486b9d04d7e6e202aa3e8c40d553f2fdf5e7e0c6a71ef" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" dependencies = [ "libc", ] @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "futures" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adc00f486adfc9ce99f77d717836f0c5aa84965eb0b4f051f4e83f7cab53f8b" +checksum = "a12aa0eb539080d55c3f2d45a67c3b58b6b0773c1a3ca2dfec66d58c97fd66ca" dependencies = [ "futures-channel", "futures-core", @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74ed2411805f6e4e3d9bc904c95d5d423b89b3b25dc0250aa74729de20629ff9" +checksum = "5da6ba8c3bb3c165d3c7319fc1cc8304facf1fb8db99c5de877183c08a273888" dependencies = [ "futures-core", "futures-sink", @@ -464,15 +464,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99" +checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d" [[package]] name = "futures-executor" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c" +checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c" dependencies = [ "futures-core", "futures-task", @@ -481,15 +481,15 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b0e06c393068f3a6ef246c75cdca793d6a46347e75286933e5e75fd2fd11582" +checksum = "522de2a0fe3e380f1bc577ba0474108faf3f6b18321dbf60b3b9c39a75073377" [[package]] name = "futures-macro" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54913bae956fb8df7f4dc6fc90362aa72e69148e3f39041fbe8742d21e0ac57" +checksum = "18e4a4b95cea4b4ccbcf1c5675ca7c4ee4e9e75eb79944d07defde18068f79bb" dependencies = [ "autocfg", "proc-macro-hack", @@ -500,21 +500,21 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f30aaa67363d119812743aa5f33c201a7a66329f97d1a887022971feea4b53" +checksum = "36ea153c13024fe480590b3e3d4cad89a0cfacecc24577b68f86c6ced9c2bc11" [[package]] name = "futures-task" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2" +checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99" [[package]] name = "futures-util" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78" +checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481" dependencies = [ "autocfg", "futures-channel", @@ -729,18 +729,18 @@ checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" [[package]] name = "headers" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b7591fb62902706ae8e7aaff416b1b0fa2c0fd0878b46dc13baa3712d8a855" +checksum = "a4c4eb0471fcb85846d8b0690695ef354f9afb11cb03cac2e1d7c9253351afb0" dependencies = [ "base64", "bitflags", "bytes", "headers-core", "http", + "httpdate", "mime", "sha-1", - "time", ] [[package]] @@ -799,9 +799,9 @@ dependencies = [ [[package]] name = "http" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527e8c9ac747e28542699a951517aa9a6945af506cd1f2e1b53a576c17b6cc11" +checksum = "1323096b05d41827dadeaee54c9981958c0f94e670bc94ed80037d1a7b8b186b" dependencies = [ "bytes", "fnv", @@ -810,9 +810,9 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "399c583b2979440c60be0821a6199eca73bc3c8dcd9d070d75ac726e2c6186e5" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" dependencies = [ "bytes", "http", @@ -839,9 +839,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.11" +version = "0.14.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b61cf2d1aebcf6e6352c97b81dc2244ca29194be1b276f5d8ad5c6330fffb11" +checksum = "2b91bb1f221b6ea1f1e4371216b70f40748774c2fb5971b450c07773fb92d26b" dependencies = [ "bytes", "futures-channel", @@ -894,9 +894,9 @@ dependencies = [ [[package]] name = "if-addrs" -version = "0.6.5" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28538916eb3f3976311f5dfbe67b5362d0add1293d0a9cad17debf86f8e3aa48" +checksum = "c9a83ec4af652890ac713ffd8dc859e650420a5ef47f7b9be29b6664ab50fbc8" dependencies = [ "if-addrs-sys", "libc", @@ -925,9 +925,9 @@ dependencies = [ [[package]] name = "instant" -version = "0.1.10" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ "cfg-if 1.0.0", ] @@ -943,15 +943,15 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "jack" -version = "0.7.1" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49e720259b4a3e1f33cba335ca524a99a5f2411d405b05f6405fadd69269e2db" +checksum = "39722b9795ae57c6967da99b1ab009fe72897fcbc59be59508c7c520327d9e34" dependencies = [ "bitflags", "jack-sys", @@ -1001,9 +1001,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4bf49d50e2961077d9c99f4b7997d770a1114f087c3c2e0069b36c13fc2979d" +checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" dependencies = [ "wasm-bindgen", ] @@ -1033,9 +1033,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.99" +version = "0.2.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765" +checksum = "869d572136620d55835903746bcb5cdc54cb2851fd0aeec53220b4bb65ef3013" [[package]] name = "libloading" @@ -1049,9 +1049,9 @@ dependencies = [ [[package]] name = "libloading" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a" +checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" dependencies = [ "cfg-if 1.0.0", "winapi", @@ -1065,9 +1065,9 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libmdns" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98477a6781ae1d6a1c2aeabfd2e23353a75fe8eb7c2545f6ed282ac8f3e2fc53" +checksum = "fac185a4d02e873c6d1ead59d674651f8ae5ec23ffe1637bee8de80665562a6a" dependencies = [ "byteorder", "futures-util", @@ -1083,9 +1083,9 @@ dependencies = [ [[package]] name = "libpulse-binding" -version = "2.24.0" +version = "2.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04b4154b9bc606019cb15125f96e08e1e9c4f53d55315f1ef69ae229e30d1765" +checksum = "86835d7763ded6bc16b6c0061ec60214da7550dfcd4ef93745f6f0096129676a" dependencies = [ "bitflags", "libc", @@ -1097,9 +1097,9 @@ dependencies = [ [[package]] name = "libpulse-simple-binding" -version = "2.24.0" +version = "2.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1165af13c42b9c325582b1a75eaa4a0f176c9094bb3a13877826e9be24881231" +checksum = "d6a22538257c4d522bea6089d6478507f5d2589ea32150e20740aaaaaba44590" dependencies = [ "libpulse-binding", "libpulse-simple-sys", @@ -1108,9 +1108,9 @@ dependencies = [ [[package]] name = "libpulse-simple-sys" -version = "1.19.0" +version = "1.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83346d68605e656afdefa9a8a2f1968fa05ab9369b55f2e26f7bf2a11b7e8444" +checksum = "0b8b0fcb9665401cc7c156c337c8edc7eb4e797b9d3ae1667e1e9e17b29e0c7c" dependencies = [ "libpulse-sys", "pkg-config", @@ -1118,9 +1118,9 @@ dependencies = [ [[package]] name = "libpulse-sys" -version = "1.19.1" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ebed2cc92c38cac12307892ce6fb17e2e950bfda1ed17b3e1d47fd5184c8f2b" +checksum = "f12950b69c1b66233a900414befde36c8d4ea49deec1e1f34e4cd2f586e00c7d" dependencies = [ "libc", "num-derive", @@ -1307,9 +1307,9 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" +checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109" dependencies = [ "scopeguard", ] @@ -1350,15 +1350,6 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" -[[package]] -name = "memoffset" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1367,9 +1358,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "mio" -version = "0.7.13" +version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" +checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc" dependencies = [ "libc", "log", @@ -1476,15 +1467,14 @@ checksum = "c44922cb3dbb1c70b5e5f443d63b64363a898564d739ba5198e3a9138442868d" [[package]] name = "nix" -version = "0.20.1" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df8e5e343312e7fbeb2a52139114e9e702991ef9c2aea6817ff2440b35647d56" +checksum = "fa9b4819da1bc61c0ea48b63b7bc8604064dd43013e7cc325df098d49cd7c18a" dependencies = [ "bitflags", "cc", "cfg-if 1.0.0", "libc", - "memoffset", ] [[package]] @@ -1586,7 +1576,7 @@ version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "486ea01961c4a818096de679a8b740b26d9033146ac5291b1c98557658f8cdd9" dependencies = [ - "proc-macro-crate 1.0.0", + "proc-macro-crate 1.1.0", "proc-macro2", "quote", "syn", @@ -1638,9 +1628,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "parking_lot" -version = "0.11.1" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", @@ -1649,9 +1639,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.8.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" dependencies = [ "cfg-if 1.0.0", "instant", @@ -1703,9 +1693,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.19" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3831453b3449ceb48b6d9c7ad7c96d5ea673e9b470a1dc578c2ce6521230884c" +checksum = "12295df4f294471248581bc09bef3c38a5e46f1e36d6a37353621a0c6c357e1f" [[package]] name = "portaudio-rs" @@ -1730,9 +1720,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac74c624d6b2d21f425f752262f42188365d7b8ff1aff74c82e45136510a4857" +checksum = "ed0cfbc8191465bed66e1718596ee0b0b35d5ee1f41c5df2189d0fe8bde535ba" [[package]] name = "pretty-hex" @@ -1742,9 +1732,9 @@ checksum = "bc5c99d529f0d30937f6f4b8a86d988047327bb88d04d2c4afc356de74722131" [[package]] name = "priority-queue" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1340009a04e81f656a4e45e295f0b1191c81de424bf940c865e33577a8e223" +checksum = "cf40e51ccefb72d42720609e1d3c518de8b5800d723a09358d4a6d6245e1f8ca" dependencies = [ "autocfg", "indexmap", @@ -1761,9 +1751,9 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41fdbd1df62156fbc5945f4762632564d7d038153091c3fcf1067f6aef7cff92" +checksum = "1ebace6889caf889b4d3f76becee12e90353f2b8c7d875534a71e5742f8f6f83" dependencies = [ "thiserror", "toml", @@ -1807,33 +1797,33 @@ checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.28" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612" +checksum = "b581350bde2d774a19c6f30346796806b8f42b5fd3458c5f9a8623337fb27897" dependencies = [ "unicode-xid", ] [[package]] name = "protobuf" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020f86b07722c5c4291f7c723eac4676b3892d47d9a7708dc2779696407f039b" +checksum = "47c327e191621a2158159df97cdbc2e7074bb4e940275e35abf38eb3d2595754" [[package]] name = "protobuf-codegen" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b8ac7c5128619b0df145d9bace18e8ed057f18aebda1aa837a5525d4422f68c" +checksum = "3df8c98c08bd4d6653c2dbae00bd68c1d1d82a360265a5b0bbc73d48c63cb853" dependencies = [ "protobuf", ] [[package]] name = "protobuf-codegen-pure" -version = "2.25.0" +version = "2.25.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6d0daa1b61d6e7a128cdca8c8604b3c5ee22c424c15c8d3a92fafffeda18aaf" +checksum = "394a73e2a819405364df8d30042c0f1174737a763e0170497ec9d36f8a2ea8f7" dependencies = [ "protobuf", "protobuf-codegen", @@ -1841,9 +1831,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" dependencies = [ "proc-macro2", ] @@ -2019,18 +2009,18 @@ checksum = "568a8e6258aa33c13358f81fd834adb854c6f7c9468520910a9b1e8fac068012" [[package]] name = "serde" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.127" +version = "1.0.130" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", @@ -2039,9 +2029,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.66" +version = "1.0.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127" +checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" dependencies = [ "itoa", "ryu", @@ -2050,9 +2040,9 @@ dependencies = [ [[package]] name = "sha-1" -version = "0.9.7" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a0c8611594e2ab4ebbf06ec7cbbf0a99450b8570e96cbf5188b5d5f6ef18d81" +checksum = "99cd6713db3cf16b6c84e06321e049a9b9f699826e16096d23bbcc44d15d51a6" dependencies = [ "block-buffer", "cfg-if 1.0.0", @@ -2106,21 +2096,21 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" [[package]] name = "smallvec" -version = "1.6.1" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" +checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309" [[package]] name = "socket2" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "765f090f0e423d2b55843402a07915add955e7d60657db13707a159727326cad" +checksum = "5dc90fe6c7be1a323296982db1836d1ea9e47b6839496dde9a541bc496df3516" dependencies = [ "libc", "winapi", @@ -2164,9 +2154,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.74" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c" +checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194" dependencies = [ "proc-macro2", "quote", @@ -2175,9 +2165,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.12.5" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474aaa926faa1603c40b7885a9eaea29b444d1cb2850cb7c0e37bb1a4182f4fa" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" dependencies = [ "proc-macro2", "quote", @@ -2225,18 +2215,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.26" +version = "1.0.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" dependencies = [ "proc-macro2", "quote", @@ -2255,9 +2245,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.3.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7" dependencies = [ "tinyvec_macros", ] @@ -2270,9 +2260,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.10.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b" +checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" dependencies = [ "autocfg", "bytes", @@ -2289,9 +2279,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.3.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110" +checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd" dependencies = [ "proc-macro2", "quote", @@ -2311,9 +2301,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.6.7" +version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1caa0b0c8d94a049db56b5acf8cba99dc0623aab1b26d5b5f5e2d945846b3592" +checksum = "08d3725d3efa29485e87311c5b699de63cde14b00ed4d256b8318aa30ca452cd" dependencies = [ "bytes", "futures-core", @@ -2340,9 +2330,9 @@ checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" -version = "0.1.26" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09adeb8c97449311ccd28a427f96fb563e7fd31aabf994189879d9da2394b89d" +checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" dependencies = [ "cfg-if 1.0.0", "pin-project-lite", @@ -2351,9 +2341,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.19" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ca517f43f0fb96e0c3072ed5c275fe5eece87e8cb52f4a77b69226d3b1c9df8" +checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" dependencies = [ "lazy_static", ] @@ -2366,15 +2356,15 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "typenum" -version = "1.13.0" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f6906492a7cd215bfa4cf595b600146ccfac0c79bcbd1f3000162af5e8b06" +checksum = "b63708a265f51345575b27fe43f9500ad611579e764c79edbc2037b1121959ec" [[package]] name = "unicode-bidi" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" [[package]] name = "unicode-normalization" @@ -2393,9 +2383,9 @@ checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b" [[package]] name = "unicode-width" -version = "0.1.8" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" [[package]] name = "unicode-xid" @@ -2476,9 +2466,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" [[package]] name = "wasm-bindgen" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce9b1b516211d33767048e5d47fa2a381ed8b76fc48d2ce4aa39877f9f183e0" +checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -2486,9 +2476,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe8dc78e2326ba5f845f4b5bf548401604fa20b1dd1d365fb73b6c1d6364041" +checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" dependencies = [ "bumpalo", "lazy_static", @@ -2501,9 +2491,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44468aa53335841d9d6b6c023eaab07c0cd4bddbcfdee3e2bb1e8d2cb8069fef" +checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2511,9 +2501,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0195807922713af1e67dc66132c7328206ed9766af3858164fb583eedc25fbad" +checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" dependencies = [ "proc-macro2", "quote", @@ -2524,15 +2514,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.76" +version = "0.2.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdb075a845574a1fa5f09fd77e43f7747599301ea3417a9fbffdeedfc1f4a29" +checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" [[package]] name = "web-sys" -version = "0.3.53" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224b2f6b67919060055ef1a67807367c2066ed520c3862cc013d26cf893a783c" +checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" dependencies = [ "js-sys", "wasm-bindgen", From 52bd212e4357a755fd8b680be47f1ab68c822945 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 26 Oct 2021 22:06:52 -0500 Subject: [PATCH 34/76] Add disable credential cache flag As mentioned in https://github.com/librespot-org/librespot/discussions/870, this allows someone who would otherwise like to take advantage of audio file and volume caching to disable credential caching. --- core/src/cache.rs | 29 +++++++++++++++++++---------- src/main.rs | 17 +++++++++++++---- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/core/src/cache.rs b/core/src/cache.rs index 612b7c39d..20270e3e7 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -238,29 +238,38 @@ pub struct RemoveFileError(()); impl Cache { pub fn new>( - system_location: Option

, - audio_location: Option

, + credentials: Option

, + volume: Option

, + audio: Option

, size_limit: Option, ) -> io::Result { - if let Some(location) = &system_location { + let mut size_limiter = None; + + if let Some(location) = &credentials { fs::create_dir_all(location)?; } - let mut size_limiter = None; + let credentials_location = credentials + .as_ref() + .map(|p| p.as_ref().join("credentials.json")); - if let Some(location) = &audio_location { + if let Some(location) = &volume { fs::create_dir_all(location)?; + } + + let volume_location = volume.as_ref().map(|p| p.as_ref().join("volume")); + + if let Some(location) = &audio { + fs::create_dir_all(location)?; + if let Some(limit) = size_limit { let limiter = FsSizeLimiter::new(location.as_ref(), limit); + size_limiter = Some(Arc::new(limiter)); } } - let audio_location = audio_location.map(|p| p.as_ref().to_owned()); - let volume_location = system_location.as_ref().map(|p| p.as_ref().join("volume")); - let credentials_location = system_location - .as_ref() - .map(|p| p.as_ref().join("credentials.json")); + let audio_location = audio.map(|p| p.as_ref().to_owned()); let cache = Cache { credentials_location, diff --git a/src/main.rs b/src/main.rs index a3522e8c2..c60b28879 100644 --- a/src/main.rs +++ b/src/main.rs @@ -203,6 +203,7 @@ fn get_setup(args: &[String]) -> Setup { const DEVICE: &str = "device"; const DEVICE_TYPE: &str = "device-type"; const DISABLE_AUDIO_CACHE: &str = "disable-audio-cache"; + const DISABLE_CREDENTIAL_CACHE: &str = "disable-credential-cache"; const DISABLE_DISCOVERY: &str = "disable-discovery"; const DISABLE_GAPLESS: &str = "disable-gapless"; const DITHER: &str = "dither"; @@ -256,6 +257,7 @@ fn get_setup(args: &[String]) -> Setup { "Limits the size of the cache for audio files.", "SIZE" ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") + .optflag("", DISABLE_CREDENTIAL_CACHE, "Disable caching of credentials.") .optopt("n", NAME, "Device name.", "NAME") .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") .optopt( @@ -560,10 +562,11 @@ fn get_setup(args: &[String]) -> Setup { let cache = { let audio_dir; - let system_dir; + let cred_dir; + let volume_dir; if matches.opt_present(DISABLE_AUDIO_CACHE) { audio_dir = None; - system_dir = matches + volume_dir = matches .opt_str(SYSTEM_CACHE) .or_else(|| matches.opt_str(CACHE)) .map(|p| p.into()); @@ -572,12 +575,18 @@ fn get_setup(args: &[String]) -> Setup { audio_dir = cache_dir .as_ref() .map(|p| AsRef::::as_ref(p).join("files")); - system_dir = matches + volume_dir = matches .opt_str(SYSTEM_CACHE) .or(cache_dir) .map(|p| p.into()); } + if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + cred_dir = None; + } else { + cred_dir = volume_dir.clone(); + } + let limit = if audio_dir.is_some() { matches .opt_str(CACHE_SIZE_LIMIT) @@ -593,7 +602,7 @@ fn get_setup(args: &[String]) -> Setup { None }; - match Cache::new(system_dir, audio_dir, limit) { + match Cache::new(cred_dir, volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { warn!("Cannot create cache: {}", e); From 9152ca81593d5083fa4da5f787f19696aab516fc Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 26 Oct 2021 22:18:10 -0500 Subject: [PATCH 35/76] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e362ae63..678880eb8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added +- [cache] Add `disable-credential-cache` flag (breaking). + ## [0.3.1] - 2021-10-24 ### Changed From 81e7c61c1789954a4604eb742cf1a7257a7bef3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?No=C3=ABlle?= <27908024+jannuary@users.noreply.github.com> Date: Wed, 27 Oct 2021 20:03:14 +0700 Subject: [PATCH 36/76] README: Mention Spot --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 20afc01b2..5dbb5487c 100644 --- a/README.md +++ b/README.md @@ -116,3 +116,4 @@ functionality. - [librespot-java](https://github.com/devgianlu/librespot-java) - A Java port of librespot. - [ncspot](https://github.com/hrkfdn/ncspot) - Cross-platform ncurses Spotify client. - [ansible-role-librespot](https://github.com/xMordax/ansible-role-librespot/tree/master) - Ansible role that will build, install and configure Librespot. +- [Spot](https://github.com/xou816/spot) - Gtk/Rust native Spotify client for the GNOME desktop. From e543ef72ede07b26f6bbb49b9109db8f3bec6c6b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 27 Oct 2021 10:14:40 -0500 Subject: [PATCH 37/76] Clean up cache logic in main --- src/main.rs | 38 ++++++++++++++++---------------------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/main.rs b/src/main.rs index c60b28879..ae3258a1f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -561,31 +561,25 @@ fn get_setup(args: &[String]) -> Setup { }; let cache = { - let audio_dir; - let cred_dir; - let volume_dir; - if matches.opt_present(DISABLE_AUDIO_CACHE) { - audio_dir = None; - volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or_else(|| matches.opt_str(CACHE)) - .map(|p| p.into()); + let volume_dir = matches + .opt_str(SYSTEM_CACHE) + .or_else(|| matches.opt_str(CACHE)) + .map(|p| p.into()); + + let cred_dir = if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + None } else { - let cache_dir = matches.opt_str(CACHE); - audio_dir = cache_dir - .as_ref() - .map(|p| AsRef::::as_ref(p).join("files")); - volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or(cache_dir) - .map(|p| p.into()); - } + volume_dir.clone() + }; - if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { - cred_dir = None; + let audio_dir = if matches.opt_present(DISABLE_AUDIO_CACHE) { + None } else { - cred_dir = volume_dir.clone(); - } + matches + .opt_str(CACHE) + .as_ref() + .map(|p| AsRef::::as_ref(p).join("files")) + }; let limit = if audio_dir.is_some() { matches From 9e017119bb2aa24793c23c056c8c531fe7a8bceb Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 27 Oct 2021 14:47:33 -0500 Subject: [PATCH 38/76] Address review change request --- core/src/cache.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/core/src/cache.rs b/core/src/cache.rs index 20270e3e7..da2ad0221 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -238,28 +238,28 @@ pub struct RemoveFileError(()); impl Cache { pub fn new>( - credentials: Option

, - volume: Option

, - audio: Option

, + credentials_path: Option

, + volume_path: Option

, + audio_path: Option

, size_limit: Option, ) -> io::Result { let mut size_limiter = None; - if let Some(location) = &credentials { + if let Some(location) = &credentials_path { fs::create_dir_all(location)?; } - let credentials_location = credentials + let credentials_location = credentials_path .as_ref() .map(|p| p.as_ref().join("credentials.json")); - if let Some(location) = &volume { + if let Some(location) = &volume_path { fs::create_dir_all(location)?; } - let volume_location = volume.as_ref().map(|p| p.as_ref().join("volume")); + let volume_location = volume_path.as_ref().map(|p| p.as_ref().join("volume")); - if let Some(location) = &audio { + if let Some(location) = &audio_path { fs::create_dir_all(location)?; if let Some(limit) = size_limit { @@ -269,7 +269,7 @@ impl Cache { } } - let audio_location = audio.map(|p| p.as_ref().to_owned()); + let audio_location = audio_path.map(|p| p.as_ref().to_owned()); let cache = Cache { credentials_location, From 24e4d2b636e40c4adc039ca200d2f4611b502619 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 28 Oct 2021 09:10:10 -0500 Subject: [PATCH 39/76] Prevent librespot from becoming a zombie Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. --- CHANGELOG.md | 3 +++ src/main.rs | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 678880eb8..a8da8d803 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - [cache] Add `disable-credential-cache` flag (breaking). +### Fixed +- [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. + ## [0.3.1] - 2021-10-24 ### Changed diff --git a/src/main.rs b/src/main.rs index ae3258a1f..51519013e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -647,6 +647,11 @@ fn get_setup(args: &[String]) -> Setup { ) }; + if credentials.is_none() && matches.opt_present(DISABLE_DISCOVERY) { + error!("Credentials are required if discovery is disabled."); + exit(1); + } + let session_config = { let device_id = device_id(&name); @@ -923,7 +928,8 @@ async fn main() { player_event_channel = Some(event_channel); }, Err(e) => { - warn!("Connection failed: {}", e); + error!("Connection failed: {}", e); + exit(1); } }, _ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => { From 7f84ab53232d5d790480ad89654b86abf9244029 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Thu, 28 Oct 2021 17:43:41 +0200 Subject: [PATCH 40/76] Fix self-check output. My merging librespot v0.3.0 broke it. --- src/spotty.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spotty.rs b/src/spotty.rs index 15dc09aaf..b2037d12a 100644 --- a/src/spotty.rs +++ b/src/spotty.rs @@ -30,7 +30,7 @@ const DEBUGMODE: bool = true; const DEBUGMODE: bool = false; pub fn check(version_info: String) { - println!("{}", version_info.to_string()); + println!("ok {}", version_info.to_string()); let capabilities = json!({ "version": env!("CARGO_PKG_VERSION").to_string(), From 50ed4fcb7e042f7a46823d3fd7d374ea64e34680 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Tue, 9 Nov 2021 20:52:39 +0100 Subject: [PATCH 41/76] Use alternative Docker image to build Pi0 compatible ARMv7 binary. --- .github/workflows/build-spotty-linux.yml | 5 ++++- docker/docker-build.sh | 8 ++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-spotty-linux.yml b/.github/workflows/build-spotty-linux.yml index 83fd4069a..40dff39f1 100644 --- a/.github/workflows/build-spotty-linux.yml +++ b/.github/workflows/build-spotty-linux.yml @@ -21,7 +21,10 @@ jobs: contents: ${{ env.KEYMASTER_CLIENT_ID }} write-mode: overwrite - - name: Build + - name: Build ARMv7 + run: docker run --rm -v $(pwd):/source dlecan/rust-crosscompiler-arm:stable + + - name: Build other binaries (x86_64, i686, ARMv8) run: ./xbuild.sh - name: Upload artifacts diff --git a/docker/docker-build.sh b/docker/docker-build.sh index acb14f55b..1237f5033 100755 --- a/docker/docker-build.sh +++ b/docker/docker-build.sh @@ -12,8 +12,12 @@ rm -f $DESTDIR/arm-linux/* function build { echo Building for $1 to $3... - cargo build --release --target $1 \ - && $2 /build/$1/release/spotty \ + + if [!-f /build/$1/release/spotty]; then + cargo build --release --target $1 + fi + + $2 /build/$1/release/spotty \ && cp /build/$1/release/spotty $DESTDIR/$3 } From 0e9fdbe6b443c9d55dd9b9148ffc62a8e6d20c5b Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 30 Oct 2021 14:22:24 -0500 Subject: [PATCH 42/76] Refactor main.rs * Don't panic when parsing options. Instead list valid values and exit. * Get rid of needless .expect in playback/src/audio_backend/mod.rs. * Enforce reasonable ranges for option values (breaking). * Don't evaluate options that would otherwise have no effect. * Add pub const MIXERS to mixer/mod.rs very similar to the audio_backend's implementation. (non-breaking though) * Use different option descriptions and error messages based on what backends are enabled at build time. * Add a -q, --quiet option that changed the logging level to warn. * Add a short name for every flag and option. * Note removed options. * Other misc cleanups. --- CHANGELOG.md | 13 + core/src/config.rs | 12 + playback/src/audio_backend/mod.rs | 7 +- playback/src/config.rs | 4 +- playback/src/mixer/mod.rs | 18 +- src/main.rs | 1179 ++++++++++++++++++++++------- 6 files changed, 935 insertions(+), 298 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8da8d803..c480e03f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,11 +7,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- [main] Enforce reasonable ranges for option values (breaking). +- [main] Don't evaluate options that would otherwise have no effect. + ### Added - [cache] Add `disable-credential-cache` flag (breaking). +- [main] Use different option descriptions and error messages based on what backends are enabled at build time. +- [main] Add a `-q`, `--quiet` option that changes the logging level to warn. +- [main] Add a short name for every flag and option. ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. +- [main] Don't panic when parsing options. Instead list valid values and exit. + +### Removed +- [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. +- [playback] `alsamixer`: previously deprecated option `mixer-name` has been removed. +- [playback] `alsamixer`: previously deprecated option `mixer-index` has been removed. ## [0.3.1] - 2021-10-24 diff --git a/core/src/config.rs b/core/src/config.rs index 0e3eaf4ae..b8c448c2c 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -125,3 +125,15 @@ pub struct ConnectConfig { pub has_volume_ctrl: bool, pub autoplay: bool, } + +impl Default for ConnectConfig { + fn default() -> ConnectConfig { + ConnectConfig { + name: "Librespot".to_string(), + device_type: DeviceType::default(), + initial_volume: Some(50), + has_volume_ctrl: true, + autoplay: false, + } + } +} diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index b89232b7e..4d3b01714 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -146,11 +146,6 @@ pub fn find(name: Option) -> Option { .find(|backend| name == backend.0) .map(|backend| backend.1) } else { - Some( - BACKENDS - .first() - .expect("No backends were enabled at build time") - .1, - ) + BACKENDS.first().map(|backend| backend.1) } } diff --git a/playback/src/config.rs b/playback/src/config.rs index c442faee7..b8313bf40 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -76,7 +76,7 @@ impl AudioFormat { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NormalisationType { Album, Track, @@ -101,7 +101,7 @@ impl Default for NormalisationType { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq)] pub enum NormalisationMethod { Basic, Dynamic, diff --git a/playback/src/mixer/mod.rs b/playback/src/mixer/mod.rs index 5397598fa..a3c7a5a1d 100644 --- a/playback/src/mixer/mod.rs +++ b/playback/src/mixer/mod.rs @@ -53,11 +53,19 @@ fn mk_sink(config: MixerConfig) -> Box { Box::new(M::open(config)) } +pub const MIXERS: &[(&str, MixerFn)] = &[ + (SoftMixer::NAME, mk_sink::), // default goes first + #[cfg(feature = "alsa-backend")] + (AlsaMixer::NAME, mk_sink::), +]; + pub fn find(name: Option<&str>) -> Option { - match name { - None | Some(SoftMixer::NAME) => Some(mk_sink::), - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => Some(mk_sink::), - _ => None, + if let Some(name) = name { + MIXERS + .iter() + .find(|mixer| name == mixer.0) + .map(|mixer| mixer.1) + } else { + MIXERS.first().map(|mixer| mixer.1) } } diff --git a/src/main.rs b/src/main.rs index 51519013e..990de629b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,15 +19,15 @@ use librespot::playback::config::{ use librespot::playback::dither; #[cfg(feature = "alsa-backend")] use librespot::playback::mixer::alsamixer::AlsaMixer; -use librespot::playback::mixer::mappings::MappedCtrl; use librespot::playback::mixer::{self, MixerConfig, MixerFn}; -use librespot::playback::player::{db_to_ratio, Player}; +use librespot::playback::player::{db_to_ratio, ratio_to_db, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; use std::io::{stderr, Write}; +use std::ops::RangeInclusive; use std::path::Path; use std::pin::Pin; use std::process::exit; @@ -44,7 +44,7 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } -fn setup_logging(verbose: bool) { +fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { Ok(config) => { @@ -53,21 +53,29 @@ fn setup_logging(verbose: bool) { if verbose { warn!("`--verbose` flag overidden by `RUST_LOG` environment variable"); + } else if quiet { + warn!("`--quiet` flag overidden by `RUST_LOG` environment variable"); } } Err(_) => { if verbose { builder.parse_filters("libmdns=info,librespot=trace"); + } else if quiet { + builder.parse_filters("libmdns=warn,librespot=warn"); } else { builder.parse_filters("libmdns=info,librespot=info"); } builder.init(); + + if verbose && quiet { + warn!("`--verbose` and `--quiet` are mutually exclusive. Logging can not be both verbose and quiet. Using verbose mode."); + } } } } fn list_backends() { - println!("Available backends : "); + println!("Available backends: "); for (&(name, _), idx) in BACKENDS.iter().zip(0..) { if idx == 0 { println!("- {} (default)", name); @@ -194,11 +202,18 @@ struct Setup { } fn get_setup(args: &[String]) -> Setup { + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; + const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; + const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; + const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; + const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; + const AP_PORT: &str = "ap-port"; const AUTOPLAY: &str = "autoplay"; const BACKEND: &str = "backend"; - const BITRATE: &str = "b"; - const CACHE: &str = "c"; + const BITRATE: &str = "bitrate"; + const CACHE: &str = "cache"; const CACHE_SIZE_LIMIT: &str = "cache-size-limit"; const DEVICE: &str = "device"; const DEVICE_TYPE: &str = "device-type"; @@ -210,7 +225,7 @@ fn get_setup(args: &[String]) -> Setup { const EMIT_SINK_EVENTS: &str = "emit-sink-events"; const ENABLE_VOLUME_NORMALISATION: &str = "enable-volume-normalisation"; const FORMAT: &str = "format"; - const HELP: &str = "h"; + const HELP: &str = "help"; const INITIAL_VOLUME: &str = "initial-volume"; const MIXER_TYPE: &str = "mixer"; const ALSA_MIXER_DEVICE: &str = "alsa-mixer-device"; @@ -228,6 +243,7 @@ fn get_setup(args: &[String]) -> Setup { const PASSTHROUGH: &str = "passthrough"; const PASSWORD: &str = "password"; const PROXY: &str = "proxy"; + const QUIET: &str = "quiet"; const SYSTEM_CACHE: &str = "system-cache"; const USERNAME: &str = "username"; const VERBOSE: &str = "verbose"; @@ -236,196 +252,331 @@ fn get_setup(args: &[String]) -> Setup { const VOLUME_RANGE: &str = "volume-range"; const ZEROCONF_PORT: &str = "zeroconf-port"; + // Mostly arbitrary. + const AUTOPLAY_SHORT: &str = "A"; + const AP_PORT_SHORT: &str = "a"; + const BACKEND_SHORT: &str = "B"; + const BITRATE_SHORT: &str = "b"; + const SYSTEM_CACHE_SHORT: &str = "C"; + const CACHE_SHORT: &str = "c"; + const DITHER_SHORT: &str = "D"; + const DEVICE_SHORT: &str = "d"; + const VOLUME_CTRL_SHORT: &str = "E"; + const VOLUME_RANGE_SHORT: &str = "e"; + const DEVICE_TYPE_SHORT: &str = "F"; + const FORMAT_SHORT: &str = "f"; + const DISABLE_AUDIO_CACHE_SHORT: &str = "G"; + const DISABLE_GAPLESS_SHORT: &str = "g"; + const DISABLE_CREDENTIAL_CACHE_SHORT: &str = "H"; + const HELP_SHORT: &str = "h"; + const CACHE_SIZE_LIMIT_SHORT: &str = "M"; + const MIXER_TYPE_SHORT: &str = "m"; + const ENABLE_VOLUME_NORMALISATION_SHORT: &str = "N"; + const NAME_SHORT: &str = "n"; + const DISABLE_DISCOVERY_SHORT: &str = "O"; + const ONEVENT_SHORT: &str = "o"; + const PASSTHROUGH_SHORT: &str = "P"; + const PASSWORD_SHORT: &str = "p"; + const EMIT_SINK_EVENTS_SHORT: &str = "Q"; + const QUIET_SHORT: &str = "q"; + const INITIAL_VOLUME_SHORT: &str = "R"; + const ALSA_MIXER_DEVICE_SHORT: &str = "S"; + const ALSA_MIXER_INDEX_SHORT: &str = "s"; + const ALSA_MIXER_CONTROL_SHORT: &str = "T"; + const NORMALISATION_ATTACK_SHORT: &str = "U"; + const USERNAME_SHORT: &str = "u"; + const VERSION_SHORT: &str = "V"; + const VERBOSE_SHORT: &str = "v"; + const NORMALISATION_GAIN_TYPE_SHORT: &str = "W"; + const NORMALISATION_KNEE_SHORT: &str = "w"; + const NORMALISATION_METHOD_SHORT: &str = "X"; + const PROXY_SHORT: &str = "x"; + const NORMALISATION_PREGAIN_SHORT: &str = "Y"; + const NORMALISATION_RELEASE_SHORT: &str = "y"; + const NORMALISATION_THRESHOLD_SHORT: &str = "Z"; + const ZEROCONF_PORT_SHORT: &str = "z"; + + // Options that have different desc's + // depending on what backends were enabled at build time. + #[cfg(feature = "alsa-backend")] + const MIXER_TYPE_DESC: &str = "Mixer to use {alsa|softvol}. Defaults to softvol."; + #[cfg(not(feature = "alsa-backend"))] + const MIXER_TYPE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] + const DEVICE_DESC: &str = "Audio device to use. Use ? to list options if using alsa, portaudio or rodio. Defaults to the backend's default."; + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + const DEVICE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_CONTROL_DESC: &str = + "Alsa mixer control, e.g. PCM, Master or similar. Defaults to PCM."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_CONTROL_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_DEVICE_DESC: &str = "Alsa mixer device, e.g hw:0 or similar from `aplay -l`. Defaults to `--device` if specified, default otherwise."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_DEVICE_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const ALSA_MIXER_INDEX_DESC: &str = "Alsa index of the cards mixer. Defaults to 0."; + #[cfg(not(feature = "alsa-backend"))] + const ALSA_MIXER_INDEX_DESC: &str = "Not supported by the included audio backend(s)."; + #[cfg(feature = "alsa-backend")] + const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Default for softvol: 50. For the alsa mixer: the current volume."; + #[cfg(not(feature = "alsa-backend"))] + const INITIAL_VOLUME_DESC: &str = "Initial volume in % from 0 - 100. Defaults to 50."; + #[cfg(feature = "alsa-backend")] + const VOLUME_RANGE_DESC: &str = "Range of the volume control (dB) from 0.0 to 100.0. Default for softvol: 60.0. For the alsa mixer: what the control supports."; + #[cfg(not(feature = "alsa-backend"))] + const VOLUME_RANGE_DESC: &str = + "Range of the volume control (dB) from 0.0 to 100.0. Defaults to 60.0."; + let mut opts = getopts::Options::new(); opts.optflag( + HELP_SHORT, HELP, - "help", "Print this help menu.", - ).optopt( - CACHE, - "cache", - "Path to a directory where files will be cached.", - "PATH", - ).optopt( - "", - SYSTEM_CACHE, - "Path to a directory where system files (credentials, volume) will be cached. May be different from the cache option value.", - "PATH", - ).optopt( - "", - CACHE_SIZE_LIMIT, - "Limits the size of the cache for audio files.", - "SIZE" - ).optflag("", DISABLE_AUDIO_CACHE, "Disable caching of the audio data.") - .optflag("", DISABLE_CREDENTIAL_CACHE, "Disable caching of credentials.") - .optopt("n", NAME, "Device name.", "NAME") - .optopt("", DEVICE_TYPE, "Displayed device type. Defaults to 'Speaker'.", "TYPE") - .optopt( - BITRATE, - "bitrate", - "Bitrate (kbps) {96|160|320}. Defaults to 160.", - "BITRATE", ) - .optopt( - "", - ONEVENT, - "Run PROGRAM when a playback event occurs.", - "PROGRAM", + .optflag( + VERSION_SHORT, + VERSION, + "Display librespot version string.", + ) + .optflag( + VERBOSE_SHORT, + VERBOSE, + "Enable verbose log output.", + ) + .optflag( + QUIET_SHORT, + QUIET, + "Only log warning and error messages.", + ) + .optflag( + DISABLE_AUDIO_CACHE_SHORT, + DISABLE_AUDIO_CACHE, + "Disable caching of the audio data.", + ) + .optflag( + DISABLE_CREDENTIAL_CACHE_SHORT, + DISABLE_CREDENTIAL_CACHE, + "Disable caching of credentials.", + ) + .optflag( + DISABLE_DISCOVERY_SHORT, + DISABLE_DISCOVERY, + "Disable zeroconf discovery mode.", + ) + .optflag( + DISABLE_GAPLESS_SHORT, + DISABLE_GAPLESS, + "Disable gapless playback.", + ) + .optflag( + EMIT_SINK_EVENTS_SHORT, + EMIT_SINK_EVENTS, + "Run PROGRAM set by `--onevent` before the sink is opened and after it is closed.", + ) + .optflag( + AUTOPLAY_SHORT, + AUTOPLAY, + "Automatically play similar songs when your music ends.", + ) + .optflag( + PASSTHROUGH_SHORT, + PASSTHROUGH, + "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + ) + .optflag( + ENABLE_VOLUME_NORMALISATION_SHORT, + ENABLE_VOLUME_NORMALISATION, + "Play all tracks at approximately the same apparent volume.", ) - .optflag("", EMIT_SINK_EVENTS, "Run PROGRAM set by --onevent before sink is opened and after it is closed.") - .optflag("v", VERBOSE, "Enable verbose output.") - .optflag("V", VERSION, "Display librespot version string.") - .optopt("u", USERNAME, "Username used to sign in with.", "USERNAME") - .optopt("p", PASSWORD, "Password used to sign in with.", "PASSWORD") - .optopt("", PROXY, "HTTP proxy to use when connecting.", "URL") - .optopt("", AP_PORT, "Connect to an AP with a specified port. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", "PORT") - .optflag("", DISABLE_DISCOVERY, "Disable zeroconf discovery mode.") .optopt( - "", - BACKEND, - "Audio backend to use. Use '?' to list options.", + NAME_SHORT, + NAME, + "Device name. Defaults to Librespot.", "NAME", ) .optopt( - "", - DEVICE, - "Audio device to use. Use '?' to list options if using alsa, portaudio or rodio. Defaults to the backend's default.", - "NAME", + BITRATE_SHORT, + BITRATE, + "Bitrate (kbps) {96|160|320}. Defaults to 160.", + "BITRATE", ) .optopt( - "", + FORMAT_SHORT, FORMAT, "Output format {F64|F32|S32|S24|S24_3|S16}. Defaults to S16.", "FORMAT", ) .optopt( - "", + DITHER_SHORT, DITHER, - "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to 'tpdf' for formats S16, S24, S24_3 and 'none' for other formats.", + "Specify the dither algorithm to use {none|gpdf|tpdf|tpdf_hp}. Defaults to tpdf for formats S16, S24, S24_3 and none for other formats.", "DITHER", ) - .optopt("m", MIXER_TYPE, "Mixer to use {alsa|softvol}. Defaults to softvol", "MIXER") .optopt( - "", - "mixer-name", // deprecated - "", - "", + DEVICE_TYPE_SHORT, + DEVICE_TYPE, + "Displayed device type. Defaults to speaker.", + "TYPE", ) .optopt( - "", - ALSA_MIXER_CONTROL, - "Alsa mixer control, e.g. 'PCM', 'Master' or similar. Defaults to 'PCM'.", + CACHE_SHORT, + CACHE, + "Path to a directory where files will be cached.", + "PATH", + ) + .optopt( + SYSTEM_CACHE_SHORT, + SYSTEM_CACHE, + "Path to a directory where system files (credentials, volume) will be cached. May be different from the `--cache` option value.", + "PATH", + ) + .optopt( + CACHE_SIZE_LIMIT_SHORT, + CACHE_SIZE_LIMIT, + "Limits the size of the cache for audio files. It's possible to use suffixes like K, M or G, e.g. 16G for example.", + "SIZE" + ) + .optopt( + BACKEND_SHORT, + BACKEND, + "Audio backend to use. Use ? to list options.", "NAME", ) .optopt( - "", - "mixer-card", // deprecated - "", - "", + USERNAME_SHORT, + USERNAME, + "Username used to sign in with.", + "USERNAME", ) .optopt( - "", - ALSA_MIXER_DEVICE, - "Alsa mixer device, e.g 'hw:0' or similar from `aplay -l`. Defaults to `--device` if specified, 'default' otherwise.", - "DEVICE", + PASSWORD_SHORT, + PASSWORD, + "Password used to sign in with.", + "PASSWORD", + ) + .optopt( + ONEVENT_SHORT, + ONEVENT, + "Run PROGRAM when a playback event occurs.", + "PROGRAM", + ) + .optopt( + ALSA_MIXER_CONTROL_SHORT, + ALSA_MIXER_CONTROL, + ALSA_MIXER_CONTROL_DESC, + "NAME", ) .optopt( - "", - "mixer-index", // deprecated - "", - "", + ALSA_MIXER_DEVICE_SHORT, + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_DESC, + "DEVICE", ) .optopt( - "", + ALSA_MIXER_INDEX_SHORT, ALSA_MIXER_INDEX, - "Alsa index of the cards mixer. Defaults to 0.", + ALSA_MIXER_INDEX_DESC, "NUMBER", ) .optopt( - "", + MIXER_TYPE_SHORT, + MIXER_TYPE, + MIXER_TYPE_DESC, + "MIXER", + ) + .optopt( + DEVICE_SHORT, + DEVICE, + DEVICE_DESC, + "NAME", + ) + .optopt( + INITIAL_VOLUME_SHORT, INITIAL_VOLUME, - "Initial volume in % from 0-100. Default for softvol: '50'. For the Alsa mixer: the current volume.", + INITIAL_VOLUME_DESC, "VOLUME", ) .optopt( - "", - ZEROCONF_PORT, - "The port the internal server advertises over zeroconf.", - "PORT", + VOLUME_CTRL_SHORT, + VOLUME_CTRL, + "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.", + "VOLUME_CTRL" ) - .optflag( - "", - ENABLE_VOLUME_NORMALISATION, - "Play all tracks at approximately the same apparent volume.", + .optopt( + VOLUME_RANGE_SHORT, + VOLUME_RANGE, + VOLUME_RANGE_DESC, + "RANGE", ) .optopt( - "", + NORMALISATION_METHOD_SHORT, NORMALISATION_METHOD, "Specify the normalisation method to use {basic|dynamic}. Defaults to dynamic.", "METHOD", ) .optopt( - "", + NORMALISATION_GAIN_TYPE_SHORT, NORMALISATION_GAIN_TYPE, "Specify the normalisation gain type to use {track|album|auto}. Defaults to auto.", "TYPE", ) .optopt( - "", + NORMALISATION_PREGAIN_SHORT, NORMALISATION_PREGAIN, - "Pregain (dB) applied by volume normalisation. Defaults to 0.", + "Pregain (dB) applied by volume normalisation from -10.0 to 10.0. Defaults to 0.0.", "PREGAIN", ) .optopt( - "", + NORMALISATION_THRESHOLD_SHORT, NORMALISATION_THRESHOLD, - "Threshold (dBFS) at which the dynamic limiter engages to prevent clipping. Defaults to -2.0.", + "Threshold (dBFS) at which point the dynamic limiter engages to prevent clipping from 0.0 to -10.0. Defaults to -2.0.", "THRESHOLD", ) .optopt( - "", + NORMALISATION_ATTACK_SHORT, NORMALISATION_ATTACK, - "Attack time (ms) in which the dynamic limiter reduces gain. Defaults to 5.", + "Attack time (ms) in which the dynamic limiter reduces gain from 1 to 500. Defaults to 5.", "TIME", ) .optopt( - "", + NORMALISATION_RELEASE_SHORT, NORMALISATION_RELEASE, - "Release or decay time (ms) in which the dynamic limiter restores gain. Defaults to 100.", + "Release or decay time (ms) in which the dynamic limiter restores gain from 1 to 1000. Defaults to 100.", "TIME", ) .optopt( - "", + NORMALISATION_KNEE_SHORT, NORMALISATION_KNEE, - "Knee steepness of the dynamic limiter. Defaults to 1.0.", + "Knee steepness of the dynamic limiter from 0.0 to 2.0. Defaults to 1.0.", "KNEE", ) .optopt( - "", - VOLUME_CTRL, - "Volume control scale type {cubic|fixed|linear|log}. Defaults to log.", - "VOLUME_CTRL" + ZEROCONF_PORT_SHORT, + ZEROCONF_PORT, + "The port the internal server advertises over zeroconf 1 - 65535. Ports <= 1024 may require root privileges.", + "PORT", ) .optopt( - "", - VOLUME_RANGE, - "Range of the volume control (dB). Default for softvol: 60. For the Alsa mixer: what the control supports.", - "RANGE", - ) - .optflag( - "", - AUTOPLAY, - "Automatically play similar songs when your music ends.", - ) - .optflag( - "", - DISABLE_GAPLESS, - "Disable gapless playback.", + PROXY_SHORT, + PROXY, + "HTTP proxy to use when connecting.", + "URL", ) - .optflag( - "", - PASSTHROUGH, - "Pass a raw stream to the output. Only works with the pipe and subprocess backends.", + .optopt( + AP_PORT_SHORT, + AP_PORT, + "Connect to an AP with a specified port 1 - 65535. If no AP with that port is present a fallback AP will be used. Available ports are usually 80, 443 and 4070.", + "PORT", ); let matches = match opts.parse(&args[1..]) { @@ -450,110 +601,216 @@ fn get_setup(args: &[String]) -> Setup { exit(0); } - let verbose = matches.opt_present(VERBOSE); - setup_logging(verbose); + setup_logging(matches.opt_present(QUIET), matches.opt_present(VERBOSE)); info!("{}", get_version_string()); + #[cfg(not(feature = "alsa-backend"))] + for a in &[ + MIXER_TYPE, + ALSA_MIXER_DEVICE, + ALSA_MIXER_INDEX, + ALSA_MIXER_CONTROL, + ] { + if matches.opt_present(a) { + warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); + break; + } + } + let backend_name = matches.opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); } - let backend = audio_backend::find(backend_name).expect("Invalid backend"); + let backend = audio_backend::find(backend_name).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + BACKEND, + BACKEND_SHORT, + matches.opt_str(BACKEND).unwrap_or_default() + ); + list_backends(); + exit(1); + }); let format = matches .opt_str(FORMAT) .as_deref() - .map(|format| AudioFormat::from_str(format).expect("Invalid output format")) + .map(|format| { + AudioFormat::from_str(format).unwrap_or_else(|_| { + error!("Invalid `--{}` / `-{}`: {}", FORMAT, FORMAT_SHORT, format); + println!( + "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", + FORMAT, FORMAT_SHORT + ); + println!("Default: {:?}", AudioFormat::default()); + exit(1); + }) + }) .unwrap_or_default(); + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] let device = matches.opt_str(DEVICE); + + #[cfg(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + ))] if device == Some("?".into()) { backend(device, format); exit(0); } + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + let device: Option = None; + + #[cfg(not(any( + feature = "alsa-backend", + feature = "rodio-backend", + feature = "portaudio-backend" + )))] + if matches.opt_present(DEVICE) { + warn!( + "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", + DEVICE, DEVICE_SHORT, + ); + } + + #[cfg(feature = "alsa-backend")] let mixer_type = matches.opt_str(MIXER_TYPE); - let mixer = mixer::find(mixer_type.as_deref()).expect("Invalid mixer"); + #[cfg(not(feature = "alsa-backend"))] + let mixer_type: Option = None; + + let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + MIXER_TYPE, + MIXER_TYPE_SHORT, + matches.opt_str(MIXER_TYPE).unwrap_or_default() + ); + println!( + "Valid `--{}` / `-{}` values: alsa, softvol", + MIXER_TYPE, MIXER_TYPE_SHORT + ); + println!("Default: softvol"); + exit(1); + }); let mixer_config = { - let mixer_device = match matches.opt_str("mixer-card") { - Some(card) => { - warn!("--mixer-card is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-device instead."); - card - } - None => matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - MixerConfig::default().device - } - }), - }; + let mixer_default_config = MixerConfig::default(); - let index = match matches.opt_str("mixer-index") { - Some(index) => { - warn!("--mixer-index is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-index instead."); - index - .parse::() - .expect("Mixer index is not a valid number") + #[cfg(feature = "alsa-backend")] + let device = matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + if let Some(ref device_name) = device { + device_name.to_string() + } else { + mixer_default_config.device.clone() } - None => matches - .opt_str(ALSA_MIXER_INDEX) - .map(|index| { - index - .parse::() - .expect("Alsa mixer index is not a valid number") + }); + + #[cfg(not(feature = "alsa-backend"))] + let device = mixer_default_config.device; + + #[cfg(feature = "alsa-backend")] + let index = matches + .opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index.parse::().unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index + ); + println!("Default: {}", mixer_default_config.index); + exit(1); }) - .unwrap_or(0), - }; + }) + .unwrap_or_else(|| mixer_default_config.index); - let control = match matches.opt_str("mixer-name") { - Some(name) => { - warn!("--mixer-name is deprecated and will be removed in a future release."); - warn!("Please use --alsa-mixer-control instead."); - name - } - None => matches - .opt_str(ALSA_MIXER_CONTROL) - .unwrap_or_else(|| MixerConfig::default().control), - }; + #[cfg(not(feature = "alsa-backend"))] + let index = mixer_default_config.index; + + #[cfg(feature = "alsa-backend")] + let control = matches + .opt_str(ALSA_MIXER_CONTROL) + .unwrap_or(mixer_default_config.control); - let mut volume_range = matches + #[cfg(not(feature = "alsa-backend"))] + let control = mixer_default_config.control; + + let volume_range = matches .opt_str(VOLUME_RANGE) - .map(|range| range.parse::().unwrap()) + .map(|range| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + VOLUME_RANGE, VOLUME_RANGE_SHORT, range + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + VOLUME_RANGE, + VOLUME_RANGE_SHORT, + VALID_VOLUME_RANGE.start(), + VALID_VOLUME_RANGE.end() + ); + #[cfg(feature = "alsa-backend")] + println!( + "Default: softvol - {}, alsa - what the control supports", + VolumeCtrl::DEFAULT_DB_RANGE + ); + #[cfg(not(feature = "alsa-backend"))] + println!("Default: {}", VolumeCtrl::DEFAULT_DB_RANGE); + }; + + let range = range.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_VOLUME_RANGE).contains(&range) { + on_error(); + exit(1); + } + + range + }) .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => 0.0, // let Alsa query the control + Some(AlsaMixer::NAME) => 0.0, // let alsa query the control _ => VolumeCtrl::DEFAULT_DB_RANGE, }); - if volume_range < 0.0 { - // User might have specified range as minimum dB volume. - volume_range = -volume_range; - warn!( - "Please enter positive volume ranges only, assuming {:.2} dB", - volume_range - ); - } + let volume_ctrl = matches .opt_str(VOLUME_CTRL) .as_deref() .map(|volume_ctrl| { - VolumeCtrl::from_str_with_range(volume_ctrl, volume_range) - .expect("Invalid volume control type") + VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl + ); + println!( + "Valid `--{}` / `-{}` values: cubic, fixed, linear, log", + VOLUME_CTRL, VOLUME_CTRL + ); + println!("Default: log"); + exit(1); + }) }) - .unwrap_or_else(|| { - let mut volume_ctrl = VolumeCtrl::default(); - volume_ctrl.set_db_range(volume_range); - volume_ctrl - }); + .unwrap_or_else(|| VolumeCtrl::Log(volume_range)); MixerConfig { - device: mixer_device, + device, control, index, volume_ctrl, @@ -588,7 +845,10 @@ fn get_setup(args: &[String]) -> Setup { .map(parse_file_size) .map(|e| { e.unwrap_or_else(|e| { - eprintln!("Invalid argument passed as cache size limit: {}", e); + error!( + "Invalid `--{}` / `-{}`: {}", + CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e + ); exit(1); }) }) @@ -596,6 +856,13 @@ fn get_setup(args: &[String]) -> Setup { None }; + if audio_dir.is_none() && matches.opt_present(CACHE_SIZE_LIMIT) { + warn!( + "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", + CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT + ); + } + match Cache::new(cred_dir, volume_dir, audio_dir, limit) { Ok(cache) => Some(cache), Err(e) => { @@ -605,31 +872,6 @@ fn get_setup(args: &[String]) -> Setup { } }; - let initial_volume = matches - .opt_str(INITIAL_VOLUME) - .map(|initial_volume| { - let volume = initial_volume.parse::().unwrap(); - if volume > 100 { - error!("Initial volume must be in the range 0-100."); - // the cast will saturate, not necessary to take further action - } - (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 - }) - .or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => None, - _ => cache.as_ref().and_then(Cache::volume), - }); - - let zeroconf_port = matches - .opt_str(ZEROCONF_PORT) - .map(|port| port.parse::().unwrap()) - .unwrap_or(0); - - let name = matches - .opt_str(NAME) - .unwrap_or_else(|| "Librespot".to_string()); - let credentials = { let cached_credentials = cache.as_ref().and_then(Cache::credentials); @@ -647,13 +889,131 @@ fn get_setup(args: &[String]) -> Setup { ) }; - if credentials.is_none() && matches.opt_present(DISABLE_DISCOVERY) { + let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + + if credentials.is_none() && !enable_discovery { error!("Credentials are required if discovery is disabled."); exit(1); } + if !enable_discovery && matches.opt_present(ZEROCONF_PORT) { + warn!( + "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", + DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + } + + let zeroconf_port = if enable_discovery { + matches + .opt_str(ZEROCONF_PORT) + .map(|port| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port + ); + println!( + "Valid `--{}` / `-{}` values: 1 - 65535", + ZEROCONF_PORT, ZEROCONF_PORT_SHORT + ); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }) + .unwrap_or(0) + } else { + 0 + }; + + let connect_config = { + let connect_default_config = ConnectConfig::default(); + + let name = matches + .opt_str(NAME) + .unwrap_or_else(|| connect_default_config.name.clone()); + + let initial_volume = matches + .opt_str(INITIAL_VOLUME) + .map(|initial_volume| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume + ); + println!( + "Valid `--{}` / `-{}` values: 0 - 100", + INITIAL_VOLUME, INITIAL_VOLUME_SHORT + ); + #[cfg(feature = "alsa-backend")] + println!( + "Default: {}, or the current value when the alsa mixer is used.", + connect_default_config.initial_volume.unwrap_or_default() + ); + #[cfg(not(feature = "alsa-backend"))] + println!( + "Default: {}", + connect_default_config.initial_volume.unwrap_or_default() + ); + }; + + let volume = initial_volume.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if volume > 100 { + on_error(); + exit(1); + } + + (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 + }) + .or_else(|| match mixer_type.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => None, + _ => cache.as_ref().and_then(Cache::volume), + }); + + let device_type = matches + .opt_str(DEVICE_TYPE) + .as_deref() + .map(|device_type| { + DeviceType::from_str(device_type).unwrap_or_else(|_| { + error!("Invalid `--{}` / `-{}`: {}", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); + println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ + gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", + DEVICE_TYPE, DEVICE_TYPE_SHORT + ); + println!("Default: speaker"); + exit(1); + }) + }) + .unwrap_or_default(); + + let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); + let autoplay = matches.opt_present(AUTOPLAY); + + ConnectConfig { + name, + device_type, + initial_volume, + has_volume_ctrl, + autoplay, + } + }; + let session_config = { - let device_id = device_id(&name); + let device_id = device_id(&connect_config.name); SessionConfig { user_agent: version::VERSION_STRING.to_string(), @@ -663,78 +1023,329 @@ fn get_setup(args: &[String]) -> Setup { match Url::parse(&s) { Ok(url) => { if url.host().is_none() || url.port_or_known_default().is_none() { - panic!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); + exit(1); } if url.scheme() != "http" { - panic!("Only unsecure http:// proxies are supported"); + error!("Only unsecure http:// proxies are supported"); + exit(1); } + url }, - Err(err) => panic!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", err) + Err(e) => { + error!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", e); + exit(1); + } } }, ), ap_port: matches .opt_str(AP_PORT) - .map(|port| port.parse::().expect("Invalid port")), + .map(|port| { + let on_error = || { + error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); + println!("Valid `--{}` / `-{}` values: 1 - 65535", AP_PORT, AP_PORT_SHORT); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if port == 0 { + on_error(); + exit(1); + } + + port + }), } }; let player_config = { + let player_default_config = PlayerConfig::default(); + let bitrate = matches .opt_str(BITRATE) .as_deref() - .map(|bitrate| Bitrate::from_str(bitrate).expect("Invalid bitrate")) - .unwrap_or_default(); + .map(|bitrate| { + Bitrate::from_str(bitrate).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + BITRATE, BITRATE_SHORT, bitrate + ); + println!( + "Valid `--{}` / `-{}` values: 96, 160, 320", + BITRATE, BITRATE_SHORT + ); + println!("Default: 160"); + exit(1); + }) + }) + .unwrap_or(player_default_config.bitrate); let gapless = !matches.opt_present(DISABLE_GAPLESS); let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); - let normalisation_method = matches - .opt_str(NORMALISATION_METHOD) - .as_deref() - .map(|method| { - NormalisationMethod::from_str(method).expect("Invalid normalisation method") - }) - .unwrap_or_default(); - let normalisation_type = matches - .opt_str(NORMALISATION_GAIN_TYPE) - .as_deref() - .map(|gain_type| { - NormalisationType::from_str(gain_type).expect("Invalid normalisation type") - }) - .unwrap_or_default(); - let normalisation_pregain = matches - .opt_str(NORMALISATION_PREGAIN) - .map(|pregain| pregain.parse::().expect("Invalid pregain float value")) - .unwrap_or(PlayerConfig::default().normalisation_pregain); - let normalisation_threshold = matches - .opt_str(NORMALISATION_THRESHOLD) - .map(|threshold| { - db_to_ratio( - threshold - .parse::() - .expect("Invalid threshold float value"), - ) - }) - .unwrap_or(PlayerConfig::default().normalisation_threshold); - let normalisation_attack = matches - .opt_str(NORMALISATION_ATTACK) - .map(|attack| { - Duration::from_millis(attack.parse::().expect("Invalid attack value")) - }) - .unwrap_or(PlayerConfig::default().normalisation_attack); - let normalisation_release = matches - .opt_str(NORMALISATION_RELEASE) - .map(|release| { - Duration::from_millis(release.parse::().expect("Invalid release value")) - }) - .unwrap_or(PlayerConfig::default().normalisation_release); - let normalisation_knee = matches - .opt_str(NORMALISATION_KNEE) - .map(|knee| knee.parse::().expect("Invalid knee float value")) - .unwrap_or(PlayerConfig::default().normalisation_knee); + + let normalisation_method; + let normalisation_type; + let normalisation_pregain; + let normalisation_threshold; + let normalisation_attack; + let normalisation_release; + let normalisation_knee; + + if !normalisation { + for a in &[ + NORMALISATION_METHOD, + NORMALISATION_GAIN_TYPE, + NORMALISATION_PREGAIN, + NORMALISATION_THRESHOLD, + NORMALISATION_ATTACK, + NORMALISATION_RELEASE, + NORMALISATION_KNEE, + ] { + if matches.opt_present(a) { + warn!( + "Without the `--{}` / `-{}` flag normalisation options have no effect.", + ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, + ); + break; + } + } + + normalisation_method = player_default_config.normalisation_method; + normalisation_type = player_default_config.normalisation_type; + normalisation_pregain = player_default_config.normalisation_pregain; + normalisation_threshold = player_default_config.normalisation_threshold; + normalisation_attack = player_default_config.normalisation_attack; + normalisation_release = player_default_config.normalisation_release; + normalisation_knee = player_default_config.normalisation_knee; + } else { + normalisation_method = matches + .opt_str(NORMALISATION_METHOD) + .as_deref() + .map(|method| { + warn!( + "`--{}` / `-{}` will be deprecated in a future release.", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + ); + + let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + ); + println!( + "Valid `--{}` / `-{}` values: basic, dynamic", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + ); + println!("Default: {:?}", player_default_config.normalisation_method); + exit(1); + }); + + if matches!(method, NormalisationMethod::Basic) { + warn!( + "`--{}` / `-{}` {:?} will be deprecated in a future release.", + NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method + ); + } + + method + }) + .unwrap_or(player_default_config.normalisation_method); + + normalisation_type = matches + .opt_str(NORMALISATION_GAIN_TYPE) + .as_deref() + .map(|gain_type| { + NormalisationType::from_str(gain_type).unwrap_or_else(|_| { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type + ); + println!( + "Valid `--{}` / `-{}` values: track, album, auto", + NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, + ); + println!("Default: {:?}", player_default_config.normalisation_type); + exit(1); + }) + }) + .unwrap_or(player_default_config.normalisation_type); + + normalisation_pregain = matches + .opt_str(NORMALISATION_PREGAIN) + .map(|pregain| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_PREGAIN, + NORMALISATION_PREGAIN_SHORT, + VALID_NORMALISATION_PREGAIN_RANGE.start(), + VALID_NORMALISATION_PREGAIN_RANGE.end() + ); + println!("Default: {}", player_default_config.normalisation_pregain); + }; + + let pregain = pregain.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_PREGAIN_RANGE).contains(&pregain) { + on_error(); + exit(1); + } + + pregain + }) + .unwrap_or(player_default_config.normalisation_pregain); + + normalisation_threshold = matches + .opt_str(NORMALISATION_THRESHOLD) + .map(|threshold| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_THRESHOLD, + NORMALISATION_THRESHOLD_SHORT, + VALID_NORMALISATION_THRESHOLD_RANGE.start(), + VALID_NORMALISATION_THRESHOLD_RANGE.end() + ); + println!( + "Default: {}", + ratio_to_db(player_default_config.normalisation_threshold) + ); + }; + + let threshold = threshold.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_THRESHOLD_RANGE).contains(&threshold) { + on_error(); + exit(1); + } + + db_to_ratio(threshold) + }) + .unwrap_or(player_default_config.normalisation_threshold); + + normalisation_attack = matches + .opt_str(NORMALISATION_ATTACK) + .map(|attack| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_ATTACK, + NORMALISATION_ATTACK_SHORT, + VALID_NORMALISATION_ATTACK_RANGE.start(), + VALID_NORMALISATION_ATTACK_RANGE.end() + ); + println!( + "Default: {}", + player_default_config.normalisation_attack.as_millis() + ); + }; + + let attack = attack.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_ATTACK_RANGE).contains(&attack) { + on_error(); + exit(1); + } + + Duration::from_millis(attack) + }) + .unwrap_or(player_default_config.normalisation_attack); + + normalisation_release = matches + .opt_str(NORMALISATION_RELEASE) + .map(|release| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_RELEASE, + NORMALISATION_RELEASE_SHORT, + VALID_NORMALISATION_RELEASE_RANGE.start(), + VALID_NORMALISATION_RELEASE_RANGE.end() + ); + println!( + "Default: {}", + player_default_config.normalisation_release.as_millis() + ); + }; + + let release = release.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_RELEASE_RANGE).contains(&release) { + on_error(); + exit(1); + } + + Duration::from_millis(release) + }) + .unwrap_or(player_default_config.normalisation_release); + + normalisation_knee = matches + .opt_str(NORMALISATION_KNEE) + .map(|knee| { + let on_error = || { + error!( + "Invalid `--{}` / `-{}`: {}", + NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee + ); + println!( + "Valid `--{}` / `-{}` values: {} - {}", + NORMALISATION_KNEE, + NORMALISATION_KNEE_SHORT, + VALID_NORMALISATION_KNEE_RANGE.start(), + VALID_NORMALISATION_KNEE_RANGE.end() + ); + println!("Default: {}", player_default_config.normalisation_knee); + }; + + let knee = knee.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); + + if !(VALID_NORMALISATION_KNEE_RANGE).contains(&knee) { + on_error(); + exit(1); + } + + knee + }) + .unwrap_or(player_default_config.normalisation_knee); + } let ditherer_name = matches.opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { @@ -742,15 +1353,32 @@ fn get_setup(args: &[String]) -> Setup { Some("none") => None, // explicitly set on command line Some(_) => { - if format == AudioFormat::F64 || format == AudioFormat::F32 { - unimplemented!("Dithering is not available on format {:?}", format); + if matches!(format, AudioFormat::F64 | AudioFormat::F32) { + error!("Dithering is not available with format: {:?}.", format); + exit(1); } - Some(dither::find_ditherer(ditherer_name).expect("Invalid ditherer")) + + Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { + error!( + "Invalid `--{}` / `-{}`: {}", + DITHER, + DITHER_SHORT, + matches.opt_str(DITHER).unwrap_or_default() + ); + println!( + "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", + DITHER, DITHER_SHORT + ); + println!( + "Default: tpdf for formats S16, S24, S24_3 and none for other formats" + ); + exit(1); + })) } // nothing set on command line => use default None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { - PlayerConfig::default().ditherer + player_default_config.ditherer } _ => None, }, @@ -774,25 +1402,6 @@ fn get_setup(args: &[String]) -> Setup { } }; - let connect_config = { - let device_type = matches - .opt_str(DEVICE_TYPE) - .as_deref() - .map(|device_type| DeviceType::from_str(device_type).expect("Invalid device type")) - .unwrap_or_default(); - let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present(AUTOPLAY); - - ConnectConfig { - name, - device_type, - initial_volume, - has_volume_ctrl, - autoplay, - } - }; - - let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); let player_event_program = matches.opt_str(ONEVENT); let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); From 3016d6fbdb867eeb9cff7aa19fe61fa29ea13b72 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 17 Nov 2021 21:15:35 -0600 Subject: [PATCH 43/76] Guard against tracks_len being zero to prevent 'index out of bounds: the len is 0 but the index is 0' https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 --- connect/src/spirc.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index d644e2b0c..344f63b75 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -1140,6 +1140,14 @@ impl SpircTask { fn get_track_id_to_play_from_playlist(&self, index: u32) -> Option<(SpotifyId, u32)> { let tracks_len = self.state.get_track().len(); + // Guard against tracks_len being zero to prevent + // 'index out of bounds: the len is 0 but the index is 0' + // https://github.com/librespot-org/librespot/issues/226#issuecomment-971642037 + if tracks_len == 0 { + warn!("No playable track found in state: {:?}", self.state); + return None; + } + let mut new_playlist_index = index as usize; if new_playlist_index >= tracks_len { From c006a2364452a83584afc7b50e9714c4c71d24c7 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 19 Nov 2021 16:45:13 -0600 Subject: [PATCH 44/76] Improve `--device ?` functionality for the alsa backend This makes `--device ?` only show compatible devices (ones that support 2 ch 44.1 Interleaved) and it shows what `librespot` format(s) they support. This should be more useful to users as the info maps directly to `librespot`'s `--device` and `--format` options. --- CHANGELOG.md | 1 + playback/src/audio_backend/alsa.rs | 92 +++++++++++++++++++++--------- 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c480e03f8..fb800c00b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - [main] Enforce reasonable ranges for option values (breaking). - [main] Don't evaluate options that would otherwise have no effect. +- [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 9dd3ea0c5..e572f9538 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -80,6 +80,23 @@ impl From for SinkError { } } +impl From for Format { + fn from(f: AudioFormat) -> Format { + use AudioFormat::*; + match f { + F64 => Format::float64(), + F32 => Format::float(), + S32 => Format::s32(), + S24 => Format::s24(), + S16 => Format::s16(), + #[cfg(target_endian = "little")] + S24_3 => Format::S243LE, + #[cfg(target_endian = "big")] + S24_3 => Format::S243BE, + } + } +} + pub struct AlsaSink { pcm: Option, format: AudioFormat, @@ -87,20 +104,50 @@ pub struct AlsaSink { period_buffer: Vec, } -fn list_outputs() -> SinkResult<()> { - println!("Listing available Alsa outputs:"); - for t in &["pcm", "ctl", "hwdep"] { - println!("{} devices:", t); - - let i = HintIter::new_str(None, t).map_err(|_| AlsaError::Parsing)?; - - for a in i { - if let Some(Direction::Playback) = a.direction { - // mimic aplay -L - let name = a.name.ok_or(AlsaError::Parsing)?; - let desc = a.desc.ok_or(AlsaError::Parsing)?; - - println!("{}\n\t{}\n", name, desc.replace("\n", "\n\t")); +fn list_compatible_devices() -> SinkResult<()> { + println!("\n\n\tCompatible alsa device(s):\n"); + println!("\t------------------------------------------------------\n"); + + let i = HintIter::new_str(None, "pcm").map_err(|_| AlsaError::Parsing)?; + + for a in i { + if let Some(Direction::Playback) = a.direction { + let name = a.name.ok_or(AlsaError::Parsing)?; + let desc = a.desc.ok_or(AlsaError::Parsing)?; + + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + // Only show devices that support + // 2 ch 44.1 Interleaved. + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + println!("\tDevice:\n\n\t\t{}\n", name); + println!("\tDescription:\n\n\t\t{}\n", desc.replace("\n", "\n\t\t")); + + let mut supported_formats = vec![]; + + for f in &[ + AudioFormat::S16, + AudioFormat::S24, + AudioFormat::S24_3, + AudioFormat::S32, + AudioFormat::F32, + AudioFormat::F64, + ] { + if hwp.test_format(Format::from(*f)).is_ok() { + supported_formats.push(format!("{:?}", f)); + } + } + + println!( + "\tSupported Format(s):\n\n\t\t{}\n", + supported_formats.join(" ") + ); + println!("\t------------------------------------------------------\n"); + } + }; } } } @@ -114,19 +161,6 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - let alsa_format = match format { - AudioFormat::F64 => Format::float64(), - AudioFormat::F32 => Format::float(), - AudioFormat::S32 => Format::s32(), - AudioFormat::S24 => Format::s24(), - AudioFormat::S16 => Format::s16(), - - #[cfg(target_endian = "little")] - AudioFormat::S24_3 => Format::S243LE, - #[cfg(target_endian = "big")] - AudioFormat::S24_3 => Format::S243BE, - }; - let bytes_per_period = { let hwp = HwParams::any(&pcm).map_err(AlsaError::HwParams)?; @@ -136,6 +170,8 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; + let alsa_format = Format::from(format); + hwp.set_format(alsa_format) .map_err(|e| AlsaError::UnsupportedFormat { device: dev_name.to_string(), @@ -194,7 +230,7 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> impl Open for AlsaSink { fn open(device: Option, format: AudioFormat) -> Self { let name = match device.as_deref() { - Some("?") => match list_outputs() { + Some("?") => match list_compatible_devices() { Ok(_) => { exit(0); } From bbd575ed23cf9e27a1b43007875568fba8458694 Mon Sep 17 00:00:00 2001 From: Tom Vincent Date: Fri, 26 Nov 2021 18:49:50 +0000 Subject: [PATCH 45/76] Harden systemd service, update restart policy (#888) --- CHANGELOG.md | 1 + contrib/librespot.service | 8 ++++---- contrib/librespot.user.service | 2 ++ 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb800c00b..7ffd99cf3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Enforce reasonable ranges for option values (breaking). - [main] Don't evaluate options that would otherwise have no effect. - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. +- [contrib] Hardened security of the systemd service units ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/contrib/librespot.service b/contrib/librespot.service index 76037c8c9..2c92a1496 100644 --- a/contrib/librespot.service +++ b/contrib/librespot.service @@ -2,12 +2,12 @@ Description=Librespot (an open source Spotify client) Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot/wiki/Options -Requires=network-online.target -After=network-online.target +Wants=network.target sound.target +After=network.target sound.target [Service] -User=nobody -Group=audio +DynamicUser=yes +SupplementaryGroups=audio Restart=always RestartSec=10 ExecStart=/usr/bin/librespot --name "%p@%H" diff --git a/contrib/librespot.user.service b/contrib/librespot.user.service index a676dde08..36f7f8c9c 100644 --- a/contrib/librespot.user.service +++ b/contrib/librespot.user.service @@ -2,6 +2,8 @@ Description=Librespot (an open source Spotify client) Documentation=https://github.com/librespot-org/librespot Documentation=https://github.com/librespot-org/librespot/wiki/Options +Wants=network.target sound.target +After=network.target sound.target [Service] Restart=always From e66cc5508cee0413829aa347c7a31bd0293eb856 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Wed, 1 Dec 2021 14:29:58 -0600 Subject: [PATCH 46/76] parse environment variables (#886) Make librespot able to parse environment variables for options and flags. To avoid name collisions environment variables must be prepended with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. Verbose logging mode (`-v`, `--verbose`) logs all parsed environment variables and command line arguments (credentials are redacted). --- CHANGELOG.md | 2 + src/main.rs | 206 +++++++++++++++++++++++++++++++++------------------ 2 files changed, 134 insertions(+), 74 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ffd99cf3..c5757aaf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Don't evaluate options that would otherwise have no effect. - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. - [contrib] Hardened security of the systemd service units +- [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). ### Added - [cache] Add `disable-credential-cache` flag (breaking). - [main] Use different option descriptions and error messages based on what backends are enabled at build time. - [main] Add a `-q`, `--quiet` option that changes the logging level to warn. - [main] Add a short name for every flag and option. +- [main] Add the ability to parse environment variables. ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. diff --git a/src/main.rs b/src/main.rs index 990de629b..2dec56aef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,6 @@ use futures_util::{future, FutureExt, StreamExt}; use librespot_playback::player::PlayerEvent; -use log::{error, info, warn}; +use log::{error, info, trace, warn}; use sha1::{Digest, Sha1}; use thiserror::Error; use tokio::sync::mpsc::UnboundedReceiver; @@ -44,6 +44,23 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } +fn arg_to_var(arg: &str) -> String { + // To avoid name collisions environment variables must be prepended + // with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. + format!("LIBRESPOT_{}", arg.to_uppercase().replace("-", "_")) +} + +fn env_var_present(arg: &str) -> bool { + env::var(arg_to_var(arg)).is_ok() +} + +fn env_var_opt_str(option: &str) -> Option { + match env::var(arg_to_var(option)) { + Ok(value) => Some(value), + Err(_) => None, + } +} + fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { @@ -591,20 +608,84 @@ fn get_setup(args: &[String]) -> Setup { } }; - if matches.opt_present(HELP) { + let opt_present = |opt| matches.opt_present(opt) || env_var_present(opt); + + let opt_str = |opt| { + if matches.opt_present(opt) { + matches.opt_str(opt) + } else { + env_var_opt_str(opt) + } + }; + + if opt_present(HELP) { println!("{}", usage(&args[0], &opts)); exit(0); } - if matches.opt_present(VERSION) { + if opt_present(VERSION) { println!("{}", get_version_string()); exit(0); } - setup_logging(matches.opt_present(QUIET), matches.opt_present(VERBOSE)); + setup_logging(opt_present(QUIET), opt_present(VERBOSE)); info!("{}", get_version_string()); + let librespot_env_vars: Vec = env::vars_os() + .filter_map(|(k, v)| { + let mut env_var = None; + if let Some(key) = k.to_str() { + if key.starts_with("LIBRESPOT_") { + if matches!(key, "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + // Don't log creds. + env_var = Some(format!("\t\t{}=XXXXXXXX", key)); + } else if let Some(value) = v.to_str() { + env_var = Some(format!("\t\t{}={}", key, value)); + } + } + } + + env_var + }) + .collect(); + + if !librespot_env_vars.is_empty() { + trace!("Environment variable(s):"); + + for kv in librespot_env_vars { + trace!("{}", kv); + } + } + + let cmd_args = &args[1..]; + + let cmd_args_len = cmd_args.len(); + + if cmd_args_len > 0 { + trace!("Command line argument(s):"); + + for (index, key) in cmd_args.iter().enumerate() { + if key.starts_with('-') || key.starts_with("--") { + if matches!(key.as_str(), "--password" | "-p" | "--username" | "-u") { + // Don't log creds. + trace!("\t\t{} XXXXXXXX", key); + } else { + let mut value = "".to_string(); + let next = index + 1; + if next < cmd_args_len { + let next_key = cmd_args[next].clone(); + if !next_key.starts_with('-') && !next_key.starts_with("--") { + value = next_key; + } + } + + trace!("\t\t{} {}", key, value); + } + } + } + } + #[cfg(not(feature = "alsa-backend"))] for a in &[ MIXER_TYPE, @@ -612,13 +693,13 @@ fn get_setup(args: &[String]) -> Setup { ALSA_MIXER_INDEX, ALSA_MIXER_CONTROL, ] { - if matches.opt_present(a) { + if opt_present(a) { warn!("Alsa specific options have no effect if the alsa backend is not enabled at build time."); break; } } - let backend_name = matches.opt_str(BACKEND); + let backend_name = opt_str(BACKEND); if backend_name == Some("?".into()) { list_backends(); exit(0); @@ -629,14 +710,13 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", BACKEND, BACKEND_SHORT, - matches.opt_str(BACKEND).unwrap_or_default() + opt_str(BACKEND).unwrap_or_default() ); list_backends(); exit(1); }); - let format = matches - .opt_str(FORMAT) + let format = opt_str(FORMAT) .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { @@ -656,7 +736,7 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" ))] - let device = matches.opt_str(DEVICE); + let device = opt_str(DEVICE); #[cfg(any( feature = "alsa-backend", @@ -680,7 +760,7 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" )))] - if matches.opt_present(DEVICE) { + if opt_present(DEVICE) { warn!( "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", DEVICE, DEVICE_SHORT, @@ -688,7 +768,7 @@ fn get_setup(args: &[String]) -> Setup { } #[cfg(feature = "alsa-backend")] - let mixer_type = matches.opt_str(MIXER_TYPE); + let mixer_type = opt_str(MIXER_TYPE); #[cfg(not(feature = "alsa-backend"))] let mixer_type: Option = None; @@ -697,7 +777,7 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", MIXER_TYPE, MIXER_TYPE_SHORT, - matches.opt_str(MIXER_TYPE).unwrap_or_default() + opt_str(MIXER_TYPE).unwrap_or_default() ); println!( "Valid `--{}` / `-{}` values: alsa, softvol", @@ -711,7 +791,7 @@ fn get_setup(args: &[String]) -> Setup { let mixer_default_config = MixerConfig::default(); #[cfg(feature = "alsa-backend")] - let device = matches.opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { + let device = opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { if let Some(ref device_name) = device { device_name.to_string() } else { @@ -723,8 +803,7 @@ fn get_setup(args: &[String]) -> Setup { let device = mixer_default_config.device; #[cfg(feature = "alsa-backend")] - let index = matches - .opt_str(ALSA_MIXER_INDEX) + let index = opt_str(ALSA_MIXER_INDEX) .map(|index| { index.parse::().unwrap_or_else(|_| { error!( @@ -741,15 +820,12 @@ fn get_setup(args: &[String]) -> Setup { let index = mixer_default_config.index; #[cfg(feature = "alsa-backend")] - let control = matches - .opt_str(ALSA_MIXER_CONTROL) - .unwrap_or(mixer_default_config.control); + let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); #[cfg(not(feature = "alsa-backend"))] let control = mixer_default_config.control; - let volume_range = matches - .opt_str(VOLUME_RANGE) + let volume_range = opt_str(VOLUME_RANGE) .map(|range| { let on_error = || { error!( @@ -790,8 +866,7 @@ fn get_setup(args: &[String]) -> Setup { _ => VolumeCtrl::DEFAULT_DB_RANGE, }); - let volume_ctrl = matches - .opt_str(VOLUME_CTRL) + let volume_ctrl = opt_str(VOLUME_CTRL) .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { @@ -818,29 +893,26 @@ fn get_setup(args: &[String]) -> Setup { }; let cache = { - let volume_dir = matches - .opt_str(SYSTEM_CACHE) - .or_else(|| matches.opt_str(CACHE)) + let volume_dir = opt_str(SYSTEM_CACHE) + .or_else(|| opt_str(CACHE)) .map(|p| p.into()); - let cred_dir = if matches.opt_present(DISABLE_CREDENTIAL_CACHE) { + let cred_dir = if opt_present(DISABLE_CREDENTIAL_CACHE) { None } else { volume_dir.clone() }; - let audio_dir = if matches.opt_present(DISABLE_AUDIO_CACHE) { + let audio_dir = if opt_present(DISABLE_AUDIO_CACHE) { None } else { - matches - .opt_str(CACHE) + opt_str(CACHE) .as_ref() .map(|p| AsRef::::as_ref(p).join("files")) }; let limit = if audio_dir.is_some() { - matches - .opt_str(CACHE_SIZE_LIMIT) + opt_str(CACHE_SIZE_LIMIT) .as_deref() .map(parse_file_size) .map(|e| { @@ -856,7 +928,7 @@ fn get_setup(args: &[String]) -> Setup { None }; - if audio_dir.is_none() && matches.opt_present(CACHE_SIZE_LIMIT) { + if audio_dir.is_none() && opt_present(CACHE_SIZE_LIMIT) { warn!( "Without a `--{}` / `-{}` path, and/or if the `--{}` / `-{}` flag is set, `--{}` / `-{}` has no effect.", CACHE, CACHE_SHORT, DISABLE_AUDIO_CACHE, DISABLE_AUDIO_CACHE_SHORT, CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT @@ -882,21 +954,21 @@ fn get_setup(args: &[String]) -> Setup { }; get_credentials( - matches.opt_str(USERNAME), - matches.opt_str(PASSWORD), + opt_str(USERNAME), + opt_str(PASSWORD), cached_credentials, password, ) }; - let enable_discovery = !matches.opt_present(DISABLE_DISCOVERY); + let enable_discovery = !opt_present(DISABLE_DISCOVERY); if credentials.is_none() && !enable_discovery { error!("Credentials are required if discovery is disabled."); exit(1); } - if !enable_discovery && matches.opt_present(ZEROCONF_PORT) { + if !enable_discovery && opt_present(ZEROCONF_PORT) { warn!( "With the `--{}` / `-{}` flag set `--{}` / `-{}` has no effect.", DISABLE_DISCOVERY, DISABLE_DISCOVERY_SHORT, ZEROCONF_PORT, ZEROCONF_PORT_SHORT @@ -904,8 +976,7 @@ fn get_setup(args: &[String]) -> Setup { } let zeroconf_port = if enable_discovery { - matches - .opt_str(ZEROCONF_PORT) + opt_str(ZEROCONF_PORT) .map(|port| { let on_error = || { error!( @@ -938,12 +1009,9 @@ fn get_setup(args: &[String]) -> Setup { let connect_config = { let connect_default_config = ConnectConfig::default(); - let name = matches - .opt_str(NAME) - .unwrap_or_else(|| connect_default_config.name.clone()); + let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); - let initial_volume = matches - .opt_str(INITIAL_VOLUME) + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { error!( @@ -984,8 +1052,7 @@ fn get_setup(args: &[String]) -> Setup { _ => cache.as_ref().and_then(Cache::volume), }); - let device_type = matches - .opt_str(DEVICE_TYPE) + let device_type = opt_str(DEVICE_TYPE) .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { @@ -1001,7 +1068,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or_default(); let has_volume_ctrl = !matches!(mixer_config.volume_ctrl, VolumeCtrl::Fixed); - let autoplay = matches.opt_present(AUTOPLAY); + let autoplay = opt_present(AUTOPLAY); ConnectConfig { name, @@ -1018,7 +1085,7 @@ fn get_setup(args: &[String]) -> Setup { SessionConfig { user_agent: version::VERSION_STRING.to_string(), device_id, - proxy: matches.opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( + proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( |s| { match Url::parse(&s) { Ok(url) => { @@ -1041,8 +1108,7 @@ fn get_setup(args: &[String]) -> Setup { } }, ), - ap_port: matches - .opt_str(AP_PORT) + ap_port: opt_str(AP_PORT) .map(|port| { let on_error = || { error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); @@ -1067,8 +1133,7 @@ fn get_setup(args: &[String]) -> Setup { let player_config = { let player_default_config = PlayerConfig::default(); - let bitrate = matches - .opt_str(BITRATE) + let bitrate = opt_str(BITRATE) .as_deref() .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { @@ -1086,9 +1151,9 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.bitrate); - let gapless = !matches.opt_present(DISABLE_GAPLESS); + let gapless = !opt_present(DISABLE_GAPLESS); - let normalisation = matches.opt_present(ENABLE_VOLUME_NORMALISATION); + let normalisation = opt_present(ENABLE_VOLUME_NORMALISATION); let normalisation_method; let normalisation_type; @@ -1108,7 +1173,7 @@ fn get_setup(args: &[String]) -> Setup { NORMALISATION_RELEASE, NORMALISATION_KNEE, ] { - if matches.opt_present(a) { + if opt_present(a) { warn!( "Without the `--{}` / `-{}` flag normalisation options have no effect.", ENABLE_VOLUME_NORMALISATION, ENABLE_VOLUME_NORMALISATION_SHORT, @@ -1125,8 +1190,7 @@ fn get_setup(args: &[String]) -> Setup { normalisation_release = player_default_config.normalisation_release; normalisation_knee = player_default_config.normalisation_knee; } else { - normalisation_method = matches - .opt_str(NORMALISATION_METHOD) + normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() .map(|method| { warn!( @@ -1158,8 +1222,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_method); - normalisation_type = matches - .opt_str(NORMALISATION_GAIN_TYPE) + normalisation_type = opt_str(NORMALISATION_GAIN_TYPE) .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { @@ -1177,8 +1240,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_type); - normalisation_pregain = matches - .opt_str(NORMALISATION_PREGAIN) + normalisation_pregain = opt_str(NORMALISATION_PREGAIN) .map(|pregain| { let on_error = || { error!( @@ -1209,8 +1271,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_pregain); - normalisation_threshold = matches - .opt_str(NORMALISATION_THRESHOLD) + normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { let on_error = || { error!( @@ -1244,8 +1305,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_threshold); - normalisation_attack = matches - .opt_str(NORMALISATION_ATTACK) + normalisation_attack = opt_str(NORMALISATION_ATTACK) .map(|attack| { let on_error = || { error!( @@ -1279,8 +1339,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_attack); - normalisation_release = matches - .opt_str(NORMALISATION_RELEASE) + normalisation_release = opt_str(NORMALISATION_RELEASE) .map(|release| { let on_error = || { error!( @@ -1314,8 +1373,7 @@ fn get_setup(args: &[String]) -> Setup { }) .unwrap_or(player_default_config.normalisation_release); - normalisation_knee = matches - .opt_str(NORMALISATION_KNEE) + normalisation_knee = opt_str(NORMALISATION_KNEE) .map(|knee| { let on_error = || { error!( @@ -1347,7 +1405,7 @@ fn get_setup(args: &[String]) -> Setup { .unwrap_or(player_default_config.normalisation_knee); } - let ditherer_name = matches.opt_str(DITHER); + let ditherer_name = opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { // explicitly disabled on command line Some("none") => None, @@ -1363,7 +1421,7 @@ fn get_setup(args: &[String]) -> Setup { "Invalid `--{}` / `-{}`: {}", DITHER, DITHER_SHORT, - matches.opt_str(DITHER).unwrap_or_default() + opt_str(DITHER).unwrap_or_default() ); println!( "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", @@ -1384,7 +1442,7 @@ fn get_setup(args: &[String]) -> Setup { }, }; - let passthrough = matches.opt_present(PASSTHROUGH); + let passthrough = opt_present(PASSTHROUGH); PlayerConfig { bitrate, @@ -1402,8 +1460,8 @@ fn get_setup(args: &[String]) -> Setup { } }; - let player_event_program = matches.opt_str(ONEVENT); - let emit_sink_events = matches.opt_present(EMIT_SINK_EVENTS); + let player_event_program = opt_str(ONEVENT); + let emit_sink_events = opt_present(EMIT_SINK_EVENTS); Setup { format, From dd4fd60c9abb1bc967b779456674fa98118f19d3 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Fri, 3 Dec 2021 07:06:51 +0100 Subject: [PATCH 47/76] Add workflow for all platforms --- .github/workflows/build-spotty.yml | 85 ++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/build-spotty.yml diff --git a/.github/workflows/build-spotty.yml b/.github/workflows/build-spotty.yml new file mode 100644 index 000000000..418316726 --- /dev/null +++ b/.github/workflows/build-spotty.yml @@ -0,0 +1,85 @@ +name: Build Spotty for all platforms + +on: [workflow_dispatch] + +env: + CARGO_TERM_COLOR: always + KEYMASTER_CLIENT_ID: ${{ secrets.KEYMASTER_CLIENT_ID }} + +jobs: + macOS: + runs-on: macos-10.15 + + steps: + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + + - name: Checkout + uses: actions/checkout@v2 + + - name: Write Build Configuration File + uses: DamianReeves/write-file-action@v1.0 + with: + path: ./src/client_id.txt + contents: ${{ env.KEYMASTER_CLIENT_ID }} + write-mode: overwrite + + - name: Build + run: cargo build --release && strip target/release/spotty + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: spotty-mac + path: target/release/spotty + + Linux: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v1 + + - name: Write Build Configuration File + uses: DamianReeves/write-file-action@v1.0 + with: + path: ./src/client_id.txt + contents: ${{ env.KEYMASTER_CLIENT_ID }} + write-mode: overwrite + + - name: Build ARMv7 + run: docker run --rm -v $(pwd):/source dlecan/rust-crosscompiler-arm:stable + + - name: Build other binaries (x86_64, i686, ARMv8) + run: ./xbuild.sh + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: spotty-linux + path: releases/ + + windows: + runs-on: windows-2019 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Write Build Configuration File + uses: DamianReeves/write-file-action@v1.0 + with: + path: ./src/client_id.txt + contents: ${{ env.KEYMASTER_CLIENT_ID }} + write-mode: overwrite + + - name: Build + run: cargo build --release + + - name: Upload artifacts + uses: actions/upload-artifact@v2 + with: + name: spotty.exe + path: target/release/spotty.exe \ No newline at end of file From 4370258716e3e3303b9242cda4ec894c80c0c31e Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Fri, 3 Dec 2021 11:47:51 -0600 Subject: [PATCH 48/76] Address clippy lint warnings for rust 1.57 --- connect/src/context.rs | 2 ++ core/src/connection/codec.rs | 3 +-- playback/src/audio_backend/jackaudio.rs | 9 +++------ playback/src/audio_backend/mod.rs | 2 +- playback/src/audio_backend/rodio.rs | 1 + playback/src/mixer/alsamixer.rs | 1 + 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/connect/src/context.rs b/connect/src/context.rs index 63a2aebb4..154d95075 100644 --- a/connect/src/context.rs +++ b/connect/src/context.rs @@ -46,6 +46,7 @@ pub struct TrackContext { // pub metadata: MetadataContext, } +#[allow(dead_code)] #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ArtistContext { @@ -54,6 +55,7 @@ pub struct ArtistContext { image_uri: String, } +#[allow(dead_code)] #[derive(Deserialize, Debug)] pub struct MetadataContext { album_title: String, diff --git a/core/src/connection/codec.rs b/core/src/connection/codec.rs index 299220f64..86533aafd 100644 --- a/core/src/connection/codec.rs +++ b/core/src/connection/codec.rs @@ -87,8 +87,7 @@ impl Decoder for ApCodec { let mut payload = buf.split_to(size + MAC_SIZE); - self.decode_cipher - .decrypt(&mut payload.get_mut(..size).unwrap()); + self.decode_cipher.decrypt(payload.get_mut(..size).unwrap()); let mac = payload.split_off(size); self.decode_cipher.check_mac(mac.as_ref())?; diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 5ba7b7ff2..15acf99d2 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -24,15 +24,12 @@ pub struct JackData { impl ProcessHandler for JackData { fn process(&mut self, _: &Client, ps: &ProcessScope) -> Control { // get output port buffers - let mut out_r = self.port_r.as_mut_slice(ps); - let mut out_l = self.port_l.as_mut_slice(ps); - let buf_r: &mut [f32] = &mut out_r; - let buf_l: &mut [f32] = &mut out_l; + let buf_r: &mut [f32] = self.port_r.as_mut_slice(ps); + let buf_l: &mut [f32] = self.port_l.as_mut_slice(ps); // get queue iterator let mut queue_iter = self.rec.try_iter(); - let buf_size = buf_r.len(); - for i in 0..buf_size { + for i in 0..buf_r.len() { buf_r[i] = queue_iter.next().unwrap_or(0.0); buf_l[i] = queue_iter.next().unwrap_or(0.0); } diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index 4d3b01714..dc21fb3db 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -104,7 +104,7 @@ use self::gstreamer::GstreamerSink; #[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] mod rodio; -#[cfg(any(feature = "rodio-backend", feature = "rodiojack-backend"))] +#[cfg(feature = "rodio-backend")] use self::rodio::RodioSink; #[cfg(feature = "sdl-backend")] diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 200c9fc44..ab356d67d 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -227,5 +227,6 @@ impl Sink for RodioSink { } impl RodioSink { + #[allow(dead_code)] pub const NAME: &'static str = "rodio"; } diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 81d0436f4..55398cb75 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -10,6 +10,7 @@ use alsa::{Ctl, Round}; use std::ffi::CString; #[derive(Clone)] +#[allow(dead_code)] pub struct AlsaMixer { config: MixerConfig, min: i64, From 8c480b7e39e911ddbfc4d564932d09dbdd7b6af6 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sun, 5 Dec 2021 13:00:33 -0600 Subject: [PATCH 49/76] Fix Command line arguments incorrectly echoed in TRACE Fix up for #886 Closes: #898 And... * Don't silently ignore non-Unicode while parsing env vars. * Iterating over `std::env::args` will panic! on invalid unicode. Let's not do that. `getopts` will catch missing args and exit if those args are required after our error message about the arg not being valid unicode. * Gaurd against empty strings. There are a few places while parsing options strings that we don't immediately evaluate their validity let's at least makes sure that they are not empty if present. * `args` is only used in `get_setup` it doesn't need to be in main. * Nicer help header. * Get rid of `use std::io::{stderr, Write};` and just use `rpassword::prompt_password_stderr`. * Get rid of `get_credentials` it was clunky, ugly and only used once. There is no need for it to be a separate function. * Handle an empty password prompt and password prompt parsing errors. * + Other random misc clean ups. --- src/main.rs | 413 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 239 insertions(+), 174 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2dec56aef..789654ff9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -26,7 +26,6 @@ mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; use std::env; -use std::io::{stderr, Write}; use std::ops::RangeInclusive; use std::path::Path; use std::pin::Pin; @@ -40,27 +39,16 @@ fn device_id(name: &str) -> String { } fn usage(program: &str, opts: &getopts::Options) -> String { - let brief = format!("Usage: {} [options]", program); + let repo_home = env!("CARGO_PKG_REPOSITORY"); + let desc = env!("CARGO_PKG_DESCRIPTION"); + let version = get_version_string(); + let brief = format!( + "{}\n\n{}\n\n{}\n\nUsage: {} []", + version, desc, repo_home, program + ); opts.usage(&brief) } -fn arg_to_var(arg: &str) -> String { - // To avoid name collisions environment variables must be prepended - // with `LIBRESPOT_` so option/flag `foo-bar` becomes `LIBRESPOT_FOO_BAR`. - format!("LIBRESPOT_{}", arg.to_uppercase().replace("-", "_")) -} - -fn env_var_present(arg: &str) -> bool { - env::var(arg_to_var(arg)).is_ok() -} - -fn env_var_opt_str(option: &str) -> Option { - match env::var(arg_to_var(option)) { - Ok(value) => Some(value), - Err(_) => None, - } -} - fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { @@ -102,29 +90,6 @@ fn list_backends() { } } -pub fn get_credentials Option>( - username: Option, - password: Option, - cached_credentials: Option, - prompt: F, -) -> Option { - if let Some(username) = username { - if let Some(password) = password { - return Some(Credentials::with_password(username, password)); - } - - match cached_credentials { - Some(credentials) if username == credentials.username => Some(credentials), - _ => { - let password = prompt(&username)?; - Some(Credentials::with_password(username, password)) - } - } - } else { - cached_credentials - } -} - #[derive(Debug, Error)] pub enum ParseFileSizeError { #[error("empty argument")] @@ -218,9 +183,10 @@ struct Setup { emit_sink_events: bool, } -fn get_setup(args: &[String]) -> Setup { - const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; +fn get_setup() -> Setup { + const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; @@ -596,25 +562,72 @@ fn get_setup(args: &[String]) -> Setup { "PORT", ); + let args: Vec<_> = std::env::args_os() + .filter_map(|s| match s.into_string() { + Ok(valid) => Some(valid), + Err(s) => { + eprintln!( + "Command line argument was not valid Unicode and will not be evaluated: {:?}", + s + ); + None + } + }) + .collect(); + let matches = match opts.parse(&args[1..]) { Ok(m) => m, - Err(f) => { - eprintln!( - "Error parsing command line options: {}\n{}", - f, - usage(&args[0], &opts) - ); + Err(e) => { + eprintln!("Error parsing command line options: {}", e); + println!("\n{}", usage(&args[0], &opts)); exit(1); } }; - let opt_present = |opt| matches.opt_present(opt) || env_var_present(opt); + let stripped_env_key = |k: &str| { + k.trim_start_matches("LIBRESPOT_") + .replace("_", "-") + .to_lowercase() + }; + + let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| { + let mut env_var = None; + if let Ok(key) = k.into_string() { + if key.starts_with("LIBRESPOT_") { + let stripped_key = stripped_env_key(&key); + // Only match against long option/flag names. + // Something like LIBRESPOT_V for example is + // not valid because there are both -v and -V flags + // but env vars are assumed to be all uppercase. + let len = stripped_key.chars().count(); + if len > 1 && matches.opt_defined(&stripped_key) { + match v.into_string() { + Ok(value) => { + env_var = Some((key, value)); + }, + Err(s) => { + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + } + } + } + } + } + + env_var + }) + .collect(); + + let opt_present = + |opt| matches.opt_present(opt) || env_vars.iter().any(|(k, _)| stripped_env_key(k) == opt); let opt_str = |opt| { if matches.opt_present(opt) { matches.opt_str(opt) } else { - env_var_opt_str(opt) + env_vars + .iter() + .find(|(k, _)| stripped_env_key(k) == opt) + .map(|(_, v)| v.to_string()) } }; @@ -632,55 +645,43 @@ fn get_setup(args: &[String]) -> Setup { info!("{}", get_version_string()); - let librespot_env_vars: Vec = env::vars_os() - .filter_map(|(k, v)| { - let mut env_var = None; - if let Some(key) = k.to_str() { - if key.starts_with("LIBRESPOT_") { - if matches!(key, "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { - // Don't log creds. - env_var = Some(format!("\t\t{}=XXXXXXXX", key)); - } else if let Some(value) = v.to_str() { - env_var = Some(format!("\t\t{}={}", key, value)); - } - } - } - - env_var - }) - .collect(); - - if !librespot_env_vars.is_empty() { + if !env_vars.is_empty() { trace!("Environment variable(s):"); - for kv in librespot_env_vars { - trace!("{}", kv); + for (k, v) in &env_vars { + if matches!(k.as_str(), "LIBRESPOT_PASSWORD" | "LIBRESPOT_USERNAME") { + trace!("\t\t{}=\"XXXXXXXX\"", k); + } else if v.is_empty() { + trace!("\t\t{}=", k); + } else { + trace!("\t\t{}=\"{}\"", k, v); + } } } - let cmd_args = &args[1..]; - - let cmd_args_len = cmd_args.len(); + let args_len = args.len(); - if cmd_args_len > 0 { + if args_len > 1 { trace!("Command line argument(s):"); - for (index, key) in cmd_args.iter().enumerate() { - if key.starts_with('-') || key.starts_with("--") { - if matches!(key.as_str(), "--password" | "-p" | "--username" | "-u") { + for (index, key) in args.iter().enumerate() { + let opt = key.trim_start_matches('-'); + + if index > 0 + && &args[index - 1] != key + && matches.opt_defined(opt) + && matches.opt_present(opt) + { + if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) { // Don't log creds. - trace!("\t\t{} XXXXXXXX", key); + trace!("\t\t{} \"XXXXXXXX\"", key); } else { - let mut value = "".to_string(); - let next = index + 1; - if next < cmd_args_len { - let next_key = cmd_args[next].clone(); - if !next_key.starts_with('-') && !next_key.starts_with("--") { - value = next_key; - } + let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string()); + if value.is_empty() { + trace!("\t\t{}", key); + } else { + trace!("\t\t{} \"{}\"", key, value); } - - trace!("\t\t{} {}", key, value); } } } @@ -707,7 +708,7 @@ fn get_setup(args: &[String]) -> Setup { let backend = audio_backend::find(backend_name).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", BACKEND, BACKEND_SHORT, opt_str(BACKEND).unwrap_or_default() @@ -720,7 +721,10 @@ fn get_setup(args: &[String]) -> Setup { .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: {}", FORMAT, FORMAT_SHORT, format); + error!( + "Invalid `--{}` / `-{}`: \"{}\"", + FORMAT, FORMAT_SHORT, format + ); println!( "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", FORMAT, FORMAT_SHORT @@ -743,9 +747,17 @@ fn get_setup(args: &[String]) -> Setup { feature = "rodio-backend", feature = "portaudio-backend" ))] - if device == Some("?".into()) { - backend(device, format); - exit(0); + if let Some(ref value) = device { + if value == "?" { + backend(device, format); + exit(0); + } else if value.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + DEVICE, DEVICE_SHORT + ); + exit(1); + } } #[cfg(not(any( @@ -774,7 +786,7 @@ fn get_setup(args: &[String]) -> Setup { let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", MIXER_TYPE, MIXER_TYPE_SHORT, opt_str(MIXER_TYPE).unwrap_or_default() @@ -807,7 +819,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|index| { index.parse::().unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index ); println!("Default: {}", mixer_default_config.index); @@ -822,6 +834,15 @@ fn get_setup(args: &[String]) -> Setup { #[cfg(feature = "alsa-backend")] let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); + #[cfg(feature = "alsa-backend")] + if control.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT + ); + exit(1); + } + #[cfg(not(feature = "alsa-backend"))] let control = mixer_default_config.control; @@ -829,7 +850,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|range| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", VOLUME_RANGE, VOLUME_RANGE_SHORT, range ); println!( @@ -871,7 +892,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl ); println!( @@ -918,7 +939,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|e| { e.unwrap_or_else(|e| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e ); exit(1); @@ -945,20 +966,57 @@ fn get_setup(args: &[String]) -> Setup { }; let credentials = { - let cached_credentials = cache.as_ref().and_then(Cache::credentials); + let cached_creds = cache.as_ref().and_then(Cache::credentials); - let password = |username: &String| -> Option { - write!(stderr(), "Password for {}: ", username).ok()?; - stderr().flush().ok()?; - rpassword::read_password().ok() - }; - - get_credentials( - opt_str(USERNAME), - opt_str(PASSWORD), - cached_credentials, - password, - ) + if let Some(username) = opt_str(USERNAME) { + if username.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + USERNAME, USERNAME_SHORT + ); + exit(1); + } + if let Some(password) = opt_str(PASSWORD) { + if password.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + PASSWORD, PASSWORD_SHORT + ); + exit(1); + } + Some(Credentials::with_password(username, password)) + } else { + match cached_creds { + Some(creds) if username == creds.username => Some(creds), + _ => { + let prompt = &format!("Password for {}: ", username); + + match rpassword::prompt_password_stderr(prompt) { + Ok(password) => { + if !password.is_empty() { + Some(Credentials::with_password(username, password)) + } else { + trace!("Password was empty."); + if cached_creds.is_some() { + trace!("Using cached credentials."); + } + cached_creds + } + } + Err(e) => { + warn!("Cannot parse password: {}", e); + if cached_creds.is_some() { + trace!("Using cached credentials."); + } + cached_creds + } + } + } + } + } + } else { + cached_creds + } }; let enable_discovery = !opt_present(DISABLE_DISCOVERY); @@ -980,12 +1038,14 @@ fn get_setup(args: &[String]) -> Setup { .map(|port| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port ); println!( - "Valid `--{}` / `-{}` values: 1 - 65535", - ZEROCONF_PORT, ZEROCONF_PORT_SHORT + "Valid `--{}` / `-{}` values: 1 - {}", + ZEROCONF_PORT, + ZEROCONF_PORT_SHORT, + u16::MAX ); }; @@ -1011,16 +1071,27 @@ fn get_setup(args: &[String]) -> Setup { let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); + if name.is_empty() { + error!( + "`--{}` / `-{}` can not be an empty string", + NAME, NAME_SHORT + ); + exit(1); + } + let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume ); println!( - "Valid `--{}` / `-{}` values: 0 - 100", - INITIAL_VOLUME, INITIAL_VOLUME_SHORT + "Valid `--{}` / `-{}` values: {} - {}", + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + VALID_INITIAL_VOLUME_RANGE.start(), + VALID_INITIAL_VOLUME_RANGE.end() ); #[cfg(feature = "alsa-backend")] println!( @@ -1039,7 +1110,7 @@ fn get_setup(args: &[String]) -> Setup { exit(1); }); - if volume > 100 { + if !(VALID_INITIAL_VOLUME_RANGE).contains(&volume) { on_error(); exit(1); } @@ -1056,7 +1127,7 @@ fn get_setup(args: &[String]) -> Setup { .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: {}", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); + error!("Invalid `--{}` / `-{}`: \"{}\"", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", DEVICE_TYPE, DEVICE_TYPE_SHORT @@ -1079,55 +1150,50 @@ fn get_setup(args: &[String]) -> Setup { } }; - let session_config = { - let device_id = device_id(&connect_config.name); - - SessionConfig { - user_agent: version::VERSION_STRING.to_string(), - device_id, - proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( - |s| { - match Url::parse(&s) { - Ok(url) => { - if url.host().is_none() || url.port_or_known_default().is_none() { - error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); - exit(1); - } - - if url.scheme() != "http" { - error!("Only unsecure http:// proxies are supported"); - exit(1); - } - - url - }, - Err(e) => { - error!("Invalid proxy URL: {}, only URLs in the format \"http://host:port\" are allowed", e); + let session_config = SessionConfig { + user_agent: version::VERSION_STRING.to_string(), + device_id: device_id(&connect_config.name), + proxy: opt_str(PROXY).or_else(|| std::env::var("http_proxy").ok()).map( + |s| { + match Url::parse(&s) { + Ok(url) => { + if url.host().is_none() || url.port_or_known_default().is_none() { + error!("Invalid proxy url, only URLs on the format \"http://host:port\" are allowed"); exit(1); } - } - }, - ), - ap_port: opt_str(AP_PORT) - .map(|port| { - let on_error = || { - error!("Invalid `--{}` / `-{}`: {}", AP_PORT, AP_PORT_SHORT, port); - println!("Valid `--{}` / `-{}` values: 1 - 65535", AP_PORT, AP_PORT_SHORT); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); + if url.scheme() != "http" { + error!("Only unsecure http:// proxies are supported"); + exit(1); + } - if port == 0 { - on_error(); + url + }, + Err(e) => { + error!("Invalid proxy URL: \"{}\", only URLs in the format \"http://host:port\" are allowed", e); exit(1); } + } + }, + ), + ap_port: opt_str(AP_PORT).map(|port| { + let on_error = || { + error!("Invalid `--{}` / `-{}`: \"{}\"", AP_PORT, AP_PORT_SHORT, port); + println!("Valid `--{}` / `-{}` values: 1 - {}", AP_PORT, AP_PORT_SHORT, u16::MAX); + }; + + let port = port.parse::().unwrap_or_else(|_| { + on_error(); + exit(1); + }); - port - }), - } + if port == 0 { + on_error(); + exit(1); + } + + port + }), }; let player_config = { @@ -1138,7 +1204,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", BITRATE, BITRATE_SHORT, bitrate ); println!( @@ -1200,7 +1266,7 @@ fn get_setup(args: &[String]) -> Setup { let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method ); println!( @@ -1227,7 +1293,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type ); println!( @@ -1244,7 +1310,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|pregain| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain ); println!( @@ -1275,7 +1341,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|threshold| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold ); println!( @@ -1309,7 +1375,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|attack| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack ); println!( @@ -1343,7 +1409,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|release| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release ); println!( @@ -1377,7 +1443,7 @@ fn get_setup(args: &[String]) -> Setup { .map(|knee| { let on_error = || { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee ); println!( @@ -1418,7 +1484,7 @@ fn get_setup(args: &[String]) -> Setup { Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { error!( - "Invalid `--{}` / `-{}`: {}", + "Invalid `--{}` / `-{}`: \"{}\"", DITHER, DITHER_SHORT, opt_str(DITHER).unwrap_or_default() @@ -1488,8 +1554,7 @@ async fn main() { env::set_var(RUST_BACKTRACE, "full") } - let args: Vec = std::env::args().collect(); - let setup = get_setup(&args); + let setup = get_setup(); let mut last_credentials = None; let mut spirc: Option = None; From 79c4040a53f50f14f27c6d9e0fec9ad00101f638 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sat, 11 Dec 2021 21:32:34 +0100 Subject: [PATCH 50/76] Skip track on decoding error --- playback/src/player.rs | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a7ff916d2..a56130f38 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -737,7 +737,14 @@ impl PlayerTrackLoader { } }; - assert!(audio.duration >= 0); + if audio.duration < 0 { + error!( + "Track duration for <{}> cannot be {}", + spotify_id.to_uri(), + audio.duration + ); + return None; + } let duration_ms = audio.duration as u32; // (Most) podcasts seem to support only 96 bit Vorbis, so fall back to it @@ -945,7 +952,7 @@ impl Future for PlayerInternal { } Poll::Ready(Err(_)) => { warn!("Unable to load <{:?}>\nSkipping to next track", track_id); - assert!(self.state.is_loading()); + debug_assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1045,8 +1052,11 @@ impl Future for PlayerInternal { } } Err(e) => { - error!("PlayerInternal poll: {}", e); - exit(1); + warn!("Unable to decode samples for track <{:?}: {:?}>\nSkipping to next track", track_id, e); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) } } } @@ -1058,8 +1068,15 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - error!("PlayerInternal poll: {}", e); - exit(1); + warn!( + "Unable to get packet for track <{:?}: {:?}>\nSkipping to next track", + e, + track_id + ); + self.send_event(PlayerEvent::EndOfTrack { + track_id, + play_request_id, + }) } } } else { From 8f23c3498fd9eeaf7d74ab2cb57548f0e811de57 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Sun, 12 Dec 2021 20:01:05 +0100 Subject: [PATCH 51/76] Clean up warnings --- playback/src/player.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/playback/src/player.rs b/playback/src/player.rs index a56130f38..d8dbb190c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -950,8 +950,11 @@ impl Future for PlayerInternal { exit(1); } } - Poll::Ready(Err(_)) => { - warn!("Unable to load <{:?}>\nSkipping to next track", track_id); + Poll::Ready(Err(e)) => { + warn!( + "Skipping to next track, unable to load track <{:?}>: {:?}", + track_id, e + ); debug_assert!(self.state.is_loading()); self.send_event(PlayerEvent::EndOfTrack { track_id, @@ -1052,7 +1055,7 @@ impl Future for PlayerInternal { } } Err(e) => { - warn!("Unable to decode samples for track <{:?}: {:?}>\nSkipping to next track", track_id, e); + warn!("Skipping to next track, unable to decode samples for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, @@ -1068,11 +1071,7 @@ impl Future for PlayerInternal { self.handle_packet(packet, normalisation_factor); } Err(e) => { - warn!( - "Unable to get packet for track <{:?}: {:?}>\nSkipping to next track", - e, - track_id - ); + warn!("Skipping to next track, unable to get next packet for track <{:?}>: {:?}", track_id, e); self.send_event(PlayerEvent::EndOfTrack { track_id, play_request_id, From d29337c62df4e12e8d2120ab973b50ec6ab75a38 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sun, 12 Dec 2021 17:28:41 -0600 Subject: [PATCH 52/76] Dry up error messages. --- src/main.rs | 402 +++++++++++++++++++++++++--------------------------- 1 file changed, 197 insertions(+), 205 deletions(-) diff --git a/src/main.rs b/src/main.rs index 789654ff9..7e13a3237 100644 --- a/src/main.rs +++ b/src/main.rs @@ -590,30 +590,23 @@ fn get_setup() -> Setup { .to_lowercase() }; - let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| { - let mut env_var = None; - if let Ok(key) = k.into_string() { - if key.starts_with("LIBRESPOT_") { - let stripped_key = stripped_env_key(&key); - // Only match against long option/flag names. - // Something like LIBRESPOT_V for example is - // not valid because there are both -v and -V flags - // but env vars are assumed to be all uppercase. - let len = stripped_key.chars().count(); - if len > 1 && matches.opt_defined(&stripped_key) { - match v.into_string() { - Ok(value) => { - env_var = Some((key, value)); - }, - Err(s) => { - eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); - } + let env_vars: Vec<_> = env::vars_os().filter_map(|(k, v)| match k.into_string() { + Ok(key) if key.starts_with("LIBRESPOT_") => { + let stripped_key = stripped_env_key(&key); + // We only care about long option/flag names. + if stripped_key.chars().count() > 1 && matches.opt_defined(&stripped_key) { + match v.into_string() { + Ok(value) => Some((key, value)), + Err(s) => { + eprintln!("Environment variable was not valid Unicode and will not be evaluated: {}={:?}", key, s); + None } } + } else { + None } - } - - env_var + }, + _ => None }) .collect(); @@ -706,13 +699,33 @@ fn get_setup() -> Setup { exit(0); } + let invalid_error_msg = + |long: &str, short: &str, invalid: &str, valid_values: &str, default_value: &str| { + error!("Invalid `--{}` / `-{}`: \"{}\"", long, short, invalid); + + if !valid_values.is_empty() { + println!("Valid `--{}` / `-{}` values: {}", long, short, valid_values); + } + + if !default_value.is_empty() { + println!("Default: {}", default_value); + } + }; + + let empty_string_error_msg = |long: &str, short: &str| { + error!("`--{}` / `-{}` can not be an empty string", long, short); + exit(1); + }; + let backend = audio_backend::find(backend_name).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( BACKEND, BACKEND_SHORT, - opt_str(BACKEND).unwrap_or_default() + &opt_str(BACKEND).unwrap_or_default(), + "", + "", ); + list_backends(); exit(1); }); @@ -721,15 +734,15 @@ fn get_setup() -> Setup { .as_deref() .map(|format| { AudioFormat::from_str(format).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - FORMAT, FORMAT_SHORT, format + let default_value = &format!("{:?}", AudioFormat::default()); + invalid_error_msg( + FORMAT, + FORMAT_SHORT, + format, + "F64, F32, S32, S24, S24_3, S16", + default_value, ); - println!( - "Valid `--{}` / `-{}` values: F64, F32, S32, S24, S24_3, S16", - FORMAT, FORMAT_SHORT - ); - println!("Default: {:?}", AudioFormat::default()); + exit(1); }) }) @@ -752,11 +765,7 @@ fn get_setup() -> Setup { backend(device, format); exit(0); } else if value.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - DEVICE, DEVICE_SHORT - ); - exit(1); + empty_string_error_msg(DEVICE, DEVICE_SHORT); } } @@ -785,17 +794,14 @@ fn get_setup() -> Setup { let mixer_type: Option = None; let mixer = mixer::find(mixer_type.as_deref()).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( MIXER_TYPE, MIXER_TYPE_SHORT, - opt_str(MIXER_TYPE).unwrap_or_default() - ); - println!( - "Valid `--{}` / `-{}` values: alsa, softvol", - MIXER_TYPE, MIXER_TYPE_SHORT + &opt_str(MIXER_TYPE).unwrap_or_default(), + "alsa, softvol", + "softvol", ); - println!("Default: softvol"); + exit(1); }); @@ -818,11 +824,14 @@ fn get_setup() -> Setup { let index = opt_str(ALSA_MIXER_INDEX) .map(|index| { index.parse::().unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - ALSA_MIXER_INDEX, ALSA_MIXER_INDEX_SHORT, index + invalid_error_msg( + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_SHORT, + &index, + "", + &mixer_default_config.index.to_string(), ); - println!("Default: {}", mixer_default_config.index); + exit(1); }) }) @@ -836,11 +845,7 @@ fn get_setup() -> Setup { #[cfg(feature = "alsa-backend")] if control.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT - ); - exit(1); + empty_string_error_msg(ALSA_MIXER_CONTROL, ALSA_MIXER_CONTROL_SHORT); } #[cfg(not(feature = "alsa-backend"))] @@ -849,24 +854,28 @@ fn get_setup() -> Setup { let volume_range = opt_str(VOLUME_RANGE) .map(|range| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - VOLUME_RANGE, VOLUME_RANGE_SHORT, range - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - VOLUME_RANGE, - VOLUME_RANGE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_VOLUME_RANGE.start(), VALID_VOLUME_RANGE.end() ); + #[cfg(feature = "alsa-backend")] - println!( - "Default: softvol - {}, alsa - what the control supports", + let default_value = &format!( + "softvol - {}, alsa - what the control supports", VolumeCtrl::DEFAULT_DB_RANGE ); + #[cfg(not(feature = "alsa-backend"))] - println!("Default: {}", VolumeCtrl::DEFAULT_DB_RANGE); + let default_value = &VolumeCtrl::DEFAULT_DB_RANGE.to_string(); + + invalid_error_msg( + VOLUME_RANGE, + VOLUME_RANGE_SHORT, + &range, + valid_values, + default_value, + ); }; let range = range.parse::().unwrap_or_else(|_| { @@ -891,15 +900,14 @@ fn get_setup() -> Setup { .as_deref() .map(|volume_ctrl| { VolumeCtrl::from_str_with_range(volume_ctrl, volume_range).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - VOLUME_CTRL, VOLUME_CTRL_SHORT, volume_ctrl + invalid_error_msg( + VOLUME_CTRL, + VOLUME_CTRL_SHORT, + volume_ctrl, + "cubic, fixed, linear, log", + "log", ); - println!( - "Valid `--{}` / `-{}` values: cubic, fixed, linear, log", - VOLUME_CTRL, VOLUME_CTRL - ); - println!("Default: log"); + exit(1); }) }) @@ -938,10 +946,14 @@ fn get_setup() -> Setup { .map(parse_file_size) .map(|e| { e.unwrap_or_else(|e| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - CACHE_SIZE_LIMIT, CACHE_SIZE_LIMIT_SHORT, e + invalid_error_msg( + CACHE_SIZE_LIMIT, + CACHE_SIZE_LIMIT_SHORT, + &e.to_string(), + "", + "", ); + exit(1); }) }) @@ -970,19 +982,11 @@ fn get_setup() -> Setup { if let Some(username) = opt_str(USERNAME) { if username.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - USERNAME, USERNAME_SHORT - ); - exit(1); + empty_string_error_msg(USERNAME, USERNAME_SHORT); } if let Some(password) = opt_str(PASSWORD) { if password.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - PASSWORD, PASSWORD_SHORT - ); - exit(1); + empty_string_error_msg(PASSWORD, PASSWORD_SHORT); } Some(Credentials::with_password(username, password)) } else { @@ -990,7 +994,6 @@ fn get_setup() -> Setup { Some(creds) if username == creds.username => Some(creds), _ => { let prompt = &format!("Password for {}: ", username); - match rpassword::prompt_password_stderr(prompt) { Ok(password) => { if !password.is_empty() { @@ -1015,6 +1018,9 @@ fn get_setup() -> Setup { } } } else { + if cached_creds.is_some() { + trace!("Using cached credentials."); + } cached_creds } }; @@ -1037,16 +1043,8 @@ fn get_setup() -> Setup { opt_str(ZEROCONF_PORT) .map(|port| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - ZEROCONF_PORT, ZEROCONF_PORT_SHORT, port - ); - println!( - "Valid `--{}` / `-{}` values: 1 - {}", - ZEROCONF_PORT, - ZEROCONF_PORT_SHORT, - u16::MAX - ); + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, ""); }; let port = port.parse::().unwrap_or_else(|_| { @@ -1072,36 +1070,37 @@ fn get_setup() -> Setup { let name = opt_str(NAME).unwrap_or_else(|| connect_default_config.name.clone()); if name.is_empty() { - error!( - "`--{}` / `-{}` can not be an empty string", - NAME, NAME_SHORT - ); + empty_string_error_msg(NAME, NAME_SHORT); exit(1); } let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - INITIAL_VOLUME, INITIAL_VOLUME_SHORT, initial_volume - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - INITIAL_VOLUME, - INITIAL_VOLUME_SHORT, + let valid_values = &format!( + "{} - {}", VALID_INITIAL_VOLUME_RANGE.start(), VALID_INITIAL_VOLUME_RANGE.end() ); + #[cfg(feature = "alsa-backend")] - println!( - "Default: {}, or the current value when the alsa mixer is used.", + let default_value = &format!( + "{}, or the current value when the alsa mixer is used.", connect_default_config.initial_volume.unwrap_or_default() ); + #[cfg(not(feature = "alsa-backend"))] - println!( - "Default: {}", - connect_default_config.initial_volume.unwrap_or_default() + let default_value = &connect_default_config + .initial_volume + .unwrap_or_default() + .to_string(); + + invalid_error_msg( + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + &initial_volume, + valid_values, + default_value, ); }; @@ -1127,12 +1126,18 @@ fn get_setup() -> Setup { .as_deref() .map(|device_type| { DeviceType::from_str(device_type).unwrap_or_else(|_| { - error!("Invalid `--{}` / `-{}`: \"{}\"", DEVICE_TYPE, DEVICE_TYPE_SHORT, device_type); - println!("Valid `--{}` / `-{}` values: computer, tablet, smartphone, speaker, tv, avr, stb, audiodongle, \ - gameconsole, castaudio, castvideo, automobile, smartwatch, chromebook, carthing, homething", - DEVICE_TYPE, DEVICE_TYPE_SHORT + invalid_error_msg( + DEVICE_TYPE, + DEVICE_TYPE_SHORT, + device_type, + "computer, tablet, smartphone, \ + speaker, tv, avr, stb, audiodongle, \ + gameconsole, castaudio, castvideo, \ + automobile, smartwatch, chromebook, \ + carthing, homething", + "speaker", ); - println!("Default: speaker"); + exit(1); }) }) @@ -1178,8 +1183,8 @@ fn get_setup() -> Setup { ), ap_port: opt_str(AP_PORT).map(|port| { let on_error = || { - error!("Invalid `--{}` / `-{}`: \"{}\"", AP_PORT, AP_PORT_SHORT, port); - println!("Valid `--{}` / `-{}` values: 1 - {}", AP_PORT, AP_PORT_SHORT, u16::MAX); + let valid_values = &format!("1 - {}", u16::MAX); + invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, ""); }; let port = port.parse::().unwrap_or_else(|_| { @@ -1203,15 +1208,7 @@ fn get_setup() -> Setup { .as_deref() .map(|bitrate| { Bitrate::from_str(bitrate).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - BITRATE, BITRATE_SHORT, bitrate - ); - println!( - "Valid `--{}` / `-{}` values: 96, 160, 320", - BITRATE, BITRATE_SHORT - ); - println!("Default: 160"); + invalid_error_msg(BITRATE, BITRATE_SHORT, bitrate, "96, 160, 320", "160"); exit(1); }) }) @@ -1265,15 +1262,14 @@ fn get_setup() -> Setup { ); let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method - ); - println!( - "Valid `--{}` / `-{}` values: basic, dynamic", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT + invalid_error_msg( + NORMALISATION_METHOD, + NORMALISATION_METHOD_SHORT, + method, + "basic, dynamic", + &format!("{:?}", player_default_config.normalisation_method), ); - println!("Default: {:?}", player_default_config.normalisation_method); + exit(1); }); @@ -1292,15 +1288,14 @@ fn get_setup() -> Setup { .as_deref() .map(|gain_type| { NormalisationType::from_str(gain_type).unwrap_or_else(|_| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, gain_type - ); - println!( - "Valid `--{}` / `-{}` values: track, album, auto", - NORMALISATION_GAIN_TYPE, NORMALISATION_GAIN_TYPE_SHORT, + invalid_error_msg( + NORMALISATION_GAIN_TYPE, + NORMALISATION_GAIN_TYPE_SHORT, + gain_type, + "track, album, auto", + &format!("{:?}", player_default_config.normalisation_type), ); - println!("Default: {:?}", player_default_config.normalisation_type); + exit(1); }) }) @@ -1309,18 +1304,19 @@ fn get_setup() -> Setup { normalisation_pregain = opt_str(NORMALISATION_PREGAIN) .map(|pregain| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, pregain + let valid_values = &format!( + "{} - {}", + VALID_NORMALISATION_PREGAIN_RANGE.start(), + VALID_NORMALISATION_PREGAIN_RANGE.end() ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", + + invalid_error_msg( NORMALISATION_PREGAIN, NORMALISATION_PREGAIN_SHORT, - VALID_NORMALISATION_PREGAIN_RANGE.start(), - VALID_NORMALISATION_PREGAIN_RANGE.end() + &pregain, + valid_values, + &player_default_config.normalisation_pregain.to_string(), ); - println!("Default: {}", player_default_config.normalisation_pregain); }; let pregain = pregain.parse::().unwrap_or_else(|_| { @@ -1340,20 +1336,18 @@ fn get_setup() -> Setup { normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_THRESHOLD, NORMALISATION_THRESHOLD_SHORT, threshold - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_THRESHOLD, - NORMALISATION_THRESHOLD_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_THRESHOLD_RANGE.start(), VALID_NORMALISATION_THRESHOLD_RANGE.end() ); - println!( - "Default: {}", - ratio_to_db(player_default_config.normalisation_threshold) + + invalid_error_msg( + NORMALISATION_THRESHOLD, + NORMALISATION_THRESHOLD_SHORT, + &threshold, + valid_values, + &ratio_to_db(player_default_config.normalisation_threshold).to_string(), ); }; @@ -1374,20 +1368,21 @@ fn get_setup() -> Setup { normalisation_attack = opt_str(NORMALISATION_ATTACK) .map(|attack| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_ATTACK, NORMALISATION_ATTACK_SHORT, attack - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_ATTACK, - NORMALISATION_ATTACK_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_ATTACK_RANGE.start(), VALID_NORMALISATION_ATTACK_RANGE.end() ); - println!( - "Default: {}", - player_default_config.normalisation_attack.as_millis() + + invalid_error_msg( + NORMALISATION_ATTACK, + NORMALISATION_ATTACK_SHORT, + &attack, + valid_values, + &player_default_config + .normalisation_attack + .as_millis() + .to_string(), ); }; @@ -1408,20 +1403,21 @@ fn get_setup() -> Setup { normalisation_release = opt_str(NORMALISATION_RELEASE) .map(|release| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_RELEASE, NORMALISATION_RELEASE_SHORT, release - ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", - NORMALISATION_RELEASE, - NORMALISATION_RELEASE_SHORT, + let valid_values = &format!( + "{} - {}", VALID_NORMALISATION_RELEASE_RANGE.start(), VALID_NORMALISATION_RELEASE_RANGE.end() ); - println!( - "Default: {}", - player_default_config.normalisation_release.as_millis() + + invalid_error_msg( + NORMALISATION_RELEASE, + NORMALISATION_RELEASE_SHORT, + &release, + valid_values, + &player_default_config + .normalisation_release + .as_millis() + .to_string(), ); }; @@ -1442,18 +1438,19 @@ fn get_setup() -> Setup { normalisation_knee = opt_str(NORMALISATION_KNEE) .map(|knee| { let on_error = || { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", - NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, knee + let valid_values = &format!( + "{} - {}", + VALID_NORMALISATION_KNEE_RANGE.start(), + VALID_NORMALISATION_KNEE_RANGE.end() ); - println!( - "Valid `--{}` / `-{}` values: {} - {}", + + invalid_error_msg( NORMALISATION_KNEE, NORMALISATION_KNEE_SHORT, - VALID_NORMALISATION_KNEE_RANGE.start(), - VALID_NORMALISATION_KNEE_RANGE.end() + &knee, + valid_values, + &player_default_config.normalisation_knee.to_string(), ); - println!("Default: {}", player_default_config.normalisation_knee); }; let knee = knee.parse::().unwrap_or_else(|_| { @@ -1483,19 +1480,14 @@ fn get_setup() -> Setup { } Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { - error!( - "Invalid `--{}` / `-{}`: \"{}\"", + invalid_error_msg( DITHER, DITHER_SHORT, - opt_str(DITHER).unwrap_or_default() - ); - println!( - "Valid `--{}` / `-{}` values: none, gpdf, tpdf, tpdf_hp", - DITHER, DITHER_SHORT - ); - println!( - "Default: tpdf for formats S16, S24, S24_3 and none for other formats" + &opt_str(DITHER).unwrap_or_default(), + "none, gpdf, tpdf, tpdf_hp", + "tpdf for formats S16, S24, S24_3 and none for other formats", ); + exit(1); })) } From 368bee10885d5d0e528e45328676e59857cd6896 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 13 Dec 2021 16:40:26 -0600 Subject: [PATCH 53/76] condense some option parsings --- src/main.rs | 229 ++++++++++++++++++---------------------------------- 1 file changed, 78 insertions(+), 151 deletions(-) diff --git a/src/main.rs b/src/main.rs index 7e13a3237..2ce526e1b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -852,8 +852,9 @@ fn get_setup() -> Setup { let control = mixer_default_config.control; let volume_range = opt_str(VOLUME_RANGE) - .map(|range| { - let on_error = || { + .map(|range| match range.parse::() { + Ok(value) if (VALID_VOLUME_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_VOLUME_RANGE.start(), @@ -876,19 +877,9 @@ fn get_setup() -> Setup { valid_values, default_value, ); - }; - - let range = range.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - if !(VALID_VOLUME_RANGE).contains(&range) { - on_error(); exit(1); } - - range }) .unwrap_or_else(|| match mixer_type.as_deref() { #[cfg(feature = "alsa-backend")] @@ -1041,23 +1032,14 @@ fn get_setup() -> Setup { let zeroconf_port = if enable_discovery { opt_str(ZEROCONF_PORT) - .map(|port| { - let on_error = || { + .map(|port| match port.parse::() { + Ok(value) if value != 0 => value, + _ => { let valid_values = &format!("1 - {}", u16::MAX); invalid_error_msg(ZEROCONF_PORT, ZEROCONF_PORT_SHORT, &port, valid_values, ""); - }; - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if port == 0 { - on_error(); exit(1); } - - port }) .unwrap_or(0) } else { @@ -1076,43 +1058,38 @@ fn get_setup() -> Setup { let initial_volume = opt_str(INITIAL_VOLUME) .map(|initial_volume| { - let on_error = || { - let valid_values = &format!( - "{} - {}", - VALID_INITIAL_VOLUME_RANGE.start(), - VALID_INITIAL_VOLUME_RANGE.end() - ); + let volume = match initial_volume.parse::() { + Ok(value) if (VALID_INITIAL_VOLUME_RANGE).contains(&value) => value, + _ => { + let valid_values = &format!( + "{} - {}", + VALID_INITIAL_VOLUME_RANGE.start(), + VALID_INITIAL_VOLUME_RANGE.end() + ); - #[cfg(feature = "alsa-backend")] - let default_value = &format!( - "{}, or the current value when the alsa mixer is used.", - connect_default_config.initial_volume.unwrap_or_default() - ); + #[cfg(feature = "alsa-backend")] + let default_value = &format!( + "{}, or the current value when the alsa mixer is used.", + connect_default_config.initial_volume.unwrap_or_default() + ); - #[cfg(not(feature = "alsa-backend"))] - let default_value = &connect_default_config - .initial_volume - .unwrap_or_default() - .to_string(); + #[cfg(not(feature = "alsa-backend"))] + let default_value = &connect_default_config + .initial_volume + .unwrap_or_default() + .to_string(); - invalid_error_msg( - INITIAL_VOLUME, - INITIAL_VOLUME_SHORT, - &initial_volume, - valid_values, - default_value, - ); - }; - - let volume = initial_volume.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); + invalid_error_msg( + INITIAL_VOLUME, + INITIAL_VOLUME_SHORT, + &initial_volume, + valid_values, + default_value, + ); - if !(VALID_INITIAL_VOLUME_RANGE).contains(&volume) { - on_error(); - exit(1); - } + exit(1); + } + }; (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) @@ -1135,7 +1112,7 @@ fn get_setup() -> Setup { gameconsole, castaudio, castvideo, \ automobile, smartwatch, chromebook, \ carthing, homething", - "speaker", + DeviceType::default().into(), ); exit(1); @@ -1181,23 +1158,14 @@ fn get_setup() -> Setup { } }, ), - ap_port: opt_str(AP_PORT).map(|port| { - let on_error = || { + ap_port: opt_str(AP_PORT).map(|port| match port.parse::() { + Ok(value) if value != 0 => value, + _ => { let valid_values = &format!("1 - {}", u16::MAX); invalid_error_msg(AP_PORT, AP_PORT_SHORT, &port, valid_values, ""); - }; - - let port = port.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - if port == 0 { - on_error(); exit(1); } - - port }), }; @@ -1302,8 +1270,9 @@ fn get_setup() -> Setup { .unwrap_or(player_default_config.normalisation_type); normalisation_pregain = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| { - let on_error = || { + .map(|pregain| match pregain.parse::() { + Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_PREGAIN_RANGE.start(), @@ -1317,25 +1286,18 @@ fn get_setup() -> Setup { valid_values, &player_default_config.normalisation_pregain.to_string(), ); - }; - - let pregain = pregain.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - if !(VALID_NORMALISATION_PREGAIN_RANGE).contains(&pregain) { - on_error(); exit(1); } - - pregain }) .unwrap_or(player_default_config.normalisation_pregain); normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) - .map(|threshold| { - let on_error = || { + .map(|threshold| match threshold.parse::() { + Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { + db_to_ratio(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_THRESHOLD_RANGE.start(), @@ -1349,25 +1311,18 @@ fn get_setup() -> Setup { valid_values, &ratio_to_db(player_default_config.normalisation_threshold).to_string(), ); - }; - let threshold = threshold.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - - if !(VALID_NORMALISATION_THRESHOLD_RANGE).contains(&threshold) { - on_error(); exit(1); } - - db_to_ratio(threshold) }) .unwrap_or(player_default_config.normalisation_threshold); normalisation_attack = opt_str(NORMALISATION_ATTACK) - .map(|attack| { - let on_error = || { + .map(|attack| match attack.parse::() { + Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { + Duration::from_millis(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_ATTACK_RANGE.start(), @@ -1384,25 +1339,18 @@ fn get_setup() -> Setup { .as_millis() .to_string(), ); - }; - - let attack = attack.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - if !(VALID_NORMALISATION_ATTACK_RANGE).contains(&attack) { - on_error(); exit(1); } - - Duration::from_millis(attack) }) .unwrap_or(player_default_config.normalisation_attack); normalisation_release = opt_str(NORMALISATION_RELEASE) - .map(|release| { - let on_error = || { + .map(|release| match release.parse::() { + Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { + Duration::from_millis(value) + } + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_RELEASE_RANGE.start(), @@ -1419,25 +1367,16 @@ fn get_setup() -> Setup { .as_millis() .to_string(), ); - }; - - let release = release.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - if !(VALID_NORMALISATION_RELEASE_RANGE).contains(&release) { - on_error(); exit(1); } - - Duration::from_millis(release) }) .unwrap_or(player_default_config.normalisation_release); normalisation_knee = opt_str(NORMALISATION_KNEE) - .map(|knee| { - let on_error = || { + .map(|knee| match knee.parse::() { + Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, + _ => { let valid_values = &format!( "{} - {}", VALID_NORMALISATION_KNEE_RANGE.start(), @@ -1451,47 +1390,35 @@ fn get_setup() -> Setup { valid_values, &player_default_config.normalisation_knee.to_string(), ); - }; - - let knee = knee.parse::().unwrap_or_else(|_| { - on_error(); - exit(1); - }); - if !(VALID_NORMALISATION_KNEE_RANGE).contains(&knee) { - on_error(); exit(1); } - - knee }) .unwrap_or(player_default_config.normalisation_knee); } let ditherer_name = opt_str(DITHER); let ditherer = match ditherer_name.as_deref() { - // explicitly disabled on command line - Some("none") => None, - // explicitly set on command line - Some(_) => { - if matches!(format, AudioFormat::F64 | AudioFormat::F32) { - error!("Dithering is not available with format: {:?}.", format); - exit(1); - } - - Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { - invalid_error_msg( - DITHER, - DITHER_SHORT, - &opt_str(DITHER).unwrap_or_default(), - "none, gpdf, tpdf, tpdf_hp", - "tpdf for formats S16, S24, S24_3 and none for other formats", - ); + Some(value) => match value { + "none" => None, + _ => match format { + AudioFormat::F64 | AudioFormat::F32 => { + error!("Dithering is not available with format: {:?}.", format); + exit(1); + } + _ => Some(dither::find_ditherer(ditherer_name).unwrap_or_else(|| { + invalid_error_msg( + DITHER, + DITHER_SHORT, + &opt_str(DITHER).unwrap_or_default(), + "none, gpdf, tpdf, tpdf_hp for formats S16, S24, S24_3, S32, none for formats F32, F64", + "tpdf for formats S16, S24, S24_3 and none for formats S32, F32, F64", + ); - exit(1); - })) - } - // nothing set on command line => use default + exit(1); + })), + }, + }, None => match format { AudioFormat::S16 | AudioFormat::S24 | AudioFormat::S24_3 => { player_default_config.ditherer From 67836b5b02f2990376849fc04d2f9cd0316f1e0b Mon Sep 17 00:00:00 2001 From: Mateusz Mojsiejuk Date: Tue, 14 Dec 2021 21:23:13 +0100 Subject: [PATCH 54/76] Added arm64 target to docker run examples. Also removed feature quotes as they're not nessesary and don't match the non-quoted examples of targets in the WIKI --- contrib/Dockerfile | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/contrib/Dockerfile b/contrib/Dockerfile index 74b83d31f..aa29183c1 100644 --- a/contrib/Dockerfile +++ b/contrib/Dockerfile @@ -9,8 +9,10 @@ # # If only one architecture is desired, cargo can be invoked directly with the appropriate options : # $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --no-default-features --features "alsa-backend" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features "alsa-backend" -# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features "alsa-backend" +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabihf --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target arm-unknown-linux-gnueabi --no-default-features --features alsa-backend +# $ docker run -v /tmp/librespot-build:/build librespot-cross cargo build --release --target aarch64-unknown-linux-gnu --no-default-features --features alsa-backend + # $ docker run -v /tmp/librespot-build:/build librespot-cross contrib/docker-build-pi-armv6hf.sh FROM debian:stretch From d5efb8a620554ced4fda731c72046c4b1af53111 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 14 Dec 2021 16:49:09 -0600 Subject: [PATCH 55/76] Dynamic failable buffer sizing alsa-backend Dynamically set the alsa buffer and period based on the device's reported min/max buffer and period sizes. In the event of failure use the device's defaults. This should have no effect on devices that allow for reasonable buffer and period sizes but would allow us to be more forgiving with less reasonable devices or configurations. Closes: https://github.com/librespot-org/librespot/issues/895 --- playback/src/audio_backend/alsa.rs | 187 +++++++++++++++++++++++++++-- 1 file changed, 174 insertions(+), 13 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index e572f9538..4f82a0976 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -4,16 +4,18 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use crate::{NUM_CHANNELS, SAMPLE_RATE}; use alsa::device_name::HintIter; -use alsa::pcm::{Access, Format, HwParams, PCM}; +use alsa::pcm::{Access, Format, Frames, HwParams, PCM}; use alsa::{Direction, ValueOr}; use std::cmp::min; use std::process::exit; -use std::time::Duration; use thiserror::Error; -// 0.5 sec buffer. -const PERIOD_TIME: Duration = Duration::from_millis(100); -const BUFFER_TIME: Duration = Duration::from_millis(500); +const MAX_BUFFER: Frames = (SAMPLE_RATE / 2) as Frames; +const MIN_BUFFER: Frames = (SAMPLE_RATE / 10) as Frames; +const ZERO_FRAMES: Frames = 0; + +const MAX_PERIOD_DIVISOR: Frames = 4; +const MIN_PERIOD_DIVISOR: Frames = 10; #[derive(Debug, Error)] enum AlsaError { @@ -195,28 +197,187 @@ fn open_device(dev_name: &str, format: AudioFormat) -> SinkResult<(PCM, usize)> e, })?; - hwp.set_buffer_time_near(BUFFER_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; + // Clone the hwp while it's in + // a good working state so that + // in the event of an error setting + // the buffer and period sizes + // we can use the good working clone + // instead of the hwp that's in an + // error state. + let hwp_clone = hwp.clone(); + + // At a sampling rate of 44100: + // The largest buffer is 22050 Frames (500ms) with 5512 Frame periods (125ms). + // The smallest buffer is 4410 Frames (100ms) with 441 Frame periods (10ms). + // Actual values may vary. + // + // Larger buffer and period sizes are preferred as extremely small values + // will cause high CPU useage. + // + // If no buffer or period size is in those ranges or an error happens + // trying to set the buffer or period size use the device's defaults + // which may not be ideal but are *hopefully* serviceable. + + let buffer_size = { + let max = match hwp.get_buffer_size_max() { + Err(e) => { + trace!("Error getting the device's max Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; - hwp.set_period_time_near(PERIOD_TIME.as_micros() as u32, ValueOr::Nearest) - .map_err(AlsaError::HwParams)?; + let min = match hwp.get_buffer_size_min() { + Err(e) => { + trace!("Error getting the device's min Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let buffer_size = if min < max { + match (MIN_BUFFER..=MAX_BUFFER) + .rev() + .find(|f| (min..=max).contains(f)) + { + Some(size) => { + trace!("Desired Frames per Buffer: {:?}", size); + + match hwp.set_buffer_size_near(size) { + Err(e) => { + trace!("Error setting the device's Buffer size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + } + } + None => { + trace!("No Desired Buffer size in range reported by the device."); + ZERO_FRAMES + } + } + } else { + trace!("The device's min reported Buffer size was greater than or equal to it's max reported Buffer size."); + ZERO_FRAMES + }; + + if buffer_size == ZERO_FRAMES { + trace!( + "Desired Buffer Frame range: {:?} - {:?}", + MIN_BUFFER, + MAX_BUFFER + ); + + trace!( + "Actual Buffer Frame range as reported by the device: {:?} - {:?}", + min, + max + ); + } - pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + buffer_size + }; + + let period_size = { + if buffer_size == ZERO_FRAMES { + ZERO_FRAMES + } else { + let max = match hwp.get_period_size_max() { + Err(e) => { + trace!("Error getting the device's max Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; - let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + let min = match hwp.get_period_size_min() { + Err(e) => { + trace!("Error getting the device's min Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + }; + + let max_period = buffer_size / MAX_PERIOD_DIVISOR; + let min_period = buffer_size / MIN_PERIOD_DIVISOR; + + let period_size = if min < max && min_period < max_period { + match (min_period..=max_period) + .rev() + .find(|f| (min..=max).contains(f)) + { + Some(size) => { + trace!("Desired Frames per Period: {:?}", size); + + match hwp.set_period_size_near(size, ValueOr::Nearest) { + Err(e) => { + trace!("Error setting the device's Period size: {}", e); + ZERO_FRAMES + } + Ok(s) => s, + } + } + None => { + trace!("No Desired Period size in range reported by the device."); + ZERO_FRAMES + } + } + } else { + trace!("The device's min reported Period size was greater than or equal to it's max reported Period size,"); + trace!("or the desired min Period size was greater than or equal to the desired max Period size."); + ZERO_FRAMES + }; + + if period_size == ZERO_FRAMES { + trace!("Buffer size: {:?}", buffer_size); + + trace!( + "Desired Period Frame range: {:?} (Buffer size / {:?}) - {:?} (Buffer size / {:?})", + min_period, + MIN_PERIOD_DIVISOR, + max_period, + MAX_PERIOD_DIVISOR, + ); + + trace!( + "Actual Period Frame range as reported by the device: {:?} - {:?}", + min, + max + ); + } + + period_size + } + }; + + if buffer_size == ZERO_FRAMES || period_size == ZERO_FRAMES { + trace!( + "Failed to set Buffer and/or Period size, falling back to the device's defaults." + ); + + trace!("You may experience higher than normal CPU usage and/or audio issues."); + + pcm.hw_params(&hwp_clone).map_err(AlsaError::Pcm)?; + } else { + pcm.hw_params(&hwp).map_err(AlsaError::Pcm)?; + } + + let hwp = pcm.hw_params_current().map_err(AlsaError::Pcm)?; // Don't assume we got what we wanted. Ask to make sure. let frames_per_period = hwp.get_period_size().map_err(AlsaError::HwParams)?; let frames_per_buffer = hwp.get_buffer_size().map_err(AlsaError::HwParams)?; + let swp = pcm.sw_params_current().map_err(AlsaError::Pcm)?; + swp.set_start_threshold(frames_per_buffer - frames_per_period) .map_err(AlsaError::SwParams)?; pcm.sw_params(&swp).map_err(AlsaError::Pcm)?; - trace!("Frames per Buffer: {:?}", frames_per_buffer); - trace!("Frames per Period: {:?}", frames_per_period); + trace!("Actual Frames per Buffer: {:?}", frames_per_buffer); + trace!("Actual Frames per Period: {:?}", frames_per_period); // Let ALSA do the math for us. pcm.frames_to_bytes(frames_per_period) as usize From 1cf13ea4d4e90252bec7621122fd19eadc04e1af Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Fri, 17 Dec 2021 06:51:47 +0100 Subject: [PATCH 56/76] Fix bad merge --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 036f9c013..e0ca50140 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,7 @@ fn usage(program: &str, opts: &getopts::Options) -> String { opts.usage(&brief) } +#[cfg(debug_assertions)] fn setup_logging(quiet: bool, verbose: bool) { let mut builder = env_logger::Builder::new(); match env::var("RUST_LOG") { From 305f80bdfd1305028bb24cc250f1bb8e3502e10a Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 16 Dec 2021 23:13:50 -0600 Subject: [PATCH 57/76] Fix auto fallback for --alsa-mixer-device and --alsa-mixer-index As mentioned in https://github.com/librespot-org/librespot/issues/898#issuecomment-986528998 --- CHANGELOG.md | 1 + src/main.rs | 161 +++++++++++++++++++++++++++++++++++++++++---------- 2 files changed, 131 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5757aaf8..1d603a93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. - [main] Don't panic when parsing options. Instead list valid values and exit. +- [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/src/main.rs b/src/main.rs index 2ce526e1b..84ad2a760 100644 --- a/src/main.rs +++ b/src/main.rs @@ -805,40 +805,135 @@ fn get_setup() -> Setup { exit(1); }); + let is_alsa_mixer = match mixer_type.as_deref() { + #[cfg(feature = "alsa-backend")] + Some(AlsaMixer::NAME) => true, + _ => false, + }; + + #[cfg(feature = "alsa-backend")] + if !is_alsa_mixer { + for a in &[ALSA_MIXER_DEVICE, ALSA_MIXER_INDEX, ALSA_MIXER_CONTROL] { + if opt_present(a) { + warn!("Alsa specific mixer options have no effect if not using the alsa mixer."); + break; + } + } + } + let mixer_config = { let mixer_default_config = MixerConfig::default(); #[cfg(feature = "alsa-backend")] - let device = opt_str(ALSA_MIXER_DEVICE).unwrap_or_else(|| { - if let Some(ref device_name) = device { - device_name.to_string() - } else { - mixer_default_config.device.clone() - } - }); + let index = if !is_alsa_mixer { + mixer_default_config.index + } else { + opt_str(ALSA_MIXER_INDEX) + .map(|index| { + index.parse::().unwrap_or_else(|_| { + invalid_error_msg( + ALSA_MIXER_INDEX, + ALSA_MIXER_INDEX_SHORT, + &index, + "", + &mixer_default_config.index.to_string(), + ); + + exit(1); + }) + }) + .unwrap_or_else(|| match device { + // Look for the dev index portion of --device. + // Specifically when --device is :CARD=,DEV= + // or :,. + + // If --device does not contain a ',' it does not contain a dev index. + // In the case that the dev index is omitted it is assumed to be 0 (mixer_default_config.index). + // Malformed --device values will also fallback to mixer_default_config.index. + Some(ref device_name) if device_name.contains(',') => { + // Turn :CARD=,DEV= or :, + // into DEV= or . + let dev = &device_name[device_name.find(',').unwrap_or_default()..] + .trim_start_matches(','); + + // Turn DEV= into (noop if it's already ) + // and then parse . + // Malformed --device values will fail the parse and fallback to mixer_default_config.index. + dev[dev.find('=').unwrap_or_default()..] + .trim_start_matches('=') + .parse::() + .unwrap_or(mixer_default_config.index) + } + _ => mixer_default_config.index, + }) + }; #[cfg(not(feature = "alsa-backend"))] - let device = mixer_default_config.device; + let index = mixer_default_config.index; #[cfg(feature = "alsa-backend")] - let index = opt_str(ALSA_MIXER_INDEX) - .map(|index| { - index.parse::().unwrap_or_else(|_| { - invalid_error_msg( - ALSA_MIXER_INDEX, - ALSA_MIXER_INDEX_SHORT, - &index, - "", - &mixer_default_config.index.to_string(), - ); + let device = if !is_alsa_mixer { + mixer_default_config.device + } else { + match opt_str(ALSA_MIXER_DEVICE) { + Some(mixer_device) => { + if mixer_device.is_empty() { + empty_string_error_msg(ALSA_MIXER_DEVICE, ALSA_MIXER_DEVICE_SHORT); + } - exit(1); - }) - }) - .unwrap_or_else(|| mixer_default_config.index); + mixer_device + } + None => match device { + Some(ref device_name) => { + // Look for the card name or card index portion of --device. + // Specifically when --device is :CARD=,DEV= + // or card index when --device is :,. + // --device values like `pulse`, `default`, `jack` may be valid but there is no way to + // infer automatically what the mixer should be so they fail auto fallback + // so --alsa-mixer-device must be manually specified in those situations. + let start_index = device_name.find(':').unwrap_or_default(); + + let end_index = match device_name.find(',') { + Some(index) if index > start_index => index, + _ => device_name.len(), + }; + + let card = &device_name[start_index..end_index]; + + if card.starts_with(':') { + // mixers are assumed to be hw:CARD= or hw:. + "hw".to_owned() + card + } else { + error!( + "Could not find an alsa mixer for \"{}\", it must be specified with `--{}` / `-{}`", + &device.unwrap_or_default(), + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_SHORT + ); + + exit(1); + } + } + None => { + error!( + "`--{}` / `-{}` or `--{}` / `-{}` \ + must be specified when `--{}` / `-{}` is set to \"alsa\"", + DEVICE, + DEVICE_SHORT, + ALSA_MIXER_DEVICE, + ALSA_MIXER_DEVICE_SHORT, + MIXER_TYPE, + MIXER_TYPE_SHORT + ); + + exit(1); + } + }, + } + }; #[cfg(not(feature = "alsa-backend"))] - let index = mixer_default_config.index; + let device = mixer_default_config.device; #[cfg(feature = "alsa-backend")] let control = opt_str(ALSA_MIXER_CONTROL).unwrap_or(mixer_default_config.control); @@ -881,10 +976,12 @@ fn get_setup() -> Setup { exit(1); } }) - .unwrap_or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => 0.0, // let alsa query the control - _ => VolumeCtrl::DEFAULT_DB_RANGE, + .unwrap_or_else(|| { + if is_alsa_mixer { + 0.0 + } else { + VolumeCtrl::DEFAULT_DB_RANGE + } }); let volume_ctrl = opt_str(VOLUME_CTRL) @@ -1093,10 +1190,12 @@ fn get_setup() -> Setup { (volume as f32 / 100.0 * VolumeCtrl::MAX_VOLUME as f32) as u16 }) - .or_else(|| match mixer_type.as_deref() { - #[cfg(feature = "alsa-backend")] - Some(AlsaMixer::NAME) => None, - _ => cache.as_ref().and_then(Cache::volume), + .or_else(|| { + if is_alsa_mixer { + None + } else { + cache.as_ref().and_then(Cache::volume) + } }); let device_type = opt_str(DEVICE_TYPE) From 1f43e9e389a1f1e40f1f4ebb728661d2d09c1c65 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 23 Dec 2021 20:56:16 -0600 Subject: [PATCH 58/76] Remove that last couple unwraps from main Also: * Don't just hang if Spirc shuts down too often. * Replace the while loop with Vec retain. * Be more explicit with the rate limit. --- src/main.rs | 82 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/src/main.rs b/src/main.rs index 84ad2a760..6eb9d84d9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1568,6 +1568,9 @@ fn get_setup() -> Setup { #[tokio::main(flavor = "current_thread")] async fn main() { const RUST_BACKTRACE: &str = "RUST_BACKTRACE"; + const RECONNECT_RATE_LIMIT_WINDOW: Duration = Duration::from_secs(600); + const RECONNECT_RATE_LIMIT: usize = 5; + if env::var(RUST_BACKTRACE).is_err() { env::set_var(RUST_BACKTRACE, "full") } @@ -1585,14 +1588,18 @@ async fn main() { if setup.enable_discovery { let device_id = setup.session_config.device_id.clone(); - discovery = Some( - librespot::discovery::Discovery::builder(device_id) - .name(setup.connect_config.name.clone()) - .device_type(setup.connect_config.device_type) - .port(setup.zeroconf_port) - .launch() - .unwrap(), - ); + discovery = match librespot::discovery::Discovery::builder(device_id) + .name(setup.connect_config.name.clone()) + .device_type(setup.connect_config.device_type) + .port(setup.zeroconf_port) + .launch() + { + Ok(d) => Some(d), + Err(e) => { + error!("Discovery Error: {}", e); + exit(1); + } + } } if let Some(credentials) = setup.credentials { @@ -1609,7 +1616,12 @@ async fn main() { loop { tokio::select! { - credentials = async { discovery.as_mut().unwrap().next().await }, if discovery.is_some() => { + credentials = async { + match discovery.as_mut() { + Some(d) => d.next().await, + _ => None + } + }, if discovery.is_some() => { match credentials { Some(credentials) => { last_credentials = Some(credentials.clone()); @@ -1630,8 +1642,8 @@ async fn main() { ).fuse()); }, None => { - warn!("Discovery stopped!"); - discovery = None; + error!("Discovery stopped unexpectedly"); + exit(1); } } }, @@ -1682,20 +1694,22 @@ async fn main() { exit(1); } }, - _ = async { spirc_task.as_mut().unwrap().await }, if spirc_task.is_some() => { + _ = async { + if let Some(task) = spirc_task.as_mut() { + task.await; + } + }, if spirc_task.is_some() => { spirc_task = None; warn!("Spirc shut down unexpectedly"); - while !auto_connect_times.is_empty() - && ((Instant::now() - auto_connect_times[0]).as_secs() > 600) - { - let _ = auto_connect_times.remove(0); - } - if let Some(credentials) = last_credentials.clone() { - if auto_connect_times.len() >= 5 { - warn!("Spirc shut down too often. Not reconnecting automatically."); - } else { + let mut reconnect_exceeds_rate_limit = || { + auto_connect_times.retain(|&t| t.elapsed() < RECONNECT_RATE_LIMIT_WINDOW); + auto_connect_times.len() > RECONNECT_RATE_LIMIT + }; + + match last_credentials.clone() { + Some(credentials) if !reconnect_exceeds_rate_limit() => { auto_connect_times.push(Instant::now()); connecting = Box::pin(Session::connect( @@ -1703,19 +1717,25 @@ async fn main() { credentials, setup.cache.clone(), ).fuse()); - } + }, + _ => { + error!("Spirc shut down too often. Not reconnecting automatically."); + exit(1); + }, } }, - event = async { player_event_channel.as_mut().unwrap().recv().await }, if player_event_channel.is_some() => match event { + event = async { + match player_event_channel.as_mut() { + Some(p) => p.recv().await, + _ => None + } + }, if player_event_channel.is_some() => match event { Some(event) => { if let Some(program) = &setup.player_event_program { if let Some(child) = run_program_on_events(event, program) { - if child.is_ok() { - - let mut child = child.unwrap(); - + if let Ok(mut child) = child { tokio::spawn(async move { - match child.wait().await { + match child.wait().await { Ok(e) if e.success() => (), Ok(e) => { if let Some(code) = e.code() { @@ -1741,7 +1761,8 @@ async fn main() { }, _ = tokio::signal::ctrl_c() => { break; - } + }, + else => break, } } @@ -1754,7 +1775,8 @@ async fn main() { if let Some(mut spirc_task) = spirc_task { tokio::select! { _ = tokio::signal::ctrl_c() => (), - _ = spirc_task.as_mut() => () + _ = spirc_task.as_mut() => (), + else => (), } } } From a194e80f3688f2cb351a644c7692c4289423014b Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Mon, 27 Dec 2021 12:48:09 +0100 Subject: [PATCH 59/76] Restore giving feedback about auth success. The plugin is checking for this. --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index 5ac36487a..c9b680006 100644 --- a/src/main.rs +++ b/src/main.rs @@ -947,6 +947,7 @@ async fn main() { Ok(session) => { // Spotty auth mode: exit after saving credentials if setup.authenticate { + println!("authorized"); break; } From f09be4850ed1c79dc77becdb103b97bbe6a26d48 Mon Sep 17 00:00:00 2001 From: Guillaume Desmottes Date: Wed, 29 Dec 2021 16:26:24 +0100 Subject: [PATCH 60/76] Sink: pass ownership of the packet on write() Prevent a copy if the implementation needs to keep the data around. --- CHANGELOG.md | 1 + playback/src/audio_backend/jackaudio.rs | 2 +- playback/src/audio_backend/mod.rs | 16 ++++++++-------- playback/src/audio_backend/portaudio.rs | 8 ++++---- playback/src/audio_backend/rodio.rs | 2 +- playback/src/audio_backend/sdl.rs | 2 +- playback/src/player.rs | 2 +- 7 files changed, 17 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d603a93c..a2be09926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [playback] `alsa`: Improve `--device ?` functionality for the alsa backend. - [contrib] Hardened security of the systemd service units - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). +- [playback] `Sink`: `write()` now receives ownership of the packet (breaking). ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/jackaudio.rs b/playback/src/audio_backend/jackaudio.rs index 15acf99d2..b4d24949f 100644 --- a/playback/src/audio_backend/jackaudio.rs +++ b/playback/src/audio_backend/jackaudio.rs @@ -66,7 +66,7 @@ impl Open for JackSink { } impl Sink for JackSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() .map_err(|e| SinkError::OnWrite(e.to_string()))?; diff --git a/playback/src/audio_backend/mod.rs b/playback/src/audio_backend/mod.rs index dc21fb3db..6c903d224 100644 --- a/playback/src/audio_backend/mod.rs +++ b/playback/src/audio_backend/mod.rs @@ -28,7 +28,7 @@ pub trait Sink { fn stop(&mut self) -> SinkResult<()> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()>; + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()>; } pub type SinkBuilder = fn(Option, AudioFormat) -> Box; @@ -44,34 +44,34 @@ fn mk_sink(device: Option, format: AudioFormat // reuse code for various backends macro_rules! sink_as_bytes { () => { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { use crate::convert::i24; use zerocopy::AsBytes; match packet { AudioPacket::Samples(samples) => match self.format { AudioFormat::F64 => self.write_bytes(samples.as_bytes()), AudioFormat::F32 => { - let samples_f32: &[f32] = &converter.f64_to_f32(samples); + let samples_f32: &[f32] = &converter.f64_to_f32(&samples); self.write_bytes(samples_f32.as_bytes()) } AudioFormat::S32 => { - let samples_s32: &[i32] = &converter.f64_to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(&samples); self.write_bytes(samples_s32.as_bytes()) } AudioFormat::S24 => { - let samples_s24: &[i32] = &converter.f64_to_s24(samples); + let samples_s24: &[i32] = &converter.f64_to_s24(&samples); self.write_bytes(samples_s24.as_bytes()) } AudioFormat::S24_3 => { - let samples_s24_3: &[i24] = &converter.f64_to_s24_3(samples); + let samples_s24_3: &[i24] = &converter.f64_to_s24_3(&samples); self.write_bytes(samples_s24_3.as_bytes()) } AudioFormat::S16 => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(&samples); self.write_bytes(samples_s16.as_bytes()) } }, - AudioPacket::OggData(samples) => self.write_bytes(samples), + AudioPacket::OggData(samples) => self.write_bytes(&samples), } } }; diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 7a0b179f7..12a5404d4 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -140,7 +140,7 @@ impl<'a> Sink for PortAudioSink<'a> { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! write_sink { (ref mut $stream: expr, $samples: expr) => { $stream.as_mut().unwrap().write($samples) @@ -153,15 +153,15 @@ impl<'a> Sink for PortAudioSink<'a> { let result = match self { Self::F32(stream, _parameters) => { - let samples_f32: &[f32] = &converter.f64_to_f32(samples); + let samples_f32: &[f32] = &converter.f64_to_f32(&samples); write_sink!(ref mut stream, samples_f32) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &converter.f64_to_s32(samples); + let samples_s32: &[i32] = &converter.f64_to_s32(&samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &converter.f64_to_s16(samples); + let samples_s16: &[i16] = &converter.f64_to_s16(&samples); write_sink!(ref mut stream, samples_s16) } }; diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index ab356d67d..9f4ad059e 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -189,7 +189,7 @@ pub fn open(host: cpal::Host, device: Option, format: AudioFormat) -> Ro } impl Sink for RodioSink { - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { let samples = packet .samples() .map_err(|e| RodioError::Samples(e.to_string()))?; diff --git a/playback/src/audio_backend/sdl.rs b/playback/src/audio_backend/sdl.rs index 6272fa323..1c9794a28 100644 --- a/playback/src/audio_backend/sdl.rs +++ b/playback/src/audio_backend/sdl.rs @@ -82,7 +82,7 @@ impl Sink for SdlSink { Ok(()) } - fn write(&mut self, packet: &AudioPacket, converter: &mut Converter) -> SinkResult<()> { + fn write(&mut self, packet: AudioPacket, converter: &mut Converter) -> SinkResult<()> { macro_rules! drain_sink { ($queue: expr, $size: expr) => {{ // sleep and wait for sdl thread to drain the queue a bit diff --git a/playback/src/player.rs b/playback/src/player.rs index d8dbb190c..d3bc2b398 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -1384,7 +1384,7 @@ impl PlayerInternal { } } - if let Err(e) = self.sink.write(&packet, &mut self.converter) { + if let Err(e) = self.sink.write(packet, &mut self.converter) { error!("{}", e); exit(1); } From 8dfa00d66f64180e504067f81824397638eed6a8 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Sat, 1 Jan 2022 17:19:12 -0600 Subject: [PATCH 61/76] Clean up list_compatible_devices Fix a typo and be a little more forgiving. --- playback/src/audio_backend/alsa.rs | 85 +++++++++++++++++------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/playback/src/audio_backend/alsa.rs b/playback/src/audio_backend/alsa.rs index 4f82a0976..16aa420d9 100644 --- a/playback/src/audio_backend/alsa.rs +++ b/playback/src/audio_backend/alsa.rs @@ -62,8 +62,8 @@ enum AlsaError { #[error(" PCM, {0}")] Pcm(alsa::Error), - #[error(" Could Not Parse Ouput Name(s) and/or Description(s)")] - Parsing, + #[error(" Could Not Parse Output Name(s) and/or Description(s), {0}")] + Parsing(alsa::Error), #[error("")] NotConnected, @@ -107,49 +107,58 @@ pub struct AlsaSink { } fn list_compatible_devices() -> SinkResult<()> { + let i = HintIter::new_str(None, "pcm").map_err(AlsaError::Parsing)?; + println!("\n\n\tCompatible alsa device(s):\n"); println!("\t------------------------------------------------------\n"); - let i = HintIter::new_str(None, "pcm").map_err(|_| AlsaError::Parsing)?; - for a in i { if let Some(Direction::Playback) = a.direction { - let name = a.name.ok_or(AlsaError::Parsing)?; - let desc = a.desc.ok_or(AlsaError::Parsing)?; - - if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { - if let Ok(hwp) = HwParams::any(&pcm) { - // Only show devices that support - // 2 ch 44.1 Interleaved. - if hwp.set_access(Access::RWInterleaved).is_ok() - && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() - && hwp.set_channels(NUM_CHANNELS as u32).is_ok() - { - println!("\tDevice:\n\n\t\t{}\n", name); - println!("\tDescription:\n\n\t\t{}\n", desc.replace("\n", "\n\t\t")); - - let mut supported_formats = vec![]; - - for f in &[ - AudioFormat::S16, - AudioFormat::S24, - AudioFormat::S24_3, - AudioFormat::S32, - AudioFormat::F32, - AudioFormat::F64, - ] { - if hwp.test_format(Format::from(*f)).is_ok() { - supported_formats.push(format!("{:?}", f)); + if let Some(name) = a.name { + if let Ok(pcm) = PCM::new(&name, Direction::Playback, false) { + if let Ok(hwp) = HwParams::any(&pcm) { + // Only show devices that support + // 2 ch 44.1 Interleaved. + + if hwp.set_access(Access::RWInterleaved).is_ok() + && hwp.set_rate(SAMPLE_RATE, ValueOr::Nearest).is_ok() + && hwp.set_channels(NUM_CHANNELS as u32).is_ok() + { + let mut supported_formats = vec![]; + + for f in &[ + AudioFormat::S16, + AudioFormat::S24, + AudioFormat::S24_3, + AudioFormat::S32, + AudioFormat::F32, + AudioFormat::F64, + ] { + if hwp.test_format(Format::from(*f)).is_ok() { + supported_formats.push(format!("{:?}", f)); + } } - } - println!( - "\tSupported Format(s):\n\n\t\t{}\n", - supported_formats.join(" ") - ); - println!("\t------------------------------------------------------\n"); - } - }; + if !supported_formats.is_empty() { + println!("\tDevice:\n\n\t\t{}\n", name); + + println!( + "\tDescription:\n\n\t\t{}\n", + a.desc.unwrap_or_default().replace("\n", "\n\t\t") + ); + + println!( + "\tSupported Format(s):\n\n\t\t{}\n", + supported_formats.join(" ") + ); + + println!( + "\t------------------------------------------------------\n" + ); + } + } + }; + } } } } From cfde70f6f90fc1351ba82662f7e9a1568812aa44 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Wed, 5 Jan 2022 16:54:25 -0600 Subject: [PATCH 62/76] Fix clippy lint warning --- playback/src/audio_backend/portaudio.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/playback/src/audio_backend/portaudio.rs b/playback/src/audio_backend/portaudio.rs index 12a5404d4..1681ad07d 100644 --- a/playback/src/audio_backend/portaudio.rs +++ b/playback/src/audio_backend/portaudio.rs @@ -153,15 +153,15 @@ impl<'a> Sink for PortAudioSink<'a> { let result = match self { Self::F32(stream, _parameters) => { - let samples_f32: &[f32] = &converter.f64_to_f32(&samples); + let samples_f32: &[f32] = &converter.f64_to_f32(samples); write_sink!(ref mut stream, samples_f32) } Self::S32(stream, _parameters) => { - let samples_s32: &[i32] = &converter.f64_to_s32(&samples); + let samples_s32: &[i32] = &converter.f64_to_s32(samples); write_sink!(ref mut stream, samples_s32) } Self::S16(stream, _parameters) => { - let samples_s16: &[i16] = &converter.f64_to_s16(&samples); + let samples_s16: &[i16] = &converter.f64_to_s16(samples); write_sink!(ref mut stream, samples_s16) } }; From 5e8e2ba8c59dbc3684244ca5180e930550e4cafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20F=C3=B6rberg?= Date: Sat, 8 Jan 2022 15:48:33 +0100 Subject: [PATCH 63/76] examples/playlist_tracks: Use normal URI parser --- examples/playlist_tracks.rs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/examples/playlist_tracks.rs b/examples/playlist_tracks.rs index 75c656bbe..0bf17ee7d 100644 --- a/examples/playlist_tracks.rs +++ b/examples/playlist_tracks.rs @@ -1,4 +1,5 @@ use std::env; +use std::process; use librespot::core::authentication::Credentials; use librespot::core::config::SessionConfig; @@ -18,11 +19,13 @@ async fn main() { } let credentials = Credentials::with_password(&args[1], &args[2]); - let uri_split = args[3].split(':'); - let uri_parts: Vec<&str> = uri_split.collect(); - println!("{}, {}, {}", uri_parts[0], uri_parts[1], uri_parts[2]); - - let plist_uri = SpotifyId::from_base62(uri_parts[2]).unwrap(); + let plist_uri = SpotifyId::from_uri(&args[3]).unwrap_or_else(|_| { + eprintln!( + "PLAYLIST should be a playlist URI such as: \ + \"spotify:playlist:37i9dQZF1DXec50AjHrNTq\"" + ); + process::exit(1); + }); let session = Session::connect(session_config, credentials, None) .await From 1e54913523e7897f728984e65dcfe301cf739670 Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 08:20:29 +0100 Subject: [PATCH 64/76] Fix `--device` argument to various backends (#938) Fix `--device` argument to various backends --- CHANGELOG.md | 1 + playback/src/audio_backend/pipe.rs | 21 ++++++++++++----- playback/src/audio_backend/rodio.rs | 21 +++++++---------- playback/src/audio_backend/subprocess.rs | 26 +++++++++++++------- src/main.rs | 30 ------------------------ 5 files changed, 42 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a2be09926..0bd19240d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [contrib] Hardened security of the systemd service units - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). - [playback] `Sink`: `write()` now receives ownership of the packet (breaking). +- [playback] `pipe`: create file if it doesn't already exist ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/audio_backend/pipe.rs b/playback/src/audio_backend/pipe.rs index fd804a0e5..682f8124e 100644 --- a/playback/src/audio_backend/pipe.rs +++ b/playback/src/audio_backend/pipe.rs @@ -4,19 +4,27 @@ use crate::convert::Converter; use crate::decoder::AudioPacket; use std::fs::OpenOptions; use std::io::{self, Write}; +use std::process::exit; pub struct StdoutSink { output: Option>, - path: Option, + file: Option, format: AudioFormat, } impl Open for StdoutSink { - fn open(path: Option, format: AudioFormat) -> Self { + fn open(file: Option, format: AudioFormat) -> Self { + if let Some("?") = file.as_deref() { + info!("Usage:"); + println!(" Output to stdout: --backend pipe"); + println!(" Output to file: --backend pipe --device {{filename}}"); + exit(0); + } + info!("Using pipe sink with format: {:?}", format); Self { output: None, - path, + file, format, } } @@ -25,11 +33,12 @@ impl Open for StdoutSink { impl Sink for StdoutSink { fn start(&mut self) -> SinkResult<()> { if self.output.is_none() { - let output: Box = match self.path.as_deref() { - Some(path) => { + let output: Box = match self.file.as_deref() { + Some(file) => { let open_op = OpenOptions::new() .write(true) - .open(path) + .create(true) + .open(file) .map_err(|e| SinkError::ConnectionRefused(e.to_string()))?; Box::new(open_op) } diff --git a/playback/src/audio_backend/rodio.rs b/playback/src/audio_backend/rodio.rs index 9f4ad059e..bbc5de1a0 100644 --- a/playback/src/audio_backend/rodio.rs +++ b/playback/src/audio_backend/rodio.rs @@ -135,21 +135,18 @@ fn create_sink( host: &cpal::Host, device: Option, ) -> Result<(rodio::Sink, rodio::OutputStream), RodioError> { - let rodio_device = match device { - Some(ask) if &ask == "?" => { - let exit_code = match list_outputs(host) { - Ok(()) => 0, - Err(e) => { - error!("{}", e); - 1 - } - }; - exit(exit_code) - } + let rodio_device = match device.as_deref() { + Some("?") => match list_outputs(host) { + Ok(()) => exit(0), + Err(e) => { + error!("{}", e); + exit(1); + } + }, Some(device_name) => { host.output_devices()? .find(|d| d.name().ok().map_or(false, |name| name == device_name)) // Ignore devices for which getting name fails - .ok_or(RodioError::DeviceNotAvailable(device_name))? + .ok_or_else(|| RodioError::DeviceNotAvailable(device_name.to_string()))? } None => host .default_output_device() diff --git a/playback/src/audio_backend/subprocess.rs b/playback/src/audio_backend/subprocess.rs index c501cf83f..63fc5d880 100644 --- a/playback/src/audio_backend/subprocess.rs +++ b/playback/src/audio_backend/subprocess.rs @@ -5,7 +5,7 @@ use crate::decoder::AudioPacket; use shell_words::split; use std::io::Write; -use std::process::{Child, Command, Stdio}; +use std::process::{exit, Child, Command, Stdio}; pub struct SubprocessSink { shell_command: String, @@ -15,16 +15,24 @@ pub struct SubprocessSink { impl Open for SubprocessSink { fn open(shell_command: Option, format: AudioFormat) -> Self { + let shell_command = match shell_command.as_deref() { + Some("?") => { + info!("Usage: --backend subprocess --device {{shell_command}}"); + exit(0); + } + Some(cmd) => cmd.to_owned(), + None => { + error!("subprocess sink requires specifying a shell command"); + exit(1); + } + }; + info!("Using subprocess sink with format: {:?}", format); - if let Some(shell_command) = shell_command { - SubprocessSink { - shell_command, - child: None, - format, - } - } else { - panic!("subprocess sink requires specifying a shell command"); + Self { + shell_command, + child: None, + format, } } } diff --git a/src/main.rs b/src/main.rs index 6eb9d84d9..9bef51844 100644 --- a/src/main.rs +++ b/src/main.rs @@ -748,18 +748,7 @@ fn get_setup() -> Setup { }) .unwrap_or_default(); - #[cfg(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - ))] let device = opt_str(DEVICE); - - #[cfg(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - ))] if let Some(ref value) = device { if value == "?" { backend(device, format); @@ -769,25 +758,6 @@ fn get_setup() -> Setup { } } - #[cfg(not(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - )))] - let device: Option = None; - - #[cfg(not(any( - feature = "alsa-backend", - feature = "rodio-backend", - feature = "portaudio-backend" - )))] - if opt_present(DEVICE) { - warn!( - "The `--{}` / `-{}` option is not supported by the included audio backend(s), and has no effect.", - DEVICE, DEVICE_SHORT, - ); - } - #[cfg(feature = "alsa-backend")] let mixer_type = opt_str(MIXER_TYPE); #[cfg(not(feature = "alsa-backend"))] From 72af0d2014c7cd5f9f41ba6521e6b17f604695ce Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Fri, 14 Jan 2022 23:31:29 +0100 Subject: [PATCH 65/76] New dynamic limiter for very wide dynamic ranges (#935) New dynamic limiter for very wide dynamic ranges --- CHANGELOG.md | 1 + playback/src/config.rs | 29 +++--- playback/src/player.rs | 211 ++++++++++++++++++----------------------- src/main.rs | 88 ++++++++--------- 4 files changed, 149 insertions(+), 180 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bd19240d..c0a91a292 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Verbose logging mode (`-v`, `--verbose`) now logs all parsed environment variables and command line arguments (credentials are redacted). - [playback] `Sink`: `write()` now receives ownership of the packet (breaking). - [playback] `pipe`: create file if it doesn't already exist +- [playback] More robust dynamic limiter for very wide dynamic range (breaking) ### Added - [cache] Add `disable-credential-cache` flag (breaking). diff --git a/playback/src/config.rs b/playback/src/config.rs index b8313bf40..4070a26ae 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,10 +1,7 @@ -use super::player::db_to_ratio; -use crate::convert::i24; -pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use std::{mem, str::FromStr, time::Duration}; -use std::mem; -use std::str::FromStr; -use std::time::Duration; +pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; +use crate::{convert::i24, player::duration_to_coefficient}; #[derive(Clone, Copy, Debug, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { @@ -133,11 +130,11 @@ pub struct PlayerConfig { pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, - pub normalisation_pregain: f64, - pub normalisation_threshold: f64, - pub normalisation_attack: Duration, - pub normalisation_release: Duration, - pub normalisation_knee: f64, + pub normalisation_pregain_db: f32, + pub normalisation_threshold_dbfs: f64, + pub normalisation_attack_cf: f64, + pub normalisation_release_cf: f64, + pub normalisation_knee_db: f64, // pass function pointers so they can be lazily instantiated *after* spawning a thread // (thereby circumventing Send bounds that they might not satisfy) @@ -152,11 +149,11 @@ impl Default for PlayerConfig { normalisation: false, normalisation_type: NormalisationType::default(), normalisation_method: NormalisationMethod::default(), - normalisation_pregain: 0.0, - normalisation_threshold: db_to_ratio(-2.0), - normalisation_attack: Duration::from_millis(5), - normalisation_release: Duration::from_millis(100), - normalisation_knee: 1.0, + normalisation_pregain_db: 0.0, + normalisation_threshold_dbfs: -2.0, + normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)), + normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)), + normalisation_knee_db: 5.0, passthrough: false, ditherer: Some(mk_ditherer::), } diff --git a/playback/src/player.rs b/playback/src/player.rs index d3bc2b398..f863b0e93 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -61,12 +61,8 @@ struct PlayerInternal { event_senders: Vec>, converter: Converter, - limiter_active: bool, - limiter_attack_counter: u32, - limiter_release_counter: u32, - limiter_peak_sample: f64, - limiter_factor: f64, - limiter_strength: f64, + normalisation_integrator: f64, + normalisation_peak: f64, auto_normalise_as_album: bool, } @@ -208,6 +204,14 @@ pub fn ratio_to_db(ratio: f64) -> f64 { ratio.log10() * DB_VOLTAGE_RATIO } +pub fn duration_to_coefficient(duration: Duration) -> f64 { + f64::exp(-1.0 / (duration.as_secs_f64() * SAMPLES_PER_SECOND as f64)) +} + +pub fn coefficient_to_duration(coefficient: f64) -> Duration { + Duration::from_secs_f64(-1.0 / f64::ln(coefficient) / SAMPLES_PER_SECOND as f64) +} + #[derive(Clone, Copy, Debug)] pub struct NormalisationData { track_gain_db: f32, @@ -241,17 +245,18 @@ impl NormalisationData { return 1.0; } - let [gain_db, gain_peak] = if config.normalisation_type == NormalisationType::Album { - [data.album_gain_db, data.album_peak] + let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album { + (data.album_gain_db as f64, data.album_peak as f64) } else { - [data.track_gain_db, data.track_peak] + (data.track_gain_db as f64, data.track_peak as f64) }; - let normalisation_power = gain_db as f64 + config.normalisation_pregain; + let normalisation_power = gain_db + config.normalisation_pregain_db as f64; let mut normalisation_factor = db_to_ratio(normalisation_power); - if normalisation_factor * gain_peak as f64 > config.normalisation_threshold { - let limited_normalisation_factor = config.normalisation_threshold / gain_peak as f64; + if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs { + let limited_normalisation_factor = + db_to_ratio(config.normalisation_threshold_dbfs as f64) / gain_peak; let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { @@ -295,18 +300,25 @@ impl Player { debug!("Normalisation Type: {:?}", config.normalisation_type); debug!( "Normalisation Pregain: {:.1} dB", - config.normalisation_pregain + config.normalisation_pregain_db ); debug!( "Normalisation Threshold: {:.1} dBFS", - ratio_to_db(config.normalisation_threshold) + config.normalisation_threshold_dbfs ); debug!("Normalisation Method: {:?}", config.normalisation_method); if config.normalisation_method == NormalisationMethod::Dynamic { - debug!("Normalisation Attack: {:?}", config.normalisation_attack); - debug!("Normalisation Release: {:?}", config.normalisation_release); - debug!("Normalisation Knee: {:?}", config.normalisation_knee); + // as_millis() has rounding errors (truncates) + debug!( + "Normalisation Attack: {:.0} ms", + coefficient_to_duration(config.normalisation_attack_cf).as_secs_f64() * 1000. + ); + debug!( + "Normalisation Release: {:.0} ms", + coefficient_to_duration(config.normalisation_release_cf).as_secs_f64() * 1000. + ); + debug!("Normalisation Knee: {} dB", config.normalisation_knee_db); } } @@ -329,12 +341,8 @@ impl Player { event_senders: [event_sender].to_vec(), converter, - limiter_active: false, - limiter_attack_counter: 0, - limiter_release_counter: 0, - limiter_peak_sample: 0.0, - limiter_factor: 1.0, - limiter_strength: 0.0, + normalisation_peak: 0.0, + normalisation_integrator: 0.0, auto_normalise_as_album: false, }; @@ -1275,110 +1283,82 @@ impl PlayerInternal { Some(mut packet) => { if !packet.is_empty() { if let AudioPacket::Samples(ref mut data) = packet { + // For the basic normalisation method, a normalisation factor of 1.0 indicates that + // there is nothing to normalise (all samples should pass unaltered). For the + // dynamic method, there may still be peaks that we want to shave off. if self.config.normalisation && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON && self.config.normalisation_method == NormalisationMethod::Basic) { + // zero-cost shorthands + let threshold_db = self.config.normalisation_threshold_dbfs; + let knee_db = self.config.normalisation_knee_db; + let attack_cf = self.config.normalisation_attack_cf; + let release_cf = self.config.normalisation_release_cf; + for sample in data.iter_mut() { - let mut actual_normalisation_factor = normalisation_factor; + *sample *= normalisation_factor; // for both the basic and dynamic limiter + + // Feedforward limiter in the log domain + // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic + // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio + // Engineering Society, 60, 399-408. if self.config.normalisation_method == NormalisationMethod::Dynamic { - if self.limiter_active { - // "S"-shaped curve with a configurable knee during attack and release: - // - > 1.0 yields soft knees at start and end, steeper in between - // - 1.0 yields a linear function from 0-100% - // - between 0.0 and 1.0 yields hard knees at start and end, flatter in between - // - 0.0 yields a step response to 50%, causing distortion - // - Rates < 0.0 invert the limiter and are invalid - let mut shaped_limiter_strength = self.limiter_strength; - if shaped_limiter_strength > 0.0 - && shaped_limiter_strength < 1.0 - { - shaped_limiter_strength = 1.0 - / (1.0 - + f64::powf( - shaped_limiter_strength - / (1.0 - shaped_limiter_strength), - -self.config.normalisation_knee, - )); - } - actual_normalisation_factor = - (1.0 - shaped_limiter_strength) * normalisation_factor - + shaped_limiter_strength * self.limiter_factor; - }; + // steps 1 + 2: half-wave rectification and conversion into dB + let abs_sample_db = ratio_to_db(sample.abs()); - // Cast the fields here for better readability - let normalisation_attack = - self.config.normalisation_attack.as_secs_f64(); - let normalisation_release = - self.config.normalisation_release.as_secs_f64(); - let limiter_release_counter = - self.limiter_release_counter as f64; - let limiter_attack_counter = self.limiter_attack_counter as f64; - let samples_per_second = SAMPLES_PER_SECOND as f64; - - // Always check for peaks, even when the limiter is already active. - // There may be even higher peaks than we initially targeted. - // Check against the normalisation factor that would be applied normally. - let abs_sample = f64::abs(*sample * normalisation_factor); - if abs_sample > self.config.normalisation_threshold { - self.limiter_active = true; - if self.limiter_release_counter > 0 { - // A peak was encountered while releasing the limiter; - // synchronize with the current release limiter strength. - self.limiter_attack_counter = (((samples_per_second - * normalisation_release) - - limiter_release_counter) - / (normalisation_release / normalisation_attack)) - as u32; - self.limiter_release_counter = 0; - } - - self.limiter_attack_counter = - self.limiter_attack_counter.saturating_add(1); + // Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0) + // returns -inf and gets the peak detector stuck. + if !abs_sample_db.is_normal() { + continue; + } - self.limiter_strength = limiter_attack_counter - / (samples_per_second * normalisation_attack); + // step 3: gain computer with soft knee + let biased_sample = abs_sample_db - threshold_db; + let limited_sample = if 2.0 * biased_sample < -knee_db { + abs_sample_db + } else if 2.0 * biased_sample.abs() <= knee_db { + abs_sample_db + - (biased_sample + knee_db / 2.0).powi(2) + / (2.0 * knee_db) + } else { + threshold_db as f64 + }; - if abs_sample > self.limiter_peak_sample { - self.limiter_peak_sample = abs_sample; - self.limiter_factor = - self.config.normalisation_threshold - / self.limiter_peak_sample; - } - } else if self.limiter_active { - if self.limiter_attack_counter > 0 { - // Release may start within the attack period, before - // the limiter reached full strength. For that reason - // start the release by synchronizing with the current - // attack limiter strength. - self.limiter_release_counter = (((samples_per_second - * normalisation_attack) - - limiter_attack_counter) - * (normalisation_release / normalisation_attack)) - as u32; - self.limiter_attack_counter = 0; - } + // step 4: subtractor + let limiter_input = abs_sample_db - limited_sample; - self.limiter_release_counter = - self.limiter_release_counter.saturating_add(1); - - if self.limiter_release_counter - > (samples_per_second * normalisation_release) as u32 - { - self.reset_limiter(); - } else { - self.limiter_strength = ((samples_per_second - * normalisation_release) - - limiter_release_counter) - / (samples_per_second * normalisation_release); - } + // Spare the CPU unless the limiter is active or we are riding a peak. + if !(limiter_input > 0.0 + || self.normalisation_integrator > 0.0 + || self.normalisation_peak > 0.0) + { + continue; } + + // step 5: smooth, decoupled peak detector + self.normalisation_integrator = f64::max( + limiter_input, + release_cf * self.normalisation_integrator + + (1.0 - release_cf) * limiter_input, + ); + self.normalisation_peak = attack_cf * self.normalisation_peak + + (1.0 - attack_cf) * self.normalisation_integrator; + + // step 6: make-up gain applied later (volume attenuation) + // Applying the standard normalisation factor here won't work, + // because there are tracks with peaks as high as 6 dB above + // the default threshold, so that would clip. + + // steps 7-8: conversion into level and multiplication into gain stage + *sample *= db_to_ratio(-self.normalisation_peak); } - *sample *= actual_normalisation_factor; } } + // Apply volume attenuation last. TODO: make this so we can chain + // the normaliser and mixer as a processing pipeline. if let Some(ref editor) = self.audio_filter { editor.modify_stream(data) } @@ -1411,15 +1391,6 @@ impl PlayerInternal { } } - fn reset_limiter(&mut self) { - self.limiter_active = false; - self.limiter_release_counter = 0; - self.limiter_attack_counter = 0; - self.limiter_peak_sample = 0.0; - self.limiter_factor = 1.0; - self.limiter_strength = 0.0; - } - fn start_playback( &mut self, track_id: SpotifyId, diff --git a/src/main.rs b/src/main.rs index 9bef51844..6c4328083 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,7 @@ use librespot::playback::dither; #[cfg(feature = "alsa-backend")] use librespot::playback::mixer::alsamixer::AlsaMixer; use librespot::playback::mixer::{self, MixerConfig, MixerFn}; -use librespot::playback::player::{db_to_ratio, ratio_to_db, Player}; +use librespot::playback::player::{coefficient_to_duration, duration_to_coefficient, Player}; mod player_event_handler; use player_event_handler::{emit_sink_event, run_program_on_events}; @@ -186,8 +186,8 @@ struct Setup { fn get_setup() -> Setup { const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; - const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=2.0; - const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=10.0; + const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; @@ -540,7 +540,7 @@ fn get_setup() -> Setup { .optopt( NORMALISATION_KNEE_SHORT, NORMALISATION_KNEE, - "Knee steepness of the dynamic limiter from 0.0 to 2.0. Defaults to 1.0.", + "Knee width (dB) of the dynamic limiter from 0.0 to 10.0. Defaults to 5.0.", "KNEE", ) .optopt( @@ -1257,11 +1257,11 @@ fn get_setup() -> Setup { let normalisation_method; let normalisation_type; - let normalisation_pregain; - let normalisation_threshold; - let normalisation_attack; - let normalisation_release; - let normalisation_knee; + let normalisation_pregain_db; + let normalisation_threshold_dbfs; + let normalisation_attack_cf; + let normalisation_release_cf; + let normalisation_knee_db; if !normalisation { for a in &[ @@ -1284,11 +1284,11 @@ fn get_setup() -> Setup { normalisation_method = player_default_config.normalisation_method; normalisation_type = player_default_config.normalisation_type; - normalisation_pregain = player_default_config.normalisation_pregain; - normalisation_threshold = player_default_config.normalisation_threshold; - normalisation_attack = player_default_config.normalisation_attack; - normalisation_release = player_default_config.normalisation_release; - normalisation_knee = player_default_config.normalisation_knee; + normalisation_pregain_db = player_default_config.normalisation_pregain_db; + normalisation_threshold_dbfs = player_default_config.normalisation_threshold_dbfs; + normalisation_attack_cf = player_default_config.normalisation_attack_cf; + normalisation_release_cf = player_default_config.normalisation_release_cf; + normalisation_knee_db = player_default_config.normalisation_knee_db; } else { normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() @@ -1338,8 +1338,8 @@ fn get_setup() -> Setup { }) .unwrap_or(player_default_config.normalisation_type); - normalisation_pregain = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| match pregain.parse::() { + normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN) + .map(|pregain| match pregain.parse::() { Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, _ => { let valid_values = &format!( @@ -1353,19 +1353,17 @@ fn get_setup() -> Setup { NORMALISATION_PREGAIN_SHORT, &pregain, valid_values, - &player_default_config.normalisation_pregain.to_string(), + &player_default_config.normalisation_pregain_db.to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_pregain); + .unwrap_or(player_default_config.normalisation_pregain_db); - normalisation_threshold = opt_str(NORMALISATION_THRESHOLD) + normalisation_threshold_dbfs = opt_str(NORMALISATION_THRESHOLD) .map(|threshold| match threshold.parse::() { - Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => { - db_to_ratio(value) - } + Ok(value) if (VALID_NORMALISATION_THRESHOLD_RANGE).contains(&value) => value, _ => { let valid_values = &format!( "{} - {}", @@ -1378,18 +1376,20 @@ fn get_setup() -> Setup { NORMALISATION_THRESHOLD_SHORT, &threshold, valid_values, - &ratio_to_db(player_default_config.normalisation_threshold).to_string(), + &player_default_config + .normalisation_threshold_dbfs + .to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_threshold); + .unwrap_or(player_default_config.normalisation_threshold_dbfs); - normalisation_attack = opt_str(NORMALISATION_ATTACK) + normalisation_attack_cf = opt_str(NORMALISATION_ATTACK) .map(|attack| match attack.parse::() { Ok(value) if (VALID_NORMALISATION_ATTACK_RANGE).contains(&value) => { - Duration::from_millis(value) + duration_to_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1403,8 +1403,7 @@ fn get_setup() -> Setup { NORMALISATION_ATTACK_SHORT, &attack, valid_values, - &player_default_config - .normalisation_attack + &coefficient_to_duration(player_default_config.normalisation_attack_cf) .as_millis() .to_string(), ); @@ -1412,12 +1411,12 @@ fn get_setup() -> Setup { exit(1); } }) - .unwrap_or(player_default_config.normalisation_attack); + .unwrap_or(player_default_config.normalisation_attack_cf); - normalisation_release = opt_str(NORMALISATION_RELEASE) + normalisation_release_cf = opt_str(NORMALISATION_RELEASE) .map(|release| match release.parse::() { Ok(value) if (VALID_NORMALISATION_RELEASE_RANGE).contains(&value) => { - Duration::from_millis(value) + duration_to_coefficient(Duration::from_millis(value)) } _ => { let valid_values = &format!( @@ -1431,18 +1430,19 @@ fn get_setup() -> Setup { NORMALISATION_RELEASE_SHORT, &release, valid_values, - &player_default_config - .normalisation_release - .as_millis() - .to_string(), + &coefficient_to_duration( + player_default_config.normalisation_release_cf, + ) + .as_millis() + .to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_release); + .unwrap_or(player_default_config.normalisation_release_cf); - normalisation_knee = opt_str(NORMALISATION_KNEE) + normalisation_knee_db = opt_str(NORMALISATION_KNEE) .map(|knee| match knee.parse::() { Ok(value) if (VALID_NORMALISATION_KNEE_RANGE).contains(&value) => value, _ => { @@ -1457,13 +1457,13 @@ fn get_setup() -> Setup { NORMALISATION_KNEE_SHORT, &knee, valid_values, - &player_default_config.normalisation_knee.to_string(), + &player_default_config.normalisation_knee_db.to_string(), ); exit(1); } }) - .unwrap_or(player_default_config.normalisation_knee); + .unwrap_or(player_default_config.normalisation_knee_db); } let ditherer_name = opt_str(DITHER); @@ -1505,11 +1505,11 @@ fn get_setup() -> Setup { normalisation, normalisation_type, normalisation_method, - normalisation_pregain, - normalisation_threshold, - normalisation_attack, - normalisation_release, - normalisation_knee, + normalisation_pregain_db, + normalisation_threshold_dbfs, + normalisation_attack_cf, + normalisation_release_cf, + normalisation_knee_db, ditherer, } }; From c6e97a7f8ab4b7d97b82427071b2f1555557e0cc Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Mon, 17 Jan 2022 15:57:30 -0600 Subject: [PATCH 66/76] Save some more CPU cycles in the limiter (#939) Optimise limiter CPU usage --- playback/src/config.rs | 2 +- playback/src/player.rs | 116 ++++++++++++++++++++++------------------- src/main.rs | 4 +- 3 files changed, 64 insertions(+), 58 deletions(-) diff --git a/playback/src/config.rs b/playback/src/config.rs index 4070a26ae..f1276adb3 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -130,7 +130,7 @@ pub struct PlayerConfig { pub normalisation: bool, pub normalisation_type: NormalisationType, pub normalisation_method: NormalisationMethod, - pub normalisation_pregain_db: f32, + pub normalisation_pregain_db: f64, pub normalisation_threshold_dbfs: f64, pub normalisation_attack_cf: f64, pub normalisation_release_cf: f64, diff --git a/playback/src/player.rs b/playback/src/player.rs index f863b0e93..b5603491a 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -214,10 +214,10 @@ pub fn coefficient_to_duration(coefficient: f64) -> Duration { #[derive(Clone, Copy, Debug)] pub struct NormalisationData { - track_gain_db: f32, - track_peak: f32, - album_gain_db: f32, - album_peak: f32, + track_gain_db: f64, + track_peak: f64, + album_gain_db: f64, + album_peak: f64, } impl NormalisationData { @@ -225,10 +225,10 @@ impl NormalisationData { const SPOTIFY_NORMALIZATION_HEADER_START_OFFSET: u64 = 144; file.seek(SeekFrom::Start(SPOTIFY_NORMALIZATION_HEADER_START_OFFSET))?; - let track_gain_db = file.read_f32::()?; - let track_peak = file.read_f32::()?; - let album_gain_db = file.read_f32::()?; - let album_peak = file.read_f32::()?; + let track_gain_db = file.read_f32::()? as f64; + let track_peak = file.read_f32::()? as f64; + let album_gain_db = file.read_f32::()? as f64; + let album_peak = file.read_f32::()? as f64; let r = NormalisationData { track_gain_db, @@ -246,17 +246,17 @@ impl NormalisationData { } let (gain_db, gain_peak) = if config.normalisation_type == NormalisationType::Album { - (data.album_gain_db as f64, data.album_peak as f64) + (data.album_gain_db, data.album_peak) } else { - (data.track_gain_db as f64, data.track_peak as f64) + (data.track_gain_db, data.track_peak) }; - let normalisation_power = gain_db + config.normalisation_pregain_db as f64; + let normalisation_power = gain_db + config.normalisation_pregain_db; let mut normalisation_factor = db_to_ratio(normalisation_power); if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs { let limited_normalisation_factor = - db_to_ratio(config.normalisation_threshold_dbfs as f64) / gain_peak; + db_to_ratio(config.normalisation_threshold_dbfs) / gain_peak; let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); if config.normalisation_method == NormalisationMethod::Basic { @@ -279,7 +279,7 @@ impl NormalisationData { normalisation_factor * 100.0 ); - normalisation_factor as f64 + normalisation_factor } } @@ -1305,54 +1305,60 @@ impl PlayerInternal { // Engineering Society, 60, 399-408. if self.config.normalisation_method == NormalisationMethod::Dynamic { - // steps 1 + 2: half-wave rectification and conversion into dB - let abs_sample_db = ratio_to_db(sample.abs()); - - // Some tracks have samples that are precisely 0.0, but ratio_to_db(0.0) - // returns -inf and gets the peak detector stuck. - if !abs_sample_db.is_normal() { - continue; - } - - // step 3: gain computer with soft knee - let biased_sample = abs_sample_db - threshold_db; - let limited_sample = if 2.0 * biased_sample < -knee_db { - abs_sample_db - } else if 2.0 * biased_sample.abs() <= knee_db { - abs_sample_db - - (biased_sample + knee_db / 2.0).powi(2) - / (2.0 * knee_db) + // Some tracks have samples that are precisely 0.0. That's silence + // and we know we don't need to limit that, in which we can spare + // the CPU cycles. + // + // Also, calling `ratio_to_db(0.0)` returns `inf` and would get the + // peak detector stuck. Also catch the unlikely case where a sample + // is decoded as `NaN` or some other non-normal value. + let limiter_db = if sample.is_normal() { + // step 1-2: half-wave rectification and conversion into dB + let abs_sample_db = ratio_to_db(sample.abs()); + + // step 3-4: gain computer with soft knee and subtractor + let bias_db = abs_sample_db - threshold_db; + let knee_boundary_db = bias_db * 2.0; + + if knee_boundary_db < -knee_db { + 0.0 + } else if knee_boundary_db.abs() <= knee_db { + abs_sample_db + - (abs_sample_db + - (bias_db + knee_db / 2.0).powi(2) + / (2.0 * knee_db)) + } else { + abs_sample_db - threshold_db + } } else { - threshold_db as f64 + 0.0 }; - // step 4: subtractor - let limiter_input = abs_sample_db - limited_sample; - - // Spare the CPU unless the limiter is active or we are riding a peak. - if !(limiter_input > 0.0 + // Spare the CPU unless (1) the limiter is engaged, (2) we + // were in attack or (3) we were in release, and that attack/ + // release wasn't finished yet. + if limiter_db > 0.0 || self.normalisation_integrator > 0.0 - || self.normalisation_peak > 0.0) + || self.normalisation_peak > 0.0 { - continue; + // step 5: smooth, decoupled peak detector + self.normalisation_integrator = f64::max( + limiter_db, + release_cf * self.normalisation_integrator + + (1.0 - release_cf) * limiter_db, + ); + self.normalisation_peak = attack_cf + * self.normalisation_peak + + (1.0 - attack_cf) * self.normalisation_integrator; + + // step 6: make-up gain applied later (volume attenuation) + // Applying the standard normalisation factor here won't work, + // because there are tracks with peaks as high as 6 dB above + // the default threshold, so that would clip. + + // steps 7-8: conversion into level and multiplication into gain stage + *sample *= db_to_ratio(-self.normalisation_peak); } - - // step 5: smooth, decoupled peak detector - self.normalisation_integrator = f64::max( - limiter_input, - release_cf * self.normalisation_integrator - + (1.0 - release_cf) * limiter_input, - ); - self.normalisation_peak = attack_cf * self.normalisation_peak - + (1.0 - attack_cf) * self.normalisation_integrator; - - // step 6: make-up gain applied later (volume attenuation) - // Applying the standard normalisation factor here won't work, - // because there are tracks with peaks as high as 6 dB above - // the default threshold, so that would clip. - - // steps 7-8: conversion into level and multiplication into gain stage - *sample *= db_to_ratio(-self.normalisation_peak); } } } diff --git a/src/main.rs b/src/main.rs index 6c4328083..f43500fee 100644 --- a/src/main.rs +++ b/src/main.rs @@ -187,7 +187,7 @@ fn get_setup() -> Setup { const VALID_INITIAL_VOLUME_RANGE: RangeInclusive = 0..=100; const VALID_VOLUME_RANGE: RangeInclusive = 0.0..=100.0; const VALID_NORMALISATION_KNEE_RANGE: RangeInclusive = 0.0..=10.0; - const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; + const VALID_NORMALISATION_PREGAIN_RANGE: RangeInclusive = -10.0..=10.0; const VALID_NORMALISATION_THRESHOLD_RANGE: RangeInclusive = -10.0..=0.0; const VALID_NORMALISATION_ATTACK_RANGE: RangeInclusive = 1..=500; const VALID_NORMALISATION_RELEASE_RANGE: RangeInclusive = 1..=1000; @@ -1339,7 +1339,7 @@ fn get_setup() -> Setup { .unwrap_or(player_default_config.normalisation_type); normalisation_pregain_db = opt_str(NORMALISATION_PREGAIN) - .map(|pregain| match pregain.parse::() { + .map(|pregain| match pregain.parse::() { Ok(value) if (VALID_NORMALISATION_PREGAIN_RANGE).contains(&value) => value, _ => { let valid_values = &format!( From ceebb374f0db8848a8d4135c5a06b466f2d963e1 Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Sun, 23 Jan 2022 12:02:04 -0600 Subject: [PATCH 67/76] Remove unsafe code (#940) Remove unsafe code --- CHANGELOG.md | 1 + core/src/cache.rs | 17 ++- core/src/spotify_id.rs | 34 +++-- metadata/src/lib.rs | 262 +++++++++++++++++++++++------------- playback/src/player.rs | 7 +- src/player_event_handler.rs | 124 +++++++++++++---- 6 files changed, 295 insertions(+), 150 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0a91a292..748091046 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Prevent hang when discovery is disabled and there are no credentials or when bad credentials are given. - [main] Don't panic when parsing options. Instead list valid values and exit. - [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`. +- [core] Removed unsafe code (breaking) ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/core/src/cache.rs b/core/src/cache.rs index da2ad0221..f8b0ca2ca 100644 --- a/core/src/cache.rs +++ b/core/src/cache.rs @@ -351,12 +351,17 @@ impl Cache { } fn file_path(&self, file: FileId) -> Option { - self.audio_location.as_ref().map(|location| { - let name = file.to_base16(); - let mut path = location.join(&name[0..2]); - path.push(&name[2..]); - path - }) + match file.to_base16() { + Ok(name) => self.audio_location.as_ref().map(|location| { + let mut path = location.join(&name[0..2]); + path.push(&name[2..]); + path + }), + Err(e) => { + warn!("Invalid FileId: {}", e.utf8_error()); + None + } + } } pub fn file(&self, file: FileId) -> Option { diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index e6e2bae05..10298a42c 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -2,6 +2,7 @@ use std::convert::TryInto; use std::fmt; +use std::string::FromUtf8Error; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SpotifyAudioType { @@ -136,7 +137,7 @@ impl SpotifyId { /// Returns the `SpotifyId` as a base16 (hex) encoded, `SpotifyId::SIZE_BASE16` (32) /// character long `String`. - pub fn to_base16(&self) -> String { + pub fn to_base16(&self) -> Result { to_base16(&self.to_raw(), &mut [0u8; SpotifyId::SIZE_BASE16]) } @@ -144,7 +145,7 @@ impl SpotifyId { /// character long `String`. /// /// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn to_base62(&self) -> String { + pub fn to_base62(&self) -> Result { let mut dst = [0u8; 22]; let mut i = 0; let n = self.id; @@ -182,10 +183,7 @@ impl SpotifyId { dst.reverse(); - unsafe { - // Safety: We are only dealing with ASCII characters. - String::from_utf8_unchecked(dst.to_vec()) - } + String::from_utf8(dst.to_vec()) } /// Returns a copy of the `SpotifyId` as an array of `SpotifyId::SIZE` (16) bytes in @@ -202,7 +200,7 @@ impl SpotifyId { /// be encoded as `unknown`. /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids - pub fn to_uri(&self) -> String { + pub fn to_uri(&self) -> Result { // 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31 // + unknown size audio_type. let audio_type: &str = self.audio_type.into(); @@ -210,9 +208,10 @@ impl SpotifyId { dst.push_str("spotify:"); dst.push_str(audio_type); dst.push(':'); - dst.push_str(&self.to_base62()); + let base62 = self.to_base62()?; + dst.push_str(&base62); - dst + Ok(dst) } } @@ -220,7 +219,7 @@ impl SpotifyId { pub struct FileId(pub [u8; 20]); impl FileId { - pub fn to_base16(&self) -> String { + pub fn to_base16(&self) -> Result { to_base16(&self.0, &mut [0u8; 40]) } } @@ -233,12 +232,12 @@ impl fmt::Debug for FileId { impl fmt::Display for FileId { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(&self.to_base16()) + f.write_str(&self.to_base16().unwrap_or_default()) } } #[inline] -fn to_base16(src: &[u8], buf: &mut [u8]) -> String { +fn to_base16(src: &[u8], buf: &mut [u8]) -> Result { let mut i = 0; for v in src { buf[i] = BASE16_DIGITS[(v >> 4) as usize]; @@ -246,10 +245,7 @@ fn to_base16(src: &[u8], buf: &mut [u8]) -> String { i += 2; } - unsafe { - // Safety: We are only dealing with ASCII characters. - String::from_utf8_unchecked(buf.to_vec()) - } + String::from_utf8(buf.to_vec()) } #[cfg(test)] @@ -366,7 +362,7 @@ mod tests { audio_type: c.kind, }; - assert_eq!(id.to_base62(), c.base62); + assert_eq!(id.to_base62().unwrap(), c.base62); } } @@ -389,7 +385,7 @@ mod tests { audio_type: c.kind, }; - assert_eq!(id.to_base16(), c.base16); + assert_eq!(id.to_base16().unwrap(), c.base16); } } @@ -415,7 +411,7 @@ mod tests { audio_type: c.kind, }; - assert_eq!(id.to_uri(), c.uri); + assert_eq!(id.to_uri().unwrap(), c.uri); } } diff --git a/metadata/src/lib.rs b/metadata/src/lib.rs index 2ed9273ec..435633ad2 100644 --- a/metadata/src/lib.rs +++ b/metadata/src/lib.rs @@ -7,12 +7,12 @@ extern crate log; extern crate async_trait; pub mod cover; - use std::collections::HashMap; +use std::string::FromUtf8Error; use librespot_core::mercury::MercuryError; use librespot_core::session::Session; -use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId}; +use librespot_core::spotify_id::{FileId, SpotifyAudioType, SpotifyId, SpotifyIdError}; use librespot_protocol as protocol; use protobuf::Message; @@ -83,33 +83,48 @@ trait AudioFiles { #[async_trait] impl AudioFiles for Track { async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { - let item = Self::get(session, id).await?; - Ok(AudioItem { - id, - uri: format!("spotify:track:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives: Some(item.alternatives), - }) + match id.to_base62() { + Err(e) => { + warn!("Invalid Track SpotifyId: {}", e); + Err(MercuryError) + } + Ok(uri) => { + let item = Self::get(session, id).await?; + Ok(AudioItem { + id, + uri: format!("spotify:track:{}", uri), + files: item.files, + name: item.name, + duration: item.duration, + available: item.available, + alternatives: Some(item.alternatives), + }) + } + } } } #[async_trait] impl AudioFiles for Episode { async fn get_audio_item(session: &Session, id: SpotifyId) -> Result { - let item = Self::get(session, id).await?; - - Ok(AudioItem { - id, - uri: format!("spotify:episode:{}", id.to_base62()), - files: item.files, - name: item.name, - duration: item.duration, - available: item.available, - alternatives: None, - }) + match id.to_base62() { + Err(e) => { + warn!("Invalid Episode SpotifyId: {}", e); + Err(MercuryError) + } + Ok(uri) => { + let item = Self::get(session, id).await?; + Ok(AudioItem { + id, + uri: format!("spotify:episode:{}", uri), + files: item.files, + name: item.name, + duration: item.duration, + available: item.available, + alternatives: None, + }) + } + } } } @@ -117,16 +132,38 @@ impl AudioFiles for Episode { pub trait Metadata: Send + Sized + 'static { type Message: protobuf::Message; - fn request_url(id: SpotifyId) -> String; - fn parse(msg: &Self::Message, session: &Session) -> Self; + fn request_url(id: SpotifyId) -> Result; + fn parse(msg: &Self::Message, session: &Session) -> Result; async fn get(session: &Session, id: SpotifyId) -> Result { - let uri = Self::request_url(id); - let response = session.mercury().get(uri).await?; - let data = response.payload.first().expect("Empty payload"); - let msg = Self::Message::parse_from_bytes(data).unwrap(); - - Ok(Self::parse(&msg, session)) + match Self::request_url(id) { + Err(e) => { + warn!("Invalid SpotifyId: {}", e); + Err(MercuryError) + } + Ok(uri) => { + let response = session.mercury().get(uri).await?; + match response.payload.first() { + None => { + warn!("Empty payload"); + Err(MercuryError) + } + Some(data) => match Self::Message::parse_from_bytes(data) { + Err(e) => { + warn!("Error parsing message from bytes: {}", e); + Err(MercuryError) + } + Ok(msg) => match Self::parse(&msg, session) { + Err(e) => { + warn!("Error parsing message: {:?}", e); + Err(MercuryError) + } + Ok(parsed_msg) => Ok(parsed_msg), + }, + }, + } + } + } } } @@ -192,101 +229,125 @@ pub struct Artist { impl Metadata for Track { type Message = protocol::metadata::Track; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/track/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/track/{}", id)) } - fn parse(msg: &Self::Message, session: &Session) -> Self { + fn parse(msg: &Self::Message, session: &Session) -> Result { let country = session.country(); let artists = msg .get_artist() .iter() - .filter(|artist| artist.has_gid()) - .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) - .collect::>(); + .filter_map(|artist| { + if artist.has_gid() { + SpotifyId::from_raw(artist.get_gid()).ok() + } else { + None + } + }) + .collect(); let files = msg .get_file() .iter() - .filter(|file| file.has_file_id()) - .map(|file| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(file.get_file_id()); - (file.get_format(), FileId(dst)) + .filter_map(|file| { + if file.has_file_id() { + let mut dst = [0u8; 20]; + dst.clone_from_slice(file.get_file_id()); + Some((file.get_format(), FileId(dst))) + } else { + None + } }) .collect(); - Track { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), + Ok(Track { + id: SpotifyId::from_raw(msg.get_gid())?, name: msg.get_name().to_owned(), duration: msg.get_duration(), - album: SpotifyId::from_raw(msg.get_album().get_gid()).unwrap(), + album: SpotifyId::from_raw(msg.get_album().get_gid())?, artists, files, alternatives: msg .get_alternative() .iter() - .map(|alt| SpotifyId::from_raw(alt.get_gid()).unwrap()) + .filter_map(|alt| SpotifyId::from_raw(alt.get_gid()).ok()) .collect(), available: parse_restrictions(msg.get_restriction(), &country, "premium"), - } + }) } } impl Metadata for Album { type Message = protocol::metadata::Album; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/album/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/album/{}", id)) } - fn parse(msg: &Self::Message, _: &Session) -> Self { + fn parse(msg: &Self::Message, _: &Session) -> Result { let artists = msg .get_artist() .iter() - .filter(|artist| artist.has_gid()) - .map(|artist| SpotifyId::from_raw(artist.get_gid()).unwrap()) - .collect::>(); + .filter_map(|artist| { + if artist.has_gid() { + SpotifyId::from_raw(artist.get_gid()).ok() + } else { + None + } + }) + .collect(); let tracks = msg .get_disc() .iter() .flat_map(|disc| disc.get_track()) - .filter(|track| track.has_gid()) - .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) - .collect::>(); + .filter_map(|track| { + if track.has_gid() { + SpotifyId::from_raw(track.get_gid()).ok() + } else { + None + } + }) + .collect(); let covers = msg .get_cover_group() .get_image() .iter() - .filter(|image| image.has_file_id()) - .map(|image| { - let mut dst = [0u8; 20]; - dst.clone_from_slice(image.get_file_id()); - FileId(dst) + .filter_map(|image| { + if image.has_file_id() { + let mut dst = [0u8; 20]; + dst.clone_from_slice(image.get_file_id()); + Some(FileId(dst)) + } else { + None + } }) - .collect::>(); + .collect(); - Album { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), + Ok(Album { + id: SpotifyId::from_raw(msg.get_gid())?, name: msg.get_name().to_owned(), artists, tracks, covers, - } + }) } } impl Metadata for Playlist { type Message = protocol::playlist4changes::SelectedListContent; - fn request_url(id: SpotifyId) -> String { - format!("hm://playlist/v2/playlist/{}", id.to_base62()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base62()?; + Ok(format!("hm://playlist/v2/playlist/{}", id)) } - fn parse(msg: &Self::Message, _: &Session) -> Self { + fn parse(msg: &Self::Message, _: &Session) -> Result { let tracks = msg .get_contents() .get_items() @@ -306,23 +367,24 @@ impl Metadata for Playlist { ); } - Playlist { + Ok(Playlist { revision: msg.get_revision().to_vec(), name: msg.get_attributes().get_name().to_owned(), tracks, user: msg.get_owner_username().to_string(), - } + }) } } impl Metadata for Artist { type Message = protocol::metadata::Artist; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/artist/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/artist/{}", id)) } - fn parse(msg: &Self::Message, session: &Session) -> Self { + fn parse(msg: &Self::Message, session: &Session) -> Result { let country = session.country(); let top_tracks: Vec = match msg @@ -333,17 +395,22 @@ impl Metadata for Artist { Some(tracks) => tracks .get_track() .iter() - .filter(|track| track.has_gid()) - .map(|track| SpotifyId::from_raw(track.get_gid()).unwrap()) - .collect::>(), + .filter_map(|track| { + if track.has_gid() { + SpotifyId::from_raw(track.get_gid()).ok() + } else { + None + } + }) + .collect(), None => Vec::new(), }; - Artist { - id: SpotifyId::from_raw(msg.get_gid()).unwrap(), + Ok(Artist { + id: SpotifyId::from_raw(msg.get_gid())?, name: msg.get_name().to_owned(), top_tracks, - } + }) } } @@ -351,11 +418,12 @@ impl Metadata for Artist { impl Metadata for Episode { type Message = protocol::metadata::Episode; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/episode/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/episode/{}", id)) } - fn parse(msg: &Self::Message, session: &Session) -> Self { + fn parse(msg: &Self::Message, session: &Session) -> Result { let country = session.country(); let files = msg @@ -379,9 +447,9 @@ impl Metadata for Episode { dst.clone_from_slice(image.get_file_id()); FileId(dst) }) - .collect::>(); + .collect(); - Episode { + Ok(Episode { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), external_url: msg.get_external_url().to_owned(), @@ -392,24 +460,30 @@ impl Metadata for Episode { files, available: parse_restrictions(msg.get_restriction(), &country, "premium"), explicit: msg.get_explicit().to_owned(), - } + }) } } impl Metadata for Show { type Message = protocol::metadata::Show; - fn request_url(id: SpotifyId) -> String { - format!("hm://metadata/3/show/{}", id.to_base16()) + fn request_url(id: SpotifyId) -> Result { + let id = id.to_base16()?; + Ok(format!("hm://metadata/3/show/{}", id)) } - fn parse(msg: &Self::Message, _: &Session) -> Self { + fn parse(msg: &Self::Message, _: &Session) -> Result { let episodes = msg .get_episode() .iter() - .filter(|episode| episode.has_gid()) - .map(|episode| SpotifyId::from_raw(episode.get_gid()).unwrap()) - .collect::>(); + .filter_map(|episode| { + if episode.has_gid() { + SpotifyId::from_raw(episode.get_gid()).ok() + } else { + None + } + }) + .collect(); let covers = msg .get_covers() @@ -421,15 +495,15 @@ impl Metadata for Show { dst.clone_from_slice(image.get_file_id()); FileId(dst) }) - .collect::>(); + .collect(); - Show { + Ok(Show { id: SpotifyId::from_raw(msg.get_gid()).unwrap(), name: msg.get_name().to_owned(), publisher: msg.get_publisher().to_owned(), episodes, covers, - } + }) } } diff --git a/playback/src/player.rs b/playback/src/player.rs index b5603491a..b2bdea0cd 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -740,7 +740,10 @@ impl PlayerTrackLoader { let audio = match self.find_available_alternative(audio).await { Some(audio) => audio, None => { - warn!("<{}> is not available", spotify_id.to_uri()); + warn!( + "<{}> is not available", + spotify_id.to_uri().unwrap_or_default() + ); return None; } }; @@ -748,7 +751,7 @@ impl PlayerTrackLoader { if audio.duration < 0 { error!( "Track duration for <{}> cannot be {}", - spotify_id.to_uri(), + spotify_id.to_uri().unwrap_or_default(), audio.duration ); return None; diff --git a/src/player_event_handler.rs b/src/player_event_handler.rs index 4c75128cc..785290ed2 100644 --- a/src/player_event_handler.rs +++ b/src/player_event_handler.rs @@ -5,6 +5,7 @@ use tokio::process::{Child as AsyncChild, Command as AsyncCommand}; use std::collections::HashMap; use std::io; +use std::io::{Error, ErrorKind}; use std::process::{Command, ExitStatus}; pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option> { @@ -13,45 +14,110 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option { - env_vars.insert("PLAYER_EVENT", "changed".to_string()); - env_vars.insert("OLD_TRACK_ID", old_track_id.to_base62()); - env_vars.insert("TRACK_ID", new_track_id.to_base62()); - } - PlayerEvent::Started { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "started".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - } - PlayerEvent::Stopped { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "stopped".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - } + } => match old_track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!( + "PlayerEvent::Changed: Invalid old track id: {}", + e.utf8_error() + ), + ))) + } + Ok(old_id) => match new_track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!( + "PlayerEvent::Changed: Invalid new track id: {}", + e.utf8_error() + ), + ))) + } + Ok(new_id) => { + env_vars.insert("PLAYER_EVENT", "changed".to_string()); + env_vars.insert("OLD_TRACK_ID", old_id); + env_vars.insert("TRACK_ID", new_id); + } + }, + }, + PlayerEvent::Started { track_id, .. } => match track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Started: Invalid track id: {}", e.utf8_error()), + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "started".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, + PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Stopped: Invalid track id: {}", e.utf8_error()), + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "stopped".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, PlayerEvent::Playing { track_id, duration_ms, position_ms, .. - } => { - env_vars.insert("PLAYER_EVENT", "playing".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - env_vars.insert("DURATION_MS", duration_ms.to_string()); - env_vars.insert("POSITION_MS", position_ms.to_string()); - } + } => match track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Playing: Invalid track id: {}", e.utf8_error()), + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "playing".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("DURATION_MS", duration_ms.to_string()); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, PlayerEvent::Paused { track_id, duration_ms, position_ms, .. - } => { - env_vars.insert("PLAYER_EVENT", "paused".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - env_vars.insert("DURATION_MS", duration_ms.to_string()); - env_vars.insert("POSITION_MS", position_ms.to_string()); - } - PlayerEvent::Preloading { track_id, .. } => { - env_vars.insert("PLAYER_EVENT", "preloading".to_string()); - env_vars.insert("TRACK_ID", track_id.to_base62()); - } + } => match track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!("PlayerEvent::Paused: Invalid track id: {}", e.utf8_error()), + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "paused".to_string()); + env_vars.insert("TRACK_ID", id); + env_vars.insert("DURATION_MS", duration_ms.to_string()); + env_vars.insert("POSITION_MS", position_ms.to_string()); + } + }, + PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() { + Err(e) => { + return Some(Err(Error::new( + ErrorKind::InvalidData, + format!( + "PlayerEvent::Preloading: Invalid track id: {}", + e.utf8_error() + ), + ))) + } + Ok(id) => { + env_vars.insert("PLAYER_EVENT", "preloading".to_string()); + env_vars.insert("TRACK_ID", id); + } + }, PlayerEvent::VolumeSet { volume } => { env_vars.insert("PLAYER_EVENT", "volume_set".to_string()); env_vars.insert("VOLUME", volume.to_string()); From cf1adbacb5fff4f5babe3e24398f08bb2d472512 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Tue, 25 Jan 2022 07:11:19 +0100 Subject: [PATCH 68/76] Update LMS integration to --- src/spotty.rs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/spotty.rs b/src/spotty.rs index 1bcc1a760..14e6ed8c0 100644 --- a/src/spotty.rs +++ b/src/spotty.rs @@ -175,26 +175,32 @@ impl LMS { #[cfg(debug_assertions)] info!( "event: changed, old track: {}, new track: {}", - old_track_id.to_base62(), - new_track_id.to_base62() + old_track_id.to_base62().unwrap_or_default(), + new_track_id.to_base62().unwrap_or_default() ); command = format!( r#"["spottyconnect","change","{}","{}"]"#, - new_track_id.to_base62().to_string(), - old_track_id.to_base62().to_string() + new_track_id.to_base62().unwrap_or_default().to_string(), + old_track_id.to_base62().unwrap_or_default().to_string() ); } PlayerEvent::Started { track_id, .. } => { #[cfg(debug_assertions)] - info!("event: started, track: {}", track_id.to_base62()); + info!( + "event: started, track: {}", + track_id.to_base62().unwrap_or_default() + ); command = format!( r#"["spottyconnect","start","{}"]"#, - track_id.to_base62().to_string() + track_id.to_base62().unwrap_or_default().to_string() ); } PlayerEvent::Stopped { track_id, .. } => { #[cfg(debug_assertions)] - info!("event: stopped, track: {}", track_id.to_base62()); + info!( + "event: stopped, track: {}", + track_id.to_base62().unwrap_or_default() + ); command = r#"["spottyconnect","stop"]"#.to_string(); } PlayerEvent::Playing { @@ -206,7 +212,7 @@ impl LMS { #[cfg(debug_assertions)] info!( "event: playing, track: {}, duration: {}, position: {}", - track_id.to_base62(), + track_id.to_base62().unwrap_or_default(), duration_ms, position_ms ); @@ -226,7 +232,7 @@ impl LMS { #[cfg(debug_assertions)] info!( "event: paused, track: {}, duration: {}, position: {}", - track_id.to_base62(), + track_id.to_base62().unwrap_or_default(), duration_ms, position_ms ); From 03e71f6e0a94265c3a3106e34a87264cc9e825fd Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Thu, 27 Jan 2022 00:40:59 -0600 Subject: [PATCH 69/76] simplify get_factor (#942) Simplify `get_factor` --- CHANGELOG.md | 1 + playback/src/player.rs | 58 ++++++++++++++++++++++++++++++++---------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 748091046..c1a8fd737 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] Don't panic when parsing options. Instead list valid values and exit. - [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`. - [core] Removed unsafe code (breaking) +- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/playback/src/player.rs b/playback/src/player.rs index b2bdea0cd..48129177c 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -31,6 +31,7 @@ use crate::{MS_PER_PAGE, NUM_CHANNELS, PAGES_PER_MS, SAMPLES_PER_SECOND}; const PRELOAD_NEXT_TRACK_BEFORE_END_DURATION_MS: u32 = 30000; pub const DB_VOLTAGE_RATIO: f64 = 20.0; +pub const PCM_AT_0DBFS: f64 = 1.0; pub struct Player { commands: Option>, @@ -251,26 +252,57 @@ impl NormalisationData { (data.track_gain_db, data.track_peak) }; - let normalisation_power = gain_db + config.normalisation_pregain_db; - let mut normalisation_factor = db_to_ratio(normalisation_power); + // As per the ReplayGain 1.0 & 2.0 (proposed) spec: + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention + let normalisation_factor = if config.normalisation_method == NormalisationMethod::Basic { + // For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level). + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude + // https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude + // We then limit that to 1.0 as not to exceed dBFS (0.0 dB). + let factor = f64::min( + db_to_ratio(gain_db + config.normalisation_pregain_db), + PCM_AT_0DBFS / gain_peak, + ); - if normalisation_power + ratio_to_db(gain_peak) > config.normalisation_threshold_dbfs { - let limited_normalisation_factor = - db_to_ratio(config.normalisation_threshold_dbfs) / gain_peak; - let limited_normalisation_power = ratio_to_db(limited_normalisation_factor); + if factor > PCM_AT_0DBFS { + info!( + "Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.", + ratio_to_db(factor) + ); - if config.normalisation_method == NormalisationMethod::Basic { - warn!("Limiting gain to {:.2} dB for the duration of this track to stay under normalisation threshold.", limited_normalisation_power); - normalisation_factor = limited_normalisation_factor; + PCM_AT_0DBFS } else { + factor + } + } else { + // For Dynamic Normalisation it's up to the player to decide, + // factor = ratio of (ReplayGain + PreGain). + // We then let the dynamic limiter handle gain reduction. + let factor = db_to_ratio(gain_db + config.normalisation_pregain_db); + let threshold_ratio = db_to_ratio(config.normalisation_threshold_dbfs); + + if factor > PCM_AT_0DBFS { + let factor_db = gain_db + config.normalisation_pregain_db; + let limiting_db = factor_db + config.normalisation_threshold_dbfs.abs(); + warn!( - "This track will at its peak be subject to {:.2} dB of dynamic limiting.", - normalisation_power - limited_normalisation_power + "This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.", + factor_db, limiting_db + ); + } else if factor > threshold_ratio { + let limiting_db = gain_db + + config.normalisation_pregain_db + + config.normalisation_threshold_dbfs.abs(); + + info!( + "This track may be subject to {:.2} dB of dynamic limiting at it's peak.", + limiting_db ); } - warn!("Please lower pregain to avoid."); - } + factor + }; debug!("Normalisation Data: {:?}", data); debug!( From c8971dce63f24a2e19da07fd5ce52e09bfdcf40e Mon Sep 17 00:00:00 2001 From: Roderick van Domburg Date: Thu, 27 Jan 2022 18:39:28 +0100 Subject: [PATCH 70/76] Fix Alsa softvol linear mapping (#950) Use `--volume-range` overrides --- CHANGELOG.md | 1 + playback/src/mixer/alsamixer.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1a8fd737..24d1a17a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [main] `--alsa-mixer-device` and `--alsa-mixer-index` now fallback to the card and index specified in `--device`. - [core] Removed unsafe code (breaking) - [playback] Adhere to ReplayGain spec when calculating gain normalisation factor. +- [playback] `alsa`: Use `--volume-range` overrides for softvol controls ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. diff --git a/playback/src/mixer/alsamixer.rs b/playback/src/mixer/alsamixer.rs index 55398cb75..c04e6ee8f 100644 --- a/playback/src/mixer/alsamixer.rs +++ b/playback/src/mixer/alsamixer.rs @@ -84,7 +84,7 @@ impl Mixer for AlsaMixer { warn!("Alsa rounding error detected, setting maximum dB to {:.2} instead of {:.2}", ZERO_DB.to_db(), max_millibel.to_db()); max_millibel = ZERO_DB; } else { - warn!("Please manually set with `--volume-ctrl` if this is incorrect"); + warn!("Please manually set `--volume-range` if this is incorrect"); } } (min_millibel, max_millibel) @@ -104,12 +104,23 @@ impl Mixer for AlsaMixer { let min_db = min_millibel.to_db() as f64; let max_db = max_millibel.to_db() as f64; - let db_range = f64::abs(max_db - min_db); + let mut db_range = f64::abs(max_db - min_db); // Synchronize the volume control dB range with the mixer control, // unless it was already set with a command line option. if !config.volume_ctrl.range_ok() { + if db_range > 100.0 { + debug!("Alsa mixer reported dB range > 100, which is suspect"); + warn!("Please manually set `--volume-range` if this is incorrect"); + } config.volume_ctrl.set_db_range(db_range); + } else { + let db_range_override = config.volume_ctrl.db_range(); + debug!( + "Alsa dB volume range was detected as {} but overridden as {}", + db_range, db_range_override + ); + db_range = db_range_override; } // For hardware controls with a small range (24 dB or less), From 3c749a8f0e66df865701bbfa36df37e8ef258b9e Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Thu, 27 Jan 2022 17:00:08 -0600 Subject: [PATCH 71/76] Remove basic normalisation deprecation warning --- src/main.rs | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/src/main.rs b/src/main.rs index f43500fee..c70f39dac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1293,12 +1293,7 @@ fn get_setup() -> Setup { normalisation_method = opt_str(NORMALISATION_METHOD) .as_deref() .map(|method| { - warn!( - "`--{}` / `-{}` will be deprecated in a future release.", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT - ); - - let method = NormalisationMethod::from_str(method).unwrap_or_else(|_| { + NormalisationMethod::from_str(method).unwrap_or_else(|_| { invalid_error_msg( NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, @@ -1308,16 +1303,7 @@ fn get_setup() -> Setup { ); exit(1); - }); - - if matches!(method, NormalisationMethod::Basic) { - warn!( - "`--{}` / `-{}` {:?} will be deprecated in a future release.", - NORMALISATION_METHOD, NORMALISATION_METHOD_SHORT, method - ); - } - - method + }) }) .unwrap_or(player_default_config.normalisation_method); From edb98d5c1dce233f30ac4781bf74a939701291e9 Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Mon, 31 Jan 2022 18:20:10 -0600 Subject: [PATCH 72/76] Prevent shuffle crash fixes https://github.com/librespot-org/librespot/issues/959 --- connect/src/spirc.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/connect/src/spirc.rs b/connect/src/spirc.rs index 344f63b75..b574ff523 100644 --- a/connect/src/spirc.rs +++ b/connect/src/spirc.rs @@ -668,15 +668,15 @@ impl SpircTask { self.state.set_shuffle(frame.get_state().get_shuffle()); if self.state.get_shuffle() { let current_index = self.state.get_playing_track_index(); - { - let tracks = self.state.mut_track(); + let tracks = self.state.mut_track(); + if !tracks.is_empty() { tracks.swap(0, current_index as usize); if let Some((_, rest)) = tracks.split_first_mut() { let mut rng = rand::thread_rng(); rest.shuffle(&mut rng); } + self.state.set_playing_track_index(0); } - self.state.set_playing_track_index(0); } else { let context = self.state.get_context_uri(); debug!("{:?}", context); From d54f3982a0b265e11270d2743d847853727dc67f Mon Sep 17 00:00:00 2001 From: JasonLG1979 Date: Tue, 1 Feb 2022 17:48:13 -0600 Subject: [PATCH 73/76] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24d1a17a1..6db058bda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [core] Removed unsafe code (breaking) - [playback] Adhere to ReplayGain spec when calculating gain normalisation factor. - [playback] `alsa`: Use `--volume-range` overrides for softvol controls +- [connect] Don't panic when activating shuffle without previous interaction. ### Removed - [playback] `alsamixer`: previously deprecated option `mixer-card` has been removed. From 16b7e7ad1b4ae2572f3b4c0741c5d16f56a48c19 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Mon, 7 Feb 2022 18:03:37 +0100 Subject: [PATCH 74/76] Improved error handling when fetching tokens or track data. --- src/spotty.rs | 76 +++++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/src/spotty.rs b/src/spotty.rs index 14e6ed8c0..290e60c84 100644 --- a/src/spotty.rs +++ b/src/spotty.rs @@ -1,6 +1,6 @@ use hyper::{Body, Client, Method, Request}; #[allow(unused)] -use log::{info, warn}; +use log::{error, info, warn}; use serde_json::json; use std::fs; @@ -57,25 +57,32 @@ pub async fn get_token( if let Some(client_id) = client_id { let scopes = scopes.unwrap_or(SCOPES.to_string()); - let session = Session::connect(session_config, last_credentials, None) - .await - .unwrap(); - - let token = keymaster::get_token(&session, &client_id, &scopes) - .await - .unwrap(); - - let json_token = json!({ - // keep backwards compatibility with older versions - "accessToken": token.access_token.to_string(), - "expiresIn": token.expires_in, - }); - - if let Some(save_token) = save_token { - fs::write(save_token.to_string(), format!("{}", json_token)) - .expect("Can't write token file"); - } else { - println!("{}", json_token); + match Session::connect(session_config, last_credentials, None).await { + Ok(session) => { + match keymaster::get_token(&session, &client_id, &scopes).await { + Ok(token) => { + let json_token = json!({ + "accessToken": token.access_token.to_string(), + "expiresIn": token.expires_in, + }); + + if let Some(save_token) = save_token { + fs::write(save_token.to_string(), format!("{}", json_token)) + .expect("Can't write token file"); + } else { + println!("{}", json_token); + } + } + Err(error) => { + error!("Failed to fetch token: {:?}", error); + println!("{{ \"error\": \"Failed to get token.\" }}"); + } + } + } + Err(error) => { + error!("Failed to create session: {:?}", error); + println!("{{ \"error\": \"Failed to create session.\" }}"); + } } } else { println!("Use --client-id to provide a CLIENT_ID"); @@ -108,20 +115,23 @@ pub async fn play_track( ); match track { - Ok(track) => { - let session = Session::connect(session_config, last_credentials, None) - .await - .unwrap(); - - let (mut player, _) = Player::new(player_config, session, None, move || { - backend(None, audio_format) - }); - - player.load(track, true, start_position); - player.await_end_of_track().await; - } + Ok(track) => match Session::connect(session_config, last_credentials, None).await { + Ok(session) => { + let (mut player, _) = + Player::new(player_config, session, None, move || { + backend(None, audio_format) + }); + + player.load(track, true, start_position); + player.await_end_of_track().await; + } + Err(error) => { + error!("Failed to create session: {:?}", error); + return; + } + }, Err(error) => { - warn!("Problem getting a Spotify ID for {}: {:?}", track_id, error); + error!("Problem getting a Spotify ID for {}: {:?}", track_id, error); return; } }; From 6f6d330bce17061ea5960a593f5c0579a0a822f6 Mon Sep 17 00:00:00 2001 From: Michael Herger Date: Mon, 7 Feb 2022 23:37:29 +0100 Subject: [PATCH 75/76] Only log runtime argument if it starts with a dash "-" When there's a value that corresponds to an argument name used in the same command line, the logging of the arguments gets confused and logs the matching argument, but without the leading dash: ``` % target/debug/librespot -n c --verbose -c /path/to/my/cache [2022-02-07T22:32:25Z INFO librespot] librespot 0.3.1 55ced49 (Built on 2022-02-07, Build ID: qaEB8kEW, Profile: debug) [2022-02-07T22:32:25Z TRACE librespot] Command line argument(s): [2022-02-07T22:32:25Z TRACE librespot] -n "c" [2022-02-07T22:32:25Z TRACE librespot] c "/path/to/my/cache" [2022-02-07T22:32:25Z TRACE librespot] --verbose [2022-02-07T22:32:25Z TRACE librespot] -c "/path/to/my/cache" ``` Here we're using the literal `c` as the device name, and the `-c` argument. Thus the `c /path/to/my/cache` is logged in addition to `-c /path...`. After checking whether the key has a leading dash, this issue is gone: ``` % target/debug/librespot -n c --verbose -c /path/to/my/cache [2022-02-07T22:32:41Z INFO librespot] librespot 0.3.1 55ced49 (Built on 2022-02-07, Build ID: qaEB8kEW, Profile: debug) [2022-02-07T22:32:41Z TRACE librespot] Command line argument(s): [2022-02-07T22:32:41Z TRACE librespot] -n "c" [2022-02-07T22:32:41Z TRACE librespot] --verbose [2022-02-07T22:32:41Z TRACE librespot] -c "/path/to/my/cache" ``` --- src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.rs b/src/main.rs index c70f39dac..e9969f506 100644 --- a/src/main.rs +++ b/src/main.rs @@ -661,6 +661,7 @@ fn get_setup() -> Setup { let opt = key.trim_start_matches('-'); if index > 0 + && key.starts_with('-') && &args[index - 1] != key && matches.opt_defined(opt) && matches.opt_present(opt) From 616809b64c1d517cd6cf120ab1bc39f2cd7b0a2a Mon Sep 17 00:00:00 2001 From: Jason Gray Date: Sun, 13 Feb 2022 15:50:32 -0600 Subject: [PATCH 76/76] Quantum-realm level normalisation optimization (#965) This saves up to 1-2% CPU useage on a PI 4 depending on how much normalisation is actually being done. * We don't need to test against EPSILON. The factor will never be over 1.0 in basic normalisation mode. * Don't check the normalisation mode EVERY sample. * Do as little math as possible by simplfiying all equations as much as possible (while still retaining the textbook equations in comments). * Misc cleanup --- playback/src/convert.rs | 40 +++++------ playback/src/dither.rs | 6 +- playback/src/player.rs | 151 +++++++++++++++++++++------------------- 3 files changed, 98 insertions(+), 99 deletions(-) diff --git a/playback/src/convert.rs b/playback/src/convert.rs index 962ade66b..1bc8a88e2 100644 --- a/playback/src/convert.rs +++ b/playback/src/convert.rs @@ -23,14 +23,15 @@ pub struct Converter { impl Converter { pub fn new(dither_config: Option) -> Self { - if let Some(ref ditherer_builder) = dither_config { - let ditherer = (ditherer_builder)(); - info!("Converting with ditherer: {}", ditherer.name()); - Self { - ditherer: Some(ditherer), + match dither_config { + Some(ditherer_builder) => { + let ditherer = (ditherer_builder)(); + info!("Converting with ditherer: {}", ditherer.name()); + Self { + ditherer: Some(ditherer), + } } - } else { - Self { ditherer: None } + None => Self { ditherer: None }, } } @@ -52,18 +53,15 @@ impl Converter { const SCALE_S16: f64 = 32768.; pub fn scale(&mut self, sample: f64, factor: f64) -> f64 { - let dither = match self.ditherer { - Some(ref mut d) => d.noise(), - None => 0.0, - }; - // From the many float to int conversion methods available, match what // the reference Vorbis implementation uses: sample * 32768 (for 16 bit) - let int_value = sample * factor + dither; // Casting float to integer rounds towards zero by default, i.e. it // truncates, and that generates larger error than rounding to nearest. - int_value.round() + match self.ditherer.as_mut() { + Some(d) => (sample * factor + d.noise()).round(), + None => (sample * factor).round(), + } } // Special case for samples packed in a word of greater bit depth (e.g. @@ -79,11 +77,12 @@ impl Converter { let max = factor - 1.0; if int_value < min { - return min; + min } else if int_value > max { - return max; + max + } else { + int_value } - int_value } pub fn f64_to_f32(&mut self, samples: &[f64]) -> Vec { @@ -109,12 +108,7 @@ impl Converter { pub fn f64_to_s24_3(&mut self, samples: &[f64]) -> Vec { samples .iter() - .map(|sample| { - // Not as DRY as calling f32_to_s24 first, but this saves iterating - // over all samples twice. - let int_value = self.clamping_scale(*sample, Self::SCALE_S24) as i32; - i24::from_s24(int_value) - }) + .map(|sample| i24::from_s24(self.clamping_scale(*sample, Self::SCALE_S24) as i32)) .collect() } diff --git a/playback/src/dither.rs b/playback/src/dither.rs index 0f6679171..4b8a427c4 100644 --- a/playback/src/dither.rs +++ b/playback/src/dither.rs @@ -3,7 +3,7 @@ use rand::SeedableRng; use rand_distr::{Distribution, Normal, Triangular, Uniform}; use std::fmt; -const NUM_CHANNELS: usize = 2; +use crate::NUM_CHANNELS; // Dithering lowers digital-to-analog conversion ("requantization") error, // linearizing output, lowering distortion and replacing it with a constant, @@ -102,7 +102,7 @@ impl GaussianDitherer { pub struct HighPassDitherer { active_channel: usize, - previous_noises: [f64; NUM_CHANNELS], + previous_noises: [f64; NUM_CHANNELS as usize], cached_rng: SmallRng, distribution: Uniform, } @@ -111,7 +111,7 @@ impl Ditherer for HighPassDitherer { fn new() -> Self { Self { active_channel: 0, - previous_noises: [0.0; NUM_CHANNELS], + previous_noises: [0.0; NUM_CHANNELS as usize], cached_rng: create_rng(), distribution: Uniform::new_inclusive(-0.5, 0.5), // 1 LSB +/- 1 LSB (previous) = 2 LSB } diff --git a/playback/src/player.rs b/playback/src/player.rs index 48129177c..74ba1fc44 100644 --- a/playback/src/player.rs +++ b/playback/src/player.rs @@ -760,7 +760,16 @@ impl PlayerTrackLoader { position_ms: u32, ) -> Option { let audio = match AudioItem::get_audio_item(&self.session, spotify_id).await { - Ok(audio) => audio, + Ok(audio) => match self.find_available_alternative(audio).await { + Some(audio) => audio, + None => { + warn!( + "<{}> is not available", + spotify_id.to_uri().unwrap_or_default() + ); + return None; + } + }, Err(e) => { error!("Unable to load audio item: {:?}", e); return None; @@ -769,17 +778,6 @@ impl PlayerTrackLoader { info!("Loading <{}> with Spotify URI <{}>", audio.name, audio.uri); - let audio = match self.find_available_alternative(audio).await { - Some(audio) => audio, - None => { - warn!( - "<{}> is not available", - spotify_id.to_uri().unwrap_or_default() - ); - return None; - } - }; - if audio.duration < 0 { error!( "Track duration for <{}> cannot be {}", @@ -809,26 +807,24 @@ impl PlayerTrackLoader { ], }; - let entry = formats.iter().find_map(|format| { - if let Some(&file_id) = audio.files.get(format) { - Some((*format, file_id)) - } else { - None - } - }); - - let (format, file_id) = match entry { - Some(t) => t, - None => { - warn!("<{}> is not available in any supported format", audio.name); - return None; - } - }; + let (format, file_id) = + match formats + .iter() + .find_map(|format| match audio.files.get(format) { + Some(&file_id) => Some((*format, file_id)), + _ => None, + }) { + Some(t) => t, + None => { + warn!("<{}> is not available in any supported format", audio.name); + return None; + } + }; let bytes_per_second = self.stream_data_rate(format); let play_from_beginning = position_ms == 0; - // This is only a loop to be able to reload the file if an error occured + // This is only a loop to be able to reload the file if an error occurred // while opening a cached file. loop { let encrypted_file = AudioFile::open( @@ -1321,25 +1317,30 @@ impl PlayerInternal { // For the basic normalisation method, a normalisation factor of 1.0 indicates that // there is nothing to normalise (all samples should pass unaltered). For the // dynamic method, there may still be peaks that we want to shave off. - if self.config.normalisation - && !(f64::abs(normalisation_factor - 1.0) <= f64::EPSILON - && self.config.normalisation_method == NormalisationMethod::Basic) - { - // zero-cost shorthands - let threshold_db = self.config.normalisation_threshold_dbfs; - let knee_db = self.config.normalisation_knee_db; - let attack_cf = self.config.normalisation_attack_cf; - let release_cf = self.config.normalisation_release_cf; - - for sample in data.iter_mut() { - *sample *= normalisation_factor; // for both the basic and dynamic limiter - - // Feedforward limiter in the log domain - // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic - // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio - // Engineering Society, 60, 399-408. - if self.config.normalisation_method == NormalisationMethod::Dynamic - { + if self.config.normalisation { + if self.config.normalisation_method == NormalisationMethod::Basic + && normalisation_factor < 1.0 + { + for sample in data.iter_mut() { + *sample *= normalisation_factor; + } + } else if self.config.normalisation_method + == NormalisationMethod::Dynamic + { + // zero-cost shorthands + let threshold_db = self.config.normalisation_threshold_dbfs; + let knee_db = self.config.normalisation_knee_db; + let attack_cf = self.config.normalisation_attack_cf; + let release_cf = self.config.normalisation_release_cf; + + for sample in data.iter_mut() { + *sample *= normalisation_factor; + + // Feedforward limiter in the log domain + // After: Giannoulis, D., Massberg, M., & Reiss, J.D. (2012). Digital Dynamic + // Range Compressor Design—A Tutorial and Analysis. Journal of The Audio + // Engineering Society, 60, 399-408. + // Some tracks have samples that are precisely 0.0. That's silence // and we know we don't need to limit that, in which we can spare // the CPU cycles. @@ -1348,22 +1349,26 @@ impl PlayerInternal { // peak detector stuck. Also catch the unlikely case where a sample // is decoded as `NaN` or some other non-normal value. let limiter_db = if sample.is_normal() { - // step 1-2: half-wave rectification and conversion into dB - let abs_sample_db = ratio_to_db(sample.abs()); - - // step 3-4: gain computer with soft knee and subtractor - let bias_db = abs_sample_db - threshold_db; + // step 1-4: half-wave rectification and conversion into dB + // and gain computer with soft knee and subtractor + let bias_db = ratio_to_db(sample.abs()) - threshold_db; let knee_boundary_db = bias_db * 2.0; if knee_boundary_db < -knee_db { 0.0 } else if knee_boundary_db.abs() <= knee_db { - abs_sample_db - - (abs_sample_db - - (bias_db + knee_db / 2.0).powi(2) - / (2.0 * knee_db)) + // The textbook equation: + // ratio_to_db(sample.abs()) - (ratio_to_db(sample.abs()) - (bias_db + knee_db / 2.0).powi(2) / (2.0 * knee_db)) + // Simplifies to: + // ((2.0 * bias_db) + knee_db).powi(2) / (8.0 * knee_db) + // Which in our case further simplifies to: + // (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) + // because knee_boundary_db is 2.0 * bias_db. + (knee_boundary_db + knee_db).powi(2) / (8.0 * knee_db) } else { - abs_sample_db - threshold_db + // Textbook: + // ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db. + bias_db } } else { 0.0 @@ -1377,14 +1382,24 @@ impl PlayerInternal { || self.normalisation_peak > 0.0 { // step 5: smooth, decoupled peak detector + // Textbook: + // release_cf * self.normalisation_integrator + (1.0 - release_cf) * limiter_db + // Simplifies to: + // release_cf * self.normalisation_integrator - release_cf * limiter_db + limiter_db self.normalisation_integrator = f64::max( limiter_db, release_cf * self.normalisation_integrator - + (1.0 - release_cf) * limiter_db, + - release_cf * limiter_db + + limiter_db, ); + // Textbook: + // attack_cf * self.normalisation_peak + (1.0 - attack_cf) * self.normalisation_integrator + // Simplifies to: + // attack_cf * self.normalisation_peak - attack_cf * self.normalisation_integrator + self.normalisation_integrator self.normalisation_peak = attack_cf * self.normalisation_peak - + (1.0 - attack_cf) * self.normalisation_integrator; + - attack_cf * self.normalisation_integrator + + self.normalisation_integrator; // step 6: make-up gain applied later (volume attenuation) // Applying the standard normalisation factor here won't work, @@ -1897,15 +1912,8 @@ impl PlayerInternal { } fn send_event(&mut self, event: PlayerEvent) { - let mut index = 0; - while index < self.event_senders.len() { - match self.event_senders[index].send(event.clone()) { - Ok(_) => index += 1, - Err(_) => { - self.event_senders.remove(index); - } - } - } + self.event_senders + .retain(|sender| sender.send(event.clone()).is_ok()); } fn load_track( @@ -2079,10 +2087,7 @@ impl Seek for Subfile { }; let newpos = self.stream.seek(pos)?; - if newpos > self.offset { - Ok(newpos - self.offset) - } else { - Ok(0) - } + + Ok(newpos.saturating_sub(self.offset)) } }