mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Merge branch 'dev' into new-api and update crates
This commit is contained in:
commit
05b9b13cf8
41 changed files with 975 additions and 804 deletions
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
|
@ -191,7 +191,7 @@ jobs:
|
|||
- name: Install cross
|
||||
run: cargo install cross || true
|
||||
- name: Build
|
||||
run: cross build --locked --target ${{ matrix.target }} --no-default-features
|
||||
run: cross build --target ${{ matrix.target }} --no-default-features
|
||||
|
||||
clippy:
|
||||
needs: [test-cross-arm, test-windows]
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -5,5 +5,4 @@ spotify_appkey.key
|
|||
.project
|
||||
.history
|
||||
*.save
|
||||
|
||||
|
||||
*.*~
|
||||
|
|
84
CHANGELOG.md
84
CHANGELOG.md
|
@ -8,38 +8,64 @@ 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.
|
||||
- [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).
|
||||
- [playback] `pipe`: create file if it doesn't already exist
|
||||
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
|
||||
- [playback] `subprocess`: Better error handling
|
||||
- [playback] `pipe`: Better error handling
|
||||
|
||||
### 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.
|
||||
- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence). (breaking)
|
||||
- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence). (breaking)
|
||||
- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence). (breaking)
|
||||
- [playback] `pipe`: Implement stop
|
||||
|
||||
### 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`.
|
||||
- [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.
|
||||
- [playback] `alsamixer`: make `--volume-ctrl fixed` work as expected when combined with `--mixer alsa`
|
||||
- [main] fix `--opt=value` line argument logging
|
||||
|
||||
### 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.4.1] - 2022-05-23
|
||||
|
||||
### Changed
|
||||
- [chore] The MSRV is now 1.56
|
||||
|
||||
### Fixed
|
||||
- [playback] Fixed dependency issues when installing from crate
|
||||
|
||||
## [0.4.0] - 2022-05-21
|
||||
|
||||
### Changed
|
||||
- [chore] The MSRV is now 1.53
|
||||
- [contrib] Hardened security of the `systemd` service units
|
||||
- [core] `Session`: `connect()` now returns the long-term credentials
|
||||
- [core] `Session`: `connect()` now accepts a flag if the credentails should be stored via the cache
|
||||
- [main] Different option descriptions and error messages based on what backends are enabled at build time
|
||||
- [playback] More robust dynamic limiter for very wide dynamic range (breaking)
|
||||
- [playback] `alsa`: improve `--device ?` output for the Alsa backend
|
||||
- [playback] `gstreamer`: create own context, set correct states and use sync handler
|
||||
- [playback] `pipe`: create file if it doesn't already exist
|
||||
- [playback] `Sink`: `write()` now receives ownership of the packet (breaking)
|
||||
|
||||
### Added
|
||||
- [main] Enforce reasonable ranges for option values (breaking)
|
||||
- [main] Add the ability to parse environment variables
|
||||
- [main] Log now emits warning when trying to use options that would otherwise have no effect
|
||||
- [main] Verbose logging now logs all parsed environment variables and command line arguments (credentials are redacted)
|
||||
- [main] Add a `-q`, `--quiet` option that changes the logging level to WARN
|
||||
- [main] Add `disable-credential-cache` flag (breaking)
|
||||
- [main] Add a short name for every flag and option
|
||||
- [playback] `pulseaudio`: set the PulseAudio name to match librespot's device name via `PULSE_PROP_application.name` environment variable (user set env var value takes precedence) (breaking)
|
||||
- [playback] `pulseaudio`: set icon to `audio-x-generic` so we get an icon instead of a placeholder via `PULSE_PROP_application.icon_name` environment variable (user set env var value takes precedence) (breaking)
|
||||
- [playback] `pulseaudio`: set values to: `PULSE_PROP_application.version`, `PULSE_PROP_application.process.binary`, `PULSE_PROP_stream.description`, `PULSE_PROP_media.software` and `PULSE_PROP_media.role` environment variables (user set env var values take precedence) (breaking)
|
||||
|
||||
### Fixed
|
||||
- [connect] Don't panic when activating shuffle without previous interaction
|
||||
- [core] Removed unsafe code (breaking)
|
||||
- [main] Fix crash when built with Avahi support but Avahi is locally unavailable
|
||||
- [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`.
|
||||
- [playback] Adhere to ReplayGain spec when calculating gain normalisation factor
|
||||
- [playback] `alsa`: make `--volume-range` overrides apply to Alsa softvol controls
|
||||
|
||||
### Removed
|
||||
- [playback] `alsamixer`: previously deprecated options `mixer-card`, `mixer-name` and `mixer-index` have been removed
|
||||
|
||||
## [0.3.1] - 2021-10-24
|
||||
|
||||
|
@ -90,7 +116,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] 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
|
||||
- [playback] `alsamixer`: make `cubic` consistent between cards that report minimum volume as mute, and cards that report some dB value
|
||||
|
@ -114,7 +140,9 @@ 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.1..HEAD
|
||||
[unreleased]: https://github.com/librespot-org/librespot/compare/v0.4.1..HEAD
|
||||
[0.4.1]: https://github.com/librespot-org/librespot/compare/v0.4.0..v0.4.1
|
||||
[0.4.0]: https://github.com/librespot-org/librespot/compare/v0.3.1..v0.4.0
|
||||
[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
|
||||
|
|
|
@ -7,7 +7,11 @@ In order to compile librespot, you will first need to set up a suitable Rust bui
|
|||
### Install Rust
|
||||
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.
|
||||
|
||||
<<<<<<< HEAD
|
||||
*Note: The current minimum required Rust version at the time of writing is 1.56.*
|
||||
=======
|
||||
*Note: The current minimum required Rust version at the time of writing is 1.56, you can find the current minimum version specified in the `.github/workflow/test.yml` file.*
|
||||
>>>>>>> dev
|
||||
|
||||
#### Additional Rust tools - `rustfmt`
|
||||
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:
|
||||
|
|
815
Cargo.lock
generated
815
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
20
Cargo.toml
20
Cargo.toml
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot"
|
||||
version = "0.3.1"
|
||||
version = "0.4.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.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-connect]
|
||||
path = "connect"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-discovery]
|
||||
path = "discovery"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-metadata]
|
||||
path = "metadata"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-playback]
|
||||
path = "playback"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-protocol]
|
||||
path = "protocol"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies]
|
||||
env_logger = { version = "0.9", default-features = false, features = ["termcolor", "humantime", "atty"] }
|
||||
|
@ -54,8 +54,8 @@ futures-util = { version = "0.3", default_features = false }
|
|||
getopts = "0.2"
|
||||
hex = "0.4"
|
||||
log = "0.4"
|
||||
rpassword = "5.0"
|
||||
sha-1 = "0.10"
|
||||
rpassword = "7.0"
|
||||
sha1 = "0.10"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["rt", "macros", "signal", "sync", "parking_lot", "process"] }
|
||||
url = "2.2"
|
||||
|
|
|
@ -2,7 +2,17 @@
|
|||
|
||||
## How To
|
||||
|
||||
The bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script.
|
||||
Read through this paragraph in its entirety before running anything.
|
||||
|
||||
The Bash script in the root of the project, named `publish.sh` can be used to publish a new version of librespot and it's corresponding crates. the command should be used as follows from the project root: `./publish 0.1.0` from the project root, substituting the new version number that you wish to publish. *Note the lack of a v prefix on the version number. This is important, do not add one.* The v prefix is added where appropriate by the script.
|
||||
|
||||
Make sure that you are are starting from a clean working directory for both `dev` and `master`, completely up to date with remote and all local changes either committed and pushed or stashed.
|
||||
|
||||
You will want to perform a dry run first: `./publish --dry-run 0.1.0`. Please make note of any errors or warnings. In particular, you may need to explicitly inform Git which remote you want to track for the `master` branch like so: `git --track origin/master` (or whatever you have called the `librespot-org` remote `master` branch).
|
||||
|
||||
Depending on your system the script may fail to publish the main `librespot` crate after having published all the `librespot-xyz` sub-crates. If so then make sure the working directory is committed and pushed (watch `Cargo.toml`) and then run `cargo publish` manually after `publish.sh` finished.
|
||||
|
||||
To publish the crates your GitHub account needs to be authorized on `crates.io` by `librespot-org`.
|
||||
|
||||
## What the script does
|
||||
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
[package]
|
||||
name = "librespot-audio"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||
description="The audio fetching and processing logic for librespot"
|
||||
description = "The audio fetching logic for librespot"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/librespot-org/librespot"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies]
|
||||
aes = { version = "0.7", features = ["ctr"] }
|
||||
aes = "0.8"
|
||||
byteorder = "1.4"
|
||||
bytes = "1"
|
||||
ctr = "0.9"
|
||||
futures-core = "0.3"
|
||||
futures-util = "0.3"
|
||||
hyper = { version = "0.14", features = ["client"] }
|
||||
log = "0.4"
|
||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
tempfile = "3"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["macros", "parking_lot", "sync"] }
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use std::io;
|
||||
|
||||
use aes::{
|
||||
cipher::{generic_array::GenericArray, NewCipher, StreamCipher, StreamCipherSeek},
|
||||
Aes128Ctr,
|
||||
};
|
||||
use aes::cipher::{KeyIvInit, StreamCipher, StreamCipherSeek};
|
||||
|
||||
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
|
||||
|
||||
use librespot_core::audio_key::AudioKey;
|
||||
|
||||
|
@ -19,12 +18,12 @@ pub struct AudioDecrypt<T: io::Read> {
|
|||
|
||||
impl<T: io::Read> AudioDecrypt<T> {
|
||||
pub fn new(key: Option<AudioKey>, reader: T) -> AudioDecrypt<T> {
|
||||
let cipher = key.map(|key| {
|
||||
Aes128Ctr::new(
|
||||
GenericArray::from_slice(&key.0),
|
||||
GenericArray::from_slice(&AUDIO_AESIV),
|
||||
)
|
||||
});
|
||||
let cipher = if let Some(key) = key {
|
||||
Aes128Ctr::new_from_slices(&key.0, &AUDIO_AESIV).ok()
|
||||
} else {
|
||||
// some files are unencrypted
|
||||
None
|
||||
};
|
||||
|
||||
AudioDecrypt { cipher, reader }
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot-connect"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||
description = "The discovery and Spotify Connect logic for librespot"
|
||||
license = "MIT"
|
||||
|
@ -21,12 +21,12 @@ tokio-stream = "0.1"
|
|||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-playback]
|
||||
path = "../playback"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-protocol]
|
||||
path = "../protocol"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
|
|
@ -331,7 +331,7 @@ impl Spirc {
|
|||
);
|
||||
|
||||
// Connect *after* all message listeners are registered
|
||||
session.connect(credentials).await?;
|
||||
session.connect(credentials, true).await?;
|
||||
|
||||
let canonical_username = &session.username();
|
||||
debug!("canonical_username: {}", canonical_username);
|
||||
|
@ -612,6 +612,7 @@ impl SpircTask {
|
|||
}
|
||||
SpircCommand::Shutdown => {
|
||||
CommandSender::new(self, MessageType::kMessageTypeGoodbye).send()?;
|
||||
self.player.stop();
|
||||
self.shutdown = true;
|
||||
if let Some(rx) = self.commands.as_mut() {
|
||||
rx.close()
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot-core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||
build = "build.rs"
|
||||
description = "The core functionality provided by librespot"
|
||||
|
@ -10,10 +10,10 @@ edition = "2018"
|
|||
|
||||
[dependencies.librespot-protocol]
|
||||
path = "../protocol"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies]
|
||||
aes = "0.7"
|
||||
aes = "0.8"
|
||||
base64 = "0.13"
|
||||
byteorder = "1.4"
|
||||
bytes = "1"
|
||||
|
@ -22,7 +22,7 @@ form_urlencoded = "1.0"
|
|||
futures-core = "0.3"
|
||||
futures-util = { version = "0.3", features = ["alloc", "bilock", "sink", "unstable"] }
|
||||
hmac = "0.12"
|
||||
httparse = "1.5"
|
||||
httparse = "1.7"
|
||||
http = "0.2"
|
||||
hyper = { version = "0.14", features = ["client", "http1", "http2", "tcp"] }
|
||||
hyper-proxy = { version = "0.9", default-features = false, features = ["rustls"] }
|
||||
|
@ -33,30 +33,30 @@ num-bigint = { version = "0.4", features = ["rand"] }
|
|||
num-derive = "0.3"
|
||||
num-integer = "0.1"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.9"
|
||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
||||
pbkdf2 = { version = "0.10", default-features = false, features = ["hmac"] }
|
||||
priority-queue = "1.2.1"
|
||||
once_cell = "1"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
pbkdf2 = { version = "0.11", default-features = false, features = ["hmac"] }
|
||||
priority-queue = "1.2"
|
||||
protobuf = "2"
|
||||
quick-xml = { version = "0.22", features = ["serialize"] }
|
||||
quick-xml = { version = "0.23", features = ["serialize"] }
|
||||
rand = "0.8"
|
||||
rsa = { version = "0.5", default-features = false, features = ["alloc"] }
|
||||
rsa = "0.6"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
sha-1 = "0.10"
|
||||
sha1 = "0.10"
|
||||
shannon = "0.2"
|
||||
thiserror = "1.0"
|
||||
time = "0.3"
|
||||
tokio = { version = "1", features = ["io-util", "macros", "net", "parking_lot", "rt", "sync", "time"] }
|
||||
tokio-stream = "0.1"
|
||||
tokio-tungstenite = { version = "*", default-features = false, features = ["rustls-tls-native-roots"] }
|
||||
tokio-util = { version = "0.6", features = ["codec"] }
|
||||
tokio-util = { version = "0.7", features = ["codec"] }
|
||||
url = "2"
|
||||
uuid = { version = "0.8", default-features = false, features = ["v4"] }
|
||||
uuid = { version = "1", default-features = false, features = ["v4"] }
|
||||
|
||||
[build-dependencies]
|
||||
rand = "0.8"
|
||||
vergen = { version = "6", default-features = false, features = ["build", "git"] }
|
||||
vergen = { version = "7", default-features = false, features = ["build", "git"] }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.9"
|
||||
|
|
|
@ -13,11 +13,14 @@ fn main() {
|
|||
|
||||
vergen(config).expect("Unable to generate the cargo keys!");
|
||||
|
||||
let build_id: String = rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect();
|
||||
let build_id = match std::env::var("SOURCE_DATE_EPOCH") {
|
||||
Ok(val) => val,
|
||||
Err(_) => rand::thread_rng()
|
||||
.sample_iter(Alphanumeric)
|
||||
.take(8)
|
||||
.map(char::from)
|
||||
.collect(),
|
||||
};
|
||||
|
||||
println!("cargo:rustc-env=LIBRESPOT_BUILD_ID={}", build_id);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::io::{self, Read};
|
||||
|
||||
use aes::{Aes192, BlockDecrypt};
|
||||
use aes::Aes192;
|
||||
use byteorder::{BigEndian, ByteOrder};
|
||||
use hmac::Hmac;
|
||||
use pbkdf2::pbkdf2;
|
||||
|
@ -106,13 +106,12 @@ impl Credentials {
|
|||
|
||||
// decrypt data using ECB mode without padding
|
||||
let blob = {
|
||||
use aes::cipher::generic_array::typenum::Unsigned;
|
||||
use aes::cipher::generic_array::GenericArray;
|
||||
use aes::cipher::{BlockCipher, NewBlockCipher};
|
||||
use aes::cipher::{BlockDecrypt, BlockSizeUser, KeyInit};
|
||||
|
||||
let mut data = base64::decode(encrypted_blob)?;
|
||||
let cipher = Aes192::new(GenericArray::from_slice(&key));
|
||||
let block_size = <Aes192 as BlockCipher>::BlockSize::to_usize();
|
||||
let block_size = Aes192::block_size();
|
||||
|
||||
for chunk in data.chunks_exact_mut(block_size) {
|
||||
cipher.decrypt_block(GenericArray::from_mut_slice(chunk));
|
||||
|
|
|
@ -375,7 +375,7 @@ impl Cache {
|
|||
path
|
||||
}),
|
||||
Err(e) => {
|
||||
warn!("{}", e);
|
||||
warn!("Invalid FileId: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ pub struct SessionConfig {
|
|||
|
||||
impl Default for SessionConfig {
|
||||
fn default() -> SessionConfig {
|
||||
let device_id = uuid::Uuid::new_v4().to_hyphenated().to_string();
|
||||
let device_id = uuid::Uuid::new_v4().as_hyphenated().to_string();
|
||||
SessionConfig {
|
||||
client_id: KEYMASTER_CLIENT_ID.to_owned(),
|
||||
device_id,
|
||||
|
|
|
@ -126,7 +126,11 @@ impl Session {
|
|||
}))
|
||||
}
|
||||
|
||||
pub async fn connect(&self, credentials: Credentials) -> Result<(), Error> {
|
||||
pub async fn connect(
|
||||
&self,
|
||||
credentials: Credentials,
|
||||
store_credentials: bool,
|
||||
) -> Result<(), Error> {
|
||||
let ap = self.apresolver().resolve("accesspoint").await?;
|
||||
info!("Connecting to AP \"{}:{}\"", ap.0, ap.1);
|
||||
let mut transport = connection::connect(&ap.0, ap.1, self.config().proxy.as_ref()).await?;
|
||||
|
@ -136,7 +140,9 @@ impl Session {
|
|||
info!("Authenticated as \"{}\" !", reusable_credentials.username);
|
||||
self.set_username(&reusable_credentials.username);
|
||||
if let Some(cache) = self.cache() {
|
||||
cache.save_credentials(&reusable_credentials);
|
||||
if store_credentials {
|
||||
cache.save_credentials(&reusable_credentials);
|
||||
}
|
||||
}
|
||||
|
||||
let (tx_connection, rx_connection) = mpsc::unbounded_channel();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::{
|
||||
convert::TryInto,
|
||||
fmt::Write,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
|
@ -273,7 +274,7 @@ impl SpClient {
|
|||
Some(_) => "&",
|
||||
None => "?",
|
||||
};
|
||||
url.push_str(&format!("{}product=0", separator));
|
||||
let _ = write!(url, "{}product=0", separator);
|
||||
|
||||
let mut request = Request::builder()
|
||||
.method(method)
|
||||
|
@ -464,7 +465,7 @@ impl SpClient {
|
|||
Some(_) => "&",
|
||||
None => "?",
|
||||
};
|
||||
url.push_str(&format!("{}cid={}", separator, self.session().client_id()));
|
||||
let _ = write!(url, "{}cid={}", separator, self.session().client_id());
|
||||
|
||||
self.request_url(url).await
|
||||
}
|
||||
|
|
|
@ -200,7 +200,6 @@ impl SpotifyId {
|
|||
/// character long `String`.
|
||||
///
|
||||
/// [canonically]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn to_base62(&self) -> Result<String, Error> {
|
||||
let mut dst = [0u8; 22];
|
||||
|
@ -258,7 +257,6 @@ impl SpotifyId {
|
|||
/// be encoded as `unknown`.
|
||||
///
|
||||
/// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids
|
||||
|
||||
#[allow(clippy::wrong_self_convention)]
|
||||
pub fn to_uri(&self) -> Result<String, Error> {
|
||||
// 8 chars for the "spotify:" prefix + 1 colon + 22 chars base62 encoded ID = 31
|
||||
|
|
|
@ -7,7 +7,7 @@ use librespot_core::{authentication::Credentials, config::SessionConfig, session
|
|||
#[tokio::test]
|
||||
async fn test_connection() {
|
||||
timeout(Duration::from_secs(30), async {
|
||||
let result = Session::new(SessionConfig::default(), None)
|
||||
let result = Session::new(SessionConfig::default(), None, false)
|
||||
.connect(Credentials::with_password("test", "test"))
|
||||
.await;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot-discovery"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||
description = "The discovery logic for librespot"
|
||||
license = "MIT"
|
||||
|
@ -8,26 +8,27 @@ repository = "https://github.com/librespot-org/librespot"
|
|||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
aes = { version = "0.7", features = ["ctr"] }
|
||||
aes = "0.8"
|
||||
base64 = "0.13"
|
||||
cfg-if = "1.0"
|
||||
ctr = "0.9"
|
||||
dns-sd = { version = "0.1.3", optional = true }
|
||||
form_urlencoded = "1.0"
|
||||
futures-core = "0.3"
|
||||
futures-util = "0.3"
|
||||
hmac = "0.12"
|
||||
hyper = { version = "0.14", features = ["http1", "server", "tcp"] }
|
||||
libmdns = "0.6"
|
||||
libmdns = "0.7"
|
||||
log = "0.4"
|
||||
rand = "0.8"
|
||||
serde_json = "1.0"
|
||||
sha-1 = "0.10"
|
||||
sha1 = "0.10"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1", features = ["parking_lot", "sync", "rt"] }
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3"
|
||||
|
|
|
@ -52,13 +52,13 @@ pub struct Builder {
|
|||
/// Errors that can occur while setting up a [`Discovery`] instance.
|
||||
#[derive(Debug, Error)]
|
||||
pub enum DiscoveryError {
|
||||
/// Setting up service discovery via DNS-SD failed.
|
||||
#[error("Creating SHA1 block cipher failed")]
|
||||
AesError(#[from] aes::cipher::InvalidLength),
|
||||
#[error("Setting up dns-sd failed: {0}")]
|
||||
DnsSdError(#[from] io::Error),
|
||||
/// Setting up the http server failed.
|
||||
#[error("Creating SHA1 HMAC failed for base key {0:?}")]
|
||||
HmacError(Vec<u8>),
|
||||
#[error("Setting up the http server failed: {0}")]
|
||||
#[error("Setting up the HTTP server failed: {0}")]
|
||||
HttpServerError(#[from] hyper::Error),
|
||||
#[error("Missing params for key {0}")]
|
||||
ParamsError(&'static str),
|
||||
|
@ -67,6 +67,7 @@ pub enum DiscoveryError {
|
|||
impl From<DiscoveryError> for Error {
|
||||
fn from(err: DiscoveryError) -> Self {
|
||||
match err {
|
||||
DiscoveryError::AesError(_) => Error::unavailable(err),
|
||||
DiscoveryError::DnsSdError(_) => Error::unavailable(err),
|
||||
DiscoveryError::HmacError(_) => Error::invalid_argument(err),
|
||||
DiscoveryError::HttpServerError(_) => Error::unavailable(err),
|
||||
|
|
|
@ -8,11 +8,7 @@ use std::{
|
|||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use aes::{
|
||||
cipher::generic_array::GenericArray,
|
||||
cipher::{NewCipher, StreamCipher},
|
||||
Aes128Ctr,
|
||||
};
|
||||
use aes::cipher::{KeyIvInit, StreamCipher};
|
||||
use futures_core::Stream;
|
||||
use futures_util::{FutureExt, TryFutureExt};
|
||||
use hmac::{Hmac, Mac};
|
||||
|
@ -32,6 +28,8 @@ use crate::{
|
|||
core::{authentication::Credentials, diffie_hellman::DhLocalKeys, Error},
|
||||
};
|
||||
|
||||
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
|
||||
|
||||
type Params<'a> = BTreeMap<Cow<'a, str>, Cow<'a, str>>;
|
||||
|
||||
pub struct Config {
|
||||
|
@ -151,10 +149,8 @@ impl RequestHandler {
|
|||
|
||||
let decrypted = {
|
||||
let mut data = encrypted.to_vec();
|
||||
let mut cipher = Aes128Ctr::new(
|
||||
GenericArray::from_slice(&encryption_key[0..16]),
|
||||
GenericArray::from_slice(iv),
|
||||
);
|
||||
let mut cipher = Aes128Ctr::new_from_slices(&encryption_key[0..16], iv)
|
||||
.map_err(DiscoveryError::AesError)?;
|
||||
cipher.apply_keystream(&mut data);
|
||||
data
|
||||
};
|
||||
|
|
|
@ -19,7 +19,7 @@ async fn main() {
|
|||
let credentials = Credentials::with_password(&args[1], &args[2]);
|
||||
let session = Session::new(session_config, None);
|
||||
|
||||
match session.connect(credentials).await {
|
||||
match session.connect(credentials, false).await {
|
||||
Ok(()) => println!(
|
||||
"Token: {:#?}",
|
||||
session.token_provider().get_token(SCOPES).await.unwrap()
|
||||
|
|
|
@ -7,6 +7,7 @@ use librespot::{
|
|||
playback::{
|
||||
audio_backend,
|
||||
config::{AudioFormat, PlayerConfig},
|
||||
mixer::NoOpVolume,
|
||||
player::Player,
|
||||
},
|
||||
};
|
||||
|
@ -30,12 +31,12 @@ async fn main() {
|
|||
|
||||
println!("Connecting...");
|
||||
let session = Session::new(session_config, None);
|
||||
if let Err(e) = session.connect(credentials).await {
|
||||
if let Err(e) = session.connect(credentials, false).await {
|
||||
println!("Error connecting: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
let (mut player, _) = Player::new(player_config, session, None, move || {
|
||||
let (mut player, _) = Player::new(player_config, session, Box::new(NoOpVolume), move || {
|
||||
backend(None, audio_format)
|
||||
});
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ async fn main() {
|
|||
});
|
||||
|
||||
let session = Session::new(session_config, None);
|
||||
if let Err(e) = session.connect(credentials).await {
|
||||
if let Err(e) = session.connect(credentials, false).await {
|
||||
println!("Error connecting: {}", e);
|
||||
exit(1);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot-metadata"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Paul Lietar <paul@lietar.net>"]
|
||||
description = "The metadata logic for librespot"
|
||||
license = "MIT"
|
||||
|
@ -14,12 +14,12 @@ bytes = "1"
|
|||
log = "0.4"
|
||||
protobuf = "2"
|
||||
thiserror = "1"
|
||||
uuid = { version = "0.8", default-features = false }
|
||||
uuid = { version = "1", default-features = false }
|
||||
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies.librespot-protocol]
|
||||
path = "../protocol"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
|
||||
use crate::MetadataError;
|
||||
|
||||
use librespot_core::{Error, Session};
|
||||
|
@ -13,10 +15,10 @@ pub trait MercuryRequest {
|
|||
Some(_) => "&",
|
||||
None => "?",
|
||||
};
|
||||
metrics_uri.push_str(&format!("{}country={}", separator, session.country()));
|
||||
let _ = write!(metrics_uri, "{}country={}", separator, session.country());
|
||||
|
||||
if let Some(product) = session.get_user_attribute("type") {
|
||||
metrics_uri.push_str(&format!("&product={}", product));
|
||||
let _ = write!(metrics_uri, "&product={}", product);
|
||||
}
|
||||
|
||||
trace!("Requesting {}", metrics_uri);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot-playback"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Sasha Hilton <sashahilton00@gmail.com>"]
|
||||
description = "The audio playback logic for librespot"
|
||||
license = "MIT"
|
||||
|
@ -9,21 +9,21 @@ edition = "2018"
|
|||
|
||||
[dependencies.librespot-audio]
|
||||
path = "../audio"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
[dependencies.librespot-core]
|
||||
path = "../core"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
[dependencies.librespot-metadata]
|
||||
path = "../metadata"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1"
|
||||
futures-util = "0.3"
|
||||
log = "0.4"
|
||||
parking_lot = { version = "0.11", features = ["deadlock_detection"] }
|
||||
shell-words = "1.0.0"
|
||||
thiserror = "1.0"
|
||||
parking_lot = { version = "0.12", features = ["deadlock_detection"] }
|
||||
shell-words = "1.1"
|
||||
thiserror = "1"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt", "rt-multi-thread", "sync"] }
|
||||
zerocopy = "0.6"
|
||||
|
||||
|
@ -32,11 +32,11 @@ alsa = { version = "0.6", optional = true }
|
|||
portaudio-rs = { version = "0.3", optional = true }
|
||||
libpulse-binding = { version = "2", optional = true, default-features = false }
|
||||
libpulse-simple-binding = { version = "2", optional = true, default-features = false }
|
||||
jack = { version = "0.8", optional = true }
|
||||
jack = { version = "0.10", optional = true }
|
||||
sdl2 = { version = "0.35", optional = true }
|
||||
gst = { package = "gstreamer", version = "0.18", optional = true }
|
||||
gst-app = { package = "gstreamer-app", version = "0.18", optional = true }
|
||||
gst-audio = { package = "gstreamer-audio", version = "0.18", optional = true }
|
||||
gstreamer = { version = "0.18", optional = true }
|
||||
gstreamer-app = { version = "0.18", optional = true }
|
||||
gstreamer-audio = { version = "0.18", optional = true }
|
||||
glib = { version = "0.15", optional = true }
|
||||
|
||||
# Rodio dependencies
|
||||
|
@ -61,6 +61,6 @@ jackaudio-backend = ["jack"]
|
|||
rodio-backend = ["rodio", "cpal"]
|
||||
rodiojack-backend = ["rodio", "cpal/jack"]
|
||||
sdl-backend = ["sdl2"]
|
||||
gstreamer-backend = ["gst", "gst-app", "gst-audio", "glib"]
|
||||
gstreamer-backend = ["gstreamer", "gstreamer-app", "gstreamer-audio", "glib"]
|
||||
|
||||
passthrough-decoder = ["ogg"]
|
||||
|
|
|
@ -6,7 +6,6 @@ use crate::{NUM_CHANNELS, SAMPLE_RATE};
|
|||
use alsa::device_name::HintIter;
|
||||
use alsa::pcm::{Access, Format, Frames, HwParams, PCM};
|
||||
use alsa::{Direction, ValueOr};
|
||||
use std::cmp::min;
|
||||
use std::process::exit;
|
||||
use thiserror::Error;
|
||||
|
||||
|
@ -467,7 +466,7 @@ impl SinkAsBytes for AlsaSink {
|
|||
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 data_to_buffer = data_left.min(space_left);
|
||||
let end_index = start_index + data_to_buffer;
|
||||
|
||||
self.period_buffer
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
use gst::{
|
||||
use gstreamer::{
|
||||
event::{FlushStart, FlushStop},
|
||||
prelude::*,
|
||||
State,
|
||||
};
|
||||
|
||||
use gstreamer as gst;
|
||||
use gstreamer_app as gst_app;
|
||||
use gstreamer_audio as gst_audio;
|
||||
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -41,7 +45,7 @@ impl Open for GstreamerSink {
|
|||
let gst_caps = gst_info.to_caps().expect("Failed to create GStreamer caps");
|
||||
|
||||
let sample_size = format.size();
|
||||
let gst_bytes = NUM_CHANNELS as usize * 1024 * sample_size;
|
||||
let gst_bytes = NUM_CHANNELS as usize * 2048 * sample_size;
|
||||
|
||||
let pipeline = gst::Pipeline::new(None);
|
||||
let appsrc = gst::ElementFactory::make("appsrc", None)
|
||||
|
|
|
@ -2,9 +2,38 @@ use super::{Open, Sink, SinkAsBytes, SinkError, SinkResult};
|
|||
use crate::config::AudioFormat;
|
||||
use crate::convert::Converter;
|
||||
use crate::decoder::AudioPacket;
|
||||
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::{self, Write};
|
||||
use std::process::exit;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum StdoutError {
|
||||
#[error("<StdoutSink> {0}")]
|
||||
OnWrite(std::io::Error),
|
||||
|
||||
#[error("<StdoutSink> File Path {file} Can Not be Opened and/or Created, {e}")]
|
||||
OpenFailure { file: String, e: std::io::Error },
|
||||
|
||||
#[error("<StdoutSink> Failed to Flush the Output Stream, {0}")]
|
||||
FlushFailure(std::io::Error),
|
||||
|
||||
#[error("<StdoutSink> The Output Stream is None")]
|
||||
NoOutput,
|
||||
}
|
||||
|
||||
impl From<StdoutError> for SinkError {
|
||||
fn from(e: StdoutError) -> SinkError {
|
||||
use StdoutError::*;
|
||||
let es = e.to_string();
|
||||
match e {
|
||||
FlushFailure(_) | OnWrite(_) => SinkError::OnWrite(es),
|
||||
OpenFailure { .. } => SinkError::ConnectionRefused(es),
|
||||
NoOutput => SinkError::NotConnected(es),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct StdoutSink {
|
||||
output: Option<Box<dyn Write>>,
|
||||
|
@ -15,13 +44,12 @@ pub struct StdoutSink {
|
|||
impl Open for StdoutSink {
|
||||
fn open(file: Option<String>, 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}}");
|
||||
println!("\nUsage:\n\nOutput to stdout:\n\n\t--backend pipe\n\nOutput to file:\n\n\t--backend pipe --device {{filename}}\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
info!("Using pipe sink with format: {:?}", format);
|
||||
info!("Using StdoutSink (pipe) with format: {:?}", format);
|
||||
|
||||
Self {
|
||||
output: None,
|
||||
file,
|
||||
|
@ -32,21 +60,31 @@ impl Open for StdoutSink {
|
|||
|
||||
impl Sink for StdoutSink {
|
||||
fn start(&mut self) -> SinkResult<()> {
|
||||
if self.output.is_none() {
|
||||
let output: Box<dyn Write> = match self.file.as_deref() {
|
||||
Some(file) => {
|
||||
let open_op = OpenOptions::new()
|
||||
self.output.get_or_insert({
|
||||
match self.file.as_deref() {
|
||||
Some(file) => Box::new(
|
||||
OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(file)
|
||||
.map_err(|e| SinkError::ConnectionRefused(e.to_string()))?;
|
||||
Box::new(open_op)
|
||||
}
|
||||
.map_err(|e| StdoutError::OpenFailure {
|
||||
file: file.to_string(),
|
||||
e,
|
||||
})?,
|
||||
),
|
||||
None => Box::new(io::stdout()),
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
self.output = Some(output);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> SinkResult<()> {
|
||||
self.output
|
||||
.take()
|
||||
.ok_or(StdoutError::NoOutput)?
|
||||
.flush()
|
||||
.map_err(StdoutError::FlushFailure)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -56,19 +94,11 @@ impl Sink for StdoutSink {
|
|||
|
||||
impl SinkAsBytes for StdoutSink {
|
||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
||||
match self.output.as_deref_mut() {
|
||||
Some(output) => {
|
||||
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(SinkError::NotConnected("Output is None".to_string()));
|
||||
}
|
||||
}
|
||||
self.output
|
||||
.as_deref_mut()
|
||||
.ok_or(StdoutError::NoOutput)?
|
||||
.write_all(data)
|
||||
.map_err(StdoutError::OnWrite)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -4,30 +4,75 @@ use crate::convert::Converter;
|
|||
use crate::decoder::AudioPacket;
|
||||
use shell_words::split;
|
||||
|
||||
use std::io::Write;
|
||||
use std::io::{ErrorKind, Write};
|
||||
use std::process::{exit, Child, Command, Stdio};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
enum SubprocessError {
|
||||
#[error("<SubprocessSink> {0}")]
|
||||
OnWrite(std::io::Error),
|
||||
|
||||
#[error("<SubprocessSink> Command {command} Can Not be Executed, {e}")]
|
||||
SpawnFailure { command: String, e: std::io::Error },
|
||||
|
||||
#[error("<SubprocessSink> Failed to Parse Command args for {command}, {e}")]
|
||||
InvalidArgs {
|
||||
command: String,
|
||||
e: shell_words::ParseError,
|
||||
},
|
||||
|
||||
#[error("<SubprocessSink> Failed to Flush the Subprocess, {0}")]
|
||||
FlushFailure(std::io::Error),
|
||||
|
||||
#[error("<SubprocessSink> Failed to Kill the Subprocess, {0}")]
|
||||
KillFailure(std::io::Error),
|
||||
|
||||
#[error("<SubprocessSink> Failed to Wait for the Subprocess to Exit, {0}")]
|
||||
WaitFailure(std::io::Error),
|
||||
|
||||
#[error("<SubprocessSink> The Subprocess is no longer able to accept Bytes")]
|
||||
WriteZero,
|
||||
|
||||
#[error("<SubprocessSink> Missing Required Shell Command")]
|
||||
MissingCommand,
|
||||
|
||||
#[error("<SubprocessSink> The Subprocess is None")]
|
||||
NoChild,
|
||||
|
||||
#[error("<SubprocessSink> The Subprocess's stdin is None")]
|
||||
NoStdin,
|
||||
}
|
||||
|
||||
impl From<SubprocessError> for SinkError {
|
||||
fn from(e: SubprocessError) -> SinkError {
|
||||
use SubprocessError::*;
|
||||
let es = e.to_string();
|
||||
match e {
|
||||
FlushFailure(_) | KillFailure(_) | WaitFailure(_) | OnWrite(_) | WriteZero => {
|
||||
SinkError::OnWrite(es)
|
||||
}
|
||||
SpawnFailure { .. } => SinkError::ConnectionRefused(es),
|
||||
MissingCommand | InvalidArgs { .. } => SinkError::InvalidParams(es),
|
||||
NoChild | NoStdin => SinkError::NotConnected(es),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SubprocessSink {
|
||||
shell_command: String,
|
||||
shell_command: Option<String>,
|
||||
child: Option<Child>,
|
||||
format: AudioFormat,
|
||||
}
|
||||
|
||||
impl Open for SubprocessSink {
|
||||
fn open(shell_command: Option<String>, 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);
|
||||
}
|
||||
};
|
||||
if let Some("?") = shell_command.as_deref() {
|
||||
println!("\nUsage:\n\nOutput to a Subprocess:\n\n\t--backend subprocess --device {{shell_command}}\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
info!("Using subprocess sink with format: {:?}", format);
|
||||
info!("Using SubprocessSink with format: {:?}", format);
|
||||
|
||||
Self {
|
||||
shell_command,
|
||||
|
@ -39,26 +84,53 @@ impl Open for SubprocessSink {
|
|||
|
||||
impl Sink for SubprocessSink {
|
||||
fn start(&mut self) -> SinkResult<()> {
|
||||
let args = split(&self.shell_command).unwrap();
|
||||
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);
|
||||
self.child.get_or_insert({
|
||||
match self.shell_command.as_deref() {
|
||||
Some(command) => {
|
||||
let args = split(command).map_err(|e| SubprocessError::InvalidArgs {
|
||||
command: command.to_string(),
|
||||
e,
|
||||
})?;
|
||||
|
||||
Command::new(&args[0])
|
||||
.args(&args[1..])
|
||||
.stdin(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| SubprocessError::SpawnFailure {
|
||||
command: command.to_string(),
|
||||
e,
|
||||
})?
|
||||
}
|
||||
None => return Err(SubprocessError::MissingCommand.into()),
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn stop(&mut self) -> SinkResult<()> {
|
||||
if let Some(child) = &mut self.child.take() {
|
||||
child
|
||||
.kill()
|
||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
||||
child
|
||||
.wait()
|
||||
.map_err(|e| SinkError::OnWrite(e.to_string()))?;
|
||||
let child = &mut self.child.take().ok_or(SubprocessError::NoChild)?;
|
||||
|
||||
match child.try_wait() {
|
||||
// The process has already exited
|
||||
// nothing to do.
|
||||
Ok(Some(_)) => Ok(()),
|
||||
Ok(_) => {
|
||||
// The process Must DIE!!!
|
||||
child
|
||||
.stdin
|
||||
.take()
|
||||
.ok_or(SubprocessError::NoStdin)?
|
||||
.flush()
|
||||
.map_err(SubprocessError::FlushFailure)?;
|
||||
|
||||
child.kill().map_err(SubprocessError::KillFailure)?;
|
||||
child.wait().map_err(SubprocessError::WaitFailure)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => Err(SubprocessError::WaitFailure(e).into()),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
sink_as_bytes!();
|
||||
|
@ -66,22 +138,67 @@ impl Sink for SubprocessSink {
|
|||
|
||||
impl SinkAsBytes for SubprocessSink {
|
||||
fn write_bytes(&mut self, data: &[u8]) -> SinkResult<()> {
|
||||
if let Some(child) = &mut self.child {
|
||||
let child_stdin = child
|
||||
// We get one attempted restart per write.
|
||||
// We don't want to get stuck in a restart loop.
|
||||
let mut restarted = false;
|
||||
let mut start_index = 0;
|
||||
let data_len = data.len();
|
||||
let mut end_index = data_len;
|
||||
|
||||
loop {
|
||||
match self
|
||||
.child
|
||||
.as_ref()
|
||||
.ok_or(SubprocessError::NoChild)?
|
||||
.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()))?;
|
||||
.as_ref()
|
||||
.ok_or(SubprocessError::NoStdin)?
|
||||
.write(&data[start_index..end_index])
|
||||
{
|
||||
Ok(0) => {
|
||||
// Potentially fatal.
|
||||
// As per the docs a return value of 0
|
||||
// means we shouldn't try to write to the
|
||||
// process anymore so let's try a restart
|
||||
// if we haven't already.
|
||||
self.try_restart(SubprocessError::WriteZero, &mut restarted)?;
|
||||
|
||||
continue;
|
||||
}
|
||||
Ok(bytes_written) => {
|
||||
// What we want, a successful write.
|
||||
start_index = data_len.min(start_index + bytes_written);
|
||||
end_index = data_len.min(start_index + bytes_written);
|
||||
|
||||
if end_index == data_len {
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
// Non-fatal, retry the write.
|
||||
Err(ref e) if e.kind() == ErrorKind::Interrupted => continue,
|
||||
Err(e) => {
|
||||
// Very possibly fatal,
|
||||
// but let's try a restart anyway if we haven't already.
|
||||
self.try_restart(SubprocessError::OnWrite(e), &mut restarted)?;
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SubprocessSink {
|
||||
pub const NAME: &'static str = "subprocess";
|
||||
|
||||
fn try_restart(&mut self, e: SubprocessError, restarted: &mut bool) -> SinkResult<()> {
|
||||
// If the restart fails throw the original error back.
|
||||
if !*restarted && self.stop().is_ok() && self.start().is_ok() {
|
||||
*restarted = true;
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(e.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,23 +104,33 @@ impl Mixer for AlsaMixer {
|
|||
|
||||
let min_db = min_millibel.to_db() as f64;
|
||||
let max_db = max_millibel.to_db() as f64;
|
||||
let mut db_range = f64::abs(max_db - min_db);
|
||||
let reported_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 = if config.volume_ctrl.range_ok() {
|
||||
let db_range_override = config.volume_ctrl.db_range();
|
||||
if db_range_override.is_normal() {
|
||||
db_range_override
|
||||
} else {
|
||||
reported_db_range
|
||||
}
|
||||
} else {
|
||||
config.volume_ctrl.set_db_range(reported_db_range);
|
||||
reported_db_range
|
||||
};
|
||||
|
||||
if reported_db_range == db_range {
|
||||
debug!("Alsa dB volume range was reported as {}", reported_db_range);
|
||||
if reported_db_range > 100.0 {
|
||||
debug!("Alsa mixer reported dB range > 100, which is suspect");
|
||||
debug!("Please manually set `--volume-range` if this is incorrect");
|
||||
}
|
||||
} else {
|
||||
debug!(
|
||||
"Alsa dB volume range was detected as {} but overridden as {}",
|
||||
db_range, db_range_override
|
||||
"Alsa dB volume range was reported as {} but overridden to {}",
|
||||
reported_db_range, db_range
|
||||
);
|
||||
db_range = db_range_override;
|
||||
}
|
||||
|
||||
// For hardware controls with a small range (24 dB or less),
|
||||
|
|
|
@ -3,6 +3,8 @@ use crate::config::VolumeCtrl;
|
|||
pub mod mappings;
|
||||
use self::mappings::MappedCtrl;
|
||||
|
||||
pub struct NoOpVolume;
|
||||
|
||||
pub trait Mixer: Send {
|
||||
fn open(config: MixerConfig) -> Self
|
||||
where
|
||||
|
@ -11,13 +13,19 @@ pub trait Mixer: Send {
|
|||
fn set_volume(&self, volume: u16);
|
||||
fn volume(&self) -> u16;
|
||||
|
||||
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
|
||||
None
|
||||
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
||||
Box::new(NoOpVolume)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AudioFilter {
|
||||
fn modify_stream(&self, data: &mut [f64]);
|
||||
pub trait VolumeGetter {
|
||||
fn attenuation_factor(&self) -> f64;
|
||||
}
|
||||
|
||||
impl VolumeGetter for NoOpVolume {
|
||||
fn attenuation_factor(&self) -> f64 {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
pub mod softmixer;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::AudioFilter;
|
||||
use super::VolumeGetter;
|
||||
use super::{MappedCtrl, VolumeCtrl};
|
||||
use super::{Mixer, MixerConfig};
|
||||
|
||||
|
@ -35,10 +35,8 @@ impl Mixer for SoftMixer {
|
|||
.store(mapped_volume.to_bits(), Ordering::Relaxed)
|
||||
}
|
||||
|
||||
fn get_audio_filter(&self) -> Option<Box<dyn AudioFilter + Send>> {
|
||||
Some(Box::new(SoftVolumeApplier {
|
||||
volume: self.volume.clone(),
|
||||
}))
|
||||
fn get_soft_volume(&self) -> Box<dyn VolumeGetter + Send> {
|
||||
Box::new(SoftVolume(self.volume.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -46,17 +44,10 @@ impl SoftMixer {
|
|||
pub const NAME: &'static str = "softvol";
|
||||
}
|
||||
|
||||
struct SoftVolumeApplier {
|
||||
volume: Arc<AtomicU64>,
|
||||
}
|
||||
struct SoftVolume(Arc<AtomicU64>);
|
||||
|
||||
impl AudioFilter for SoftVolumeApplier {
|
||||
fn modify_stream(&self, data: &mut [f64]) {
|
||||
let volume = f64::from_bits(self.volume.load(Ordering::Relaxed));
|
||||
if volume < 1.0 {
|
||||
for x in data.iter_mut() {
|
||||
*x *= volume;
|
||||
}
|
||||
}
|
||||
impl VolumeGetter for SoftVolume {
|
||||
fn attenuation_factor(&self) -> f64 {
|
||||
f64::from_bits(self.0.load(Ordering::Relaxed))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ use crate::{
|
|||
core::{util::SeqGenerator, Error, Session, SpotifyId},
|
||||
decoder::{AudioDecoder, AudioPacket, AudioPacketPosition, SymphoniaDecoder},
|
||||
metadata::audio::{AudioFileFormat, AudioFiles, AudioItem},
|
||||
mixer::AudioFilter,
|
||||
mixer::VolumeGetter,
|
||||
};
|
||||
|
||||
#[cfg(feature = "passthrough-decoder")]
|
||||
|
@ -81,7 +81,7 @@ struct PlayerInternal {
|
|||
sink: Box<dyn Sink>,
|
||||
sink_status: SinkStatus,
|
||||
sink_event_callback: Option<SinkEventCallback>,
|
||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
||||
volume_getter: Box<dyn VolumeGetter + Send>,
|
||||
event_senders: Vec<mpsc::UnboundedSender<PlayerEvent>>,
|
||||
converter: Converter,
|
||||
|
||||
|
@ -368,7 +368,7 @@ impl Player {
|
|||
pub fn new<F>(
|
||||
config: PlayerConfig,
|
||||
session: Session,
|
||||
audio_filter: Option<Box<dyn AudioFilter + Send>>,
|
||||
volume_getter: Box<dyn VolumeGetter + Send>,
|
||||
sink_builder: F,
|
||||
) -> (Player, PlayerEventChannel)
|
||||
where
|
||||
|
@ -420,7 +420,7 @@ impl Player {
|
|||
sink: sink_builder(),
|
||||
sink_status: SinkStatus::Closed,
|
||||
sink_event_callback: None,
|
||||
audio_filter,
|
||||
volume_getter,
|
||||
event_senders: [event_sender].to_vec(),
|
||||
converter,
|
||||
|
||||
|
@ -1445,109 +1445,111 @@ impl PlayerInternal {
|
|||
Some((_, mut packet)) => {
|
||||
if !packet.is_empty() {
|
||||
if let AudioPacket::Samples(ref mut data) = packet {
|
||||
// Get the volume for the packet.
|
||||
// In the case of hardware volume control this will
|
||||
// always be 1.0 (no change).
|
||||
let volume = self.volume_getter.attenuation_factor();
|
||||
|
||||
// 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 {
|
||||
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.
|
||||
//
|
||||
// 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-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 {
|
||||
// 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 {
|
||||
// Textbook:
|
||||
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
||||
bias_db
|
||||
}
|
||||
} else {
|
||||
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
|
||||
{
|
||||
// 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
|
||||
- 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
|
||||
- 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,
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
// No matter the case we apply volume attenuation last if there is any.
|
||||
if !self.config.normalisation && volume < 1.0 {
|
||||
for sample in data.iter_mut() {
|
||||
*sample *= volume;
|
||||
}
|
||||
}
|
||||
} else if self.config.normalisation_method == NormalisationMethod::Basic
|
||||
&& (normalisation_factor < 1.0 || volume < 1.0)
|
||||
{
|
||||
for sample in data.iter_mut() {
|
||||
*sample *= normalisation_factor * volume;
|
||||
}
|
||||
} 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;
|
||||
|
||||
// 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)
|
||||
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.
|
||||
//
|
||||
// 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-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 {
|
||||
// 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 {
|
||||
// Textbook:
|
||||
// ratio_to_db(sample.abs()) - threshold_db, which is already our bias_db.
|
||||
bias_db
|
||||
}
|
||||
} else {
|
||||
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
|
||||
{
|
||||
// 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
|
||||
- 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
|
||||
- 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,
|
||||
// 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 *= volume;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1979,7 +1981,7 @@ impl PlayerInternal {
|
|||
|
||||
fn handle_command(&mut self, cmd: PlayerCommand) -> PlayerResult {
|
||||
debug!("command={:?}", cmd);
|
||||
let result = match cmd {
|
||||
match cmd {
|
||||
PlayerCommand::Load {
|
||||
track_id,
|
||||
play_request_id,
|
||||
|
@ -2034,7 +2036,7 @@ impl PlayerInternal {
|
|||
}
|
||||
};
|
||||
|
||||
Ok(result)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_event(&mut self, event: PlayerEvent) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "librespot-protocol"
|
||||
version = "0.3.1"
|
||||
version = "0.4.1"
|
||||
authors = ["Paul Liétar <paul@lietar.net>"]
|
||||
build = "build.rs"
|
||||
description = "The protobuf logic for communicating with Spotify servers"
|
||||
|
|
|
@ -27,14 +27,17 @@ function updateVersion {
|
|||
do
|
||||
if [ "$CRATE" = "librespot" ]
|
||||
then
|
||||
CRATE=''
|
||||
CRATE_DIR=''
|
||||
else
|
||||
CRATE_DIR=$CRATE
|
||||
fi
|
||||
crate_path="$WORKINGDIR/$CRATE/Cargo.toml"
|
||||
crate_path="$WORKINGDIR/$CRATE_DIR/Cargo.toml"
|
||||
crate_path=${crate_path//\/\///}
|
||||
sed -i '' "s/^version.*/version = \"$1\"/g" "$crate_path"
|
||||
echo "Path is $crate_path"
|
||||
if [ "$CRATE" = "librespot" ]
|
||||
then
|
||||
echo "Updating lockfile"
|
||||
if [ "$DRY_RUN" = 'true' ] ; then
|
||||
cargo update --dry-run
|
||||
git add . && git commit --dry-run -a -m "Update Cargo.lock"
|
||||
|
|
39
src/main.rs
39
src/main.rs
|
@ -668,7 +668,15 @@ fn get_setup() -> Setup {
|
|||
trace!("Command line argument(s):");
|
||||
|
||||
for (index, key) in args.iter().enumerate() {
|
||||
let opt = key.trim_start_matches('-');
|
||||
let opt = {
|
||||
let key = key.trim_start_matches('-');
|
||||
|
||||
if let Some((s, _)) = key.split_once('=') {
|
||||
s
|
||||
} else {
|
||||
key
|
||||
}
|
||||
};
|
||||
|
||||
if index > 0
|
||||
&& key.starts_with('-')
|
||||
|
@ -678,13 +686,13 @@ fn get_setup() -> Setup {
|
|||
{
|
||||
if matches!(opt, PASSWORD | PASSWORD_SHORT | USERNAME | USERNAME_SHORT) {
|
||||
// Don't log creds.
|
||||
trace!("\t\t{} \"XXXXXXXX\"", key);
|
||||
trace!("\t\t{} \"XXXXXXXX\"", opt);
|
||||
} else {
|
||||
let value = matches.opt_str(opt).unwrap_or_else(|| "".to_string());
|
||||
if value.is_empty() {
|
||||
trace!("\t\t{}", key);
|
||||
trace!("\t\t{}", opt);
|
||||
} else {
|
||||
trace!("\t\t{} \"{}\"", key, value);
|
||||
trace!("\t\t{} \"{}\"", opt, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1072,7 +1080,7 @@ fn get_setup() -> Setup {
|
|||
Some(creds) if username == creds.username => Some(creds),
|
||||
_ => {
|
||||
let prompt = &format!("Password for {}: ", username);
|
||||
match rpassword::prompt_password_stderr(prompt) {
|
||||
match rpassword::prompt_password(prompt) {
|
||||
Ok(password) => {
|
||||
if !password.is_empty() {
|
||||
Some(Credentials::with_password(username, password))
|
||||
|
@ -1599,24 +1607,25 @@ async fn main() {
|
|||
|
||||
if setup.enable_discovery {
|
||||
let device_id = setup.session_config.device_id.clone();
|
||||
|
||||
discovery = match librespot::discovery::Discovery::builder(device_id)
|
||||
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);
|
||||
}
|
||||
}
|
||||
Ok(d) => discovery = Some(d),
|
||||
Err(err) => warn!("Could not initialise discovery: {}.", err),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(credentials) = setup.credentials {
|
||||
last_credentials = Some(credentials);
|
||||
connecting = true;
|
||||
} else if discovery.is_none() {
|
||||
error!(
|
||||
"Discovery is unavailable and no credentials provided. Authentication is not possible."
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
|
||||
loop {
|
||||
|
@ -1656,12 +1665,12 @@ async fn main() {
|
|||
let player_config = setup.player_config.clone();
|
||||
let connect_config = setup.connect_config.clone();
|
||||
|
||||
let audio_filter = mixer.get_audio_filter();
|
||||
let soft_volume = mixer.get_soft_volume();
|
||||
let format = setup.format;
|
||||
let backend = setup.backend;
|
||||
let device = setup.device.clone();
|
||||
let (player, event_channel) =
|
||||
Player::new(player_config, session.clone(), audio_filter, move || {
|
||||
Player::new(player_config, session.clone(), soft_volume, move || {
|
||||
(backend)(device, format)
|
||||
});
|
||||
|
||||
|
|
|
@ -17,17 +17,17 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
|||
old_track_id,
|
||||
new_track_id,
|
||||
} => match old_track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Changed: Invalid old track id",
|
||||
format!("PlayerEvent::Changed: Invalid old track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(old_id) => match new_track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Changed: Invalid new track id",
|
||||
format!("PlayerEvent::Changed: Invalid old track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(new_id) => {
|
||||
|
@ -38,10 +38,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
|||
},
|
||||
},
|
||||
PlayerEvent::Started { track_id, .. } => match track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Started: Invalid track id",
|
||||
format!("PlayerEvent::Started: Invalid track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
|
@ -50,10 +50,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
|||
}
|
||||
},
|
||||
PlayerEvent::Stopped { track_id, .. } => match track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Stopped: Invalid track id",
|
||||
format!("PlayerEvent::Stopped: Invalid track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
|
@ -67,10 +67,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
|||
position_ms,
|
||||
..
|
||||
} => match track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Playing: Invalid track id",
|
||||
format!("PlayerEvent::Playing: Invalid track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
|
@ -86,10 +86,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
|||
position_ms,
|
||||
..
|
||||
} => match track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Paused: Invalid track id",
|
||||
format!("PlayerEvent::Paused: Invalid track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
|
@ -100,10 +100,10 @@ pub fn run_program_on_events(event: PlayerEvent, onevent: &str) -> Option<Result
|
|||
}
|
||||
},
|
||||
PlayerEvent::Preloading { track_id, .. } => match track_id.to_base62() {
|
||||
Err(_) => {
|
||||
Err(e) => {
|
||||
return Some(Err(Error::new(
|
||||
ErrorKind::InvalidData,
|
||||
"PlayerEvent::Preloading: Invalid track id",
|
||||
format!("PlayerEvent::Preloading: Invalid track id: {}", e),
|
||||
)))
|
||||
}
|
||||
Ok(id) => {
|
||||
|
|
Loading…
Reference in a new issue