-
Notifications
You must be signed in to change notification settings - Fork 645
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add volume normalisation support #162
Conversation
Having just tested this addition, the difference is noticeable, and normalising volume does improve the playback experience. My vote would be to include this as a standard feature, and not as a runtime flag, whilst keeping the pregain flag for those who wish to modify it. Thoughts on doing this appreciated. |
Hi, |
524021e
to
ff1f7e1
Compare
Have combined the two commits and left as a runtime flag. |
Sorry, didn't have the time lately. Thanks for taking care of this. |
@herrernst no problem. I was just having a look at the protobuf files, and it does not look as if there is an indicator as to whether the track is currently being played as part of an album. What I suspect is happening, based on this assumption, is that the track being played always has the track and album normalisation applied, as one would not notice it if songs were played individually, but one would if it were being played as part of an album. This still does not answer the question of how playlists/compilations handle volume normalisation though. If you have any thoughts/revelations on this later on though, adding album normalisation shouldn't be too hard once this is implemented. I'll merge this later tonight if there are no objections. |
playback/src/player.rs
Outdated
@@ -43,6 +44,14 @@ enum PlayerCommand { | |||
Seek(u32), | |||
} | |||
|
|||
#[derive(Debug)] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Derive Copy and Clone as well
playback/src/player.rs
Outdated
normalisation_factor = f32::powf(10.0, (normalisation_config.track_gain_db + self.config.normalisation_pregain) / 20.0); | ||
|
||
if normalisation_factor * normalisation_config.track_peak > 1.0 { | ||
debug!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be a warn!
playback/src/player.rs
Outdated
@@ -43,6 +44,14 @@ enum PlayerCommand { | |||
Seek(u32), | |||
} | |||
|
|||
#[derive(Debug)] | |||
struct NormalisationConfig { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't be called Config
. Something like Data
or Values
or Parameters
would be more appropriate.
playback/src/player.rs
Outdated
track_gain_db = file.read_f32::<LittleEndian>().unwrap(); | ||
debug!("Track gain: {}db", track_gain_db); | ||
|
||
file.seek(SeekFrom::Start(148)).unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no need to seek everytime, reading an f32 will already move the cursor
playback/src/player.rs
Outdated
|
||
file.seek(SeekFrom::Start(144)).unwrap(); | ||
track_gain_db = file.read_f32::<LittleEndian>().unwrap(); | ||
debug!("Track gain: {}db", track_gain_db); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a single debug! line for all 4 values (eg debug!("Normalization: {:?}", data);
), potentially in load_track
.
playback/src/player.rs
Outdated
|
||
if self.config.normalisation { | ||
let normalisation_config = self.parse_normalisation(&mut decrypted_file); | ||
normalisation_factor = f32::powf(10.0, (normalisation_config.track_gain_db + self.config.normalisation_pregain) / 20.0); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would move this code (and the clipping prevention) into a method of NormalisationCofig
I'm actually away this weekend, so don't think I'll be at a computer until next week. Feel free to implement the changes if you want to expedite things, otherwise this'll probably happen sometime next week. |
ff1f7e1
to
f8db550
Compare
Turns out editing on an iPad isn't so bad. @plietar let me know if there's any other changes you want to suggest, otherwise will merge in a couple of hours. |
@sashahilton00 When a track is played in album context, album gain should be applied; otherwise, track gain should be applied. Never should both gains be applied. Replay gain spec. |
Didn't realise Spotify implemented ReplayGain, though I didn't really look tbh. Makes sense though. If you have any thoughts as to how to add album support I'm all ears. |
Can we add a named constant for the magic 144 number and/or say where it came from (@herrernst said https://sourceforge.net/p/despotify/code/HEAD/tree/java/trunk/src/main/java/se/despotify/client/player/SpotifyOggHeader.java#l106) |
@sashahilton00 Yes, these four values are the same you would get if running |
Also, the logging uses inconsistent American vs British spelling of "normalisation". |
Good spot, I copied the debug messages, slipped through the net. What do you mean when you say you want a named constant for the 144? Or do you just want a comment with a link to despotify for reference? I've avoided commenting the code just to keep it neat. |
I think the reference to despotify is not necessary. I've discovered the values by searching for meaningful float values in a hex editor, only finding the despotify reference later. |
I think a descriptive name would be helpful here. I'm not a fan of magic numbers. SPOTIFY_HEADER_START_OFFSET = 144 or something. |
playback/src/player.rs
Outdated
} | ||
} | ||
|
||
fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since you already have a if self.config.normalisation && normalisation_factor != 1.0
condition before applying it, you can ignore the config here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missed that. removed.
playback/src/player.rs
Outdated
static SPOTIFY_HEADER_START_OFFSET: u64 = 144; | ||
file.seek(SeekFrom::Start(SPOTIFY_HEADER_START_OFFSET)).unwrap(); | ||
|
||
let track_gain_db: f32 = file.read_f32::<LittleEndian>().unwrap(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for the : f32
type annotations.
playback/src/player.rs
Outdated
|
||
impl NormalisationData { | ||
fn new<T: Read + Seek>(file: &mut AudioDecrypt<T>) -> NormalisationData { | ||
static SPOTIFY_HEADER_START_OFFSET: u64 = 144; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Make that a const
, and call it SPOTIFY_NORMALIZATION_HEADER_START_OFFSET
playback/src/player.rs
Outdated
} | ||
|
||
impl NormalisationData { | ||
fn new<T: Read + Seek>(file: &mut AudioDecrypt<T>) -> NormalisationData { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Function should be called parse_from_file
or something, and it's argument can just be a mut file: T
AudioDecrypt<T>
(and by extension &mut AudioDecrypt<T>
) already implement Read + Seek
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also it can return an io::Result<NormalisationData>
and use ?
instead of unwrap
, moving the unwrap to the caller.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
have changed it to return a Result
, but I'm using match
instead of ?
as I personally don't like the ?
operator as it's confusing (feels like it should mean optional) and feel that match is more readable, also as normalisation is not something that should fail hard if the data can't be read, I've set the behaviour to warn
if the normalisation data cannot be read, and set it to the default value of 1.0
playback/src/player.rs
Outdated
|
||
let normalisation_data = NormalisationData::new(&mut decrypted_file); | ||
|
||
let normalisation_factor: f32 = NormalisationData::get_factor(&self.config, normalisation_data); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need for : f32
Will merge in a couple of hours further feedback notwithstanding. |
Regarding the difficulty in knowing if a track is part of an album or a playlist and whether album or track gain should be applied, would it be possible or practical to add an option for the user to select either album gain or track gain? On my squeezeboxes there is an option to select album gain, track gain or smart gain which uses album gain for an album and track gain for a playlist of individual tracks. I find that using album gain all the time is preferable to smart gain and definitely preferable to permanent track gain. |
Yes, this would be a very nice option. I was just listening to The Wall, but with the volume jumps it was unbearable. |
That album makes the case perfectly. It was the album I used to test Spotify normalisation. The final track sounds particularly ridiculous. An option to choose between Track and Album gain is essential for many users. |
I'd suggest opening a new issue and/or making a PR detailing the desired change and a possible implementation so that it can be discussed, since this PR is closed. |
I've read a study (or discussion) somewhere with the result that applying album gain should be the default option (at least if you can't figure out the context of a track). Different tracks of an album often don't deviate much from the average album gain, and even if they do (like the last track on The Wall), it's probably an artistic choice that the track should be quieter compared to others. So IMHO album gain should be default option, with the option of selecting track gain (for people that absolutely need exact track gain when playing playlist with mixed tracks) and disabling it. If I read the code correctly, librespot uses track gain at the moment, but it better should use album gain as default. |
The heavy lifting was done by @herrernst. Since his PR has been quiet for a while, and there has been a significant reordering of librespot's structure, this is a reorder/rewrite, taking onboard feedback from #114. Review/suggestion welcomed. Closes #114