From 0817b08507066f448779818ea8ec214958b1d4d9 Mon Sep 17 00:00:00 2001 From: herrernst Date: Fri, 8 Dec 2017 00:57:45 +0100 Subject: [PATCH 1/6] apply pre-computed track gain from Spotify's proprietary ogg header --- src/player.rs | 49 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/player.rs b/src/player.rs index 29380e33e..3e61d2b5c 100644 --- a/src/player.rs +++ b/src/player.rs @@ -5,6 +5,9 @@ use std::mem; use std::sync::mpsc::{RecvError, TryRecvError}; use std::thread; use std; +use std::io::Seek; +use std::io::SeekFrom; +use std::io::Read; use core::config::{Bitrate, PlayerConfig}; use core::session::Session; @@ -103,10 +106,12 @@ enum PlayerState { Stopped, Paused { decoder: Decoder, + track_gain_db: f32, end_of_track: oneshot::Sender<()>, }, Playing { decoder: Decoder, + track_gain_db: f32, end_of_track: oneshot::Sender<()>, }, @@ -149,9 +154,10 @@ impl PlayerState { fn paused_to_playing(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Paused { decoder, end_of_track } => { + Paused { decoder, track_gain_db, end_of_track } => { *self = Playing { decoder: decoder, + track_gain_db: track_gain_db, end_of_track: end_of_track, }; } @@ -162,9 +168,10 @@ impl PlayerState { fn playing_to_paused(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Playing { decoder, end_of_track } => { + Playing { decoder, track_gain_db, end_of_track } => { *self = Paused { decoder: decoder, + track_gain_db: track_gain_db, end_of_track: end_of_track, }; } @@ -193,23 +200,35 @@ impl PlayerInternal { self.handle_command(cmd); } - let packet = if let PlayerState::Playing { ref mut decoder, .. } = self.state { + let mut current_track_gain_db: f32 = 0.0; + let packet = if let PlayerState::Playing { ref mut decoder, track_gain_db, .. } = self.state { + current_track_gain_db = track_gain_db; Some(decoder.next_packet().expect("Vorbis error")) } else { None }; if let Some(packet) = packet { - self.handle_packet(packet); + self.handle_packet(packet, current_track_gain_db); } } } - fn handle_packet(&mut self, packet: Option) { + fn handle_packet(&mut self, packet: Option, track_gain_db: f32) { match packet { Some(mut packet) => { if let Some(ref editor) = self.audio_filter { editor.modify_stream(&mut packet.data_mut()) }; + let normalization_factor = f32::powf(10.0, track_gain_db / 20.0); + + // info!("Use gain: {}, factor: {}", track_gain_db, normalization_factor); + + if normalization_factor != 1.0 { + for x in packet.data_mut().iter_mut() { + *x = (*x as f32 * normalization_factor) as i16; + } + } + self.sink.write(&packet.data()).unwrap(); } @@ -232,7 +251,7 @@ impl PlayerInternal { } match self.load_track(track_id, position as i64) { - Some(decoder) => { + Some((decoder, track_gain_db)) => { if play { if !self.state.is_playing() { self.run_onstart(); @@ -241,6 +260,7 @@ impl PlayerInternal { self.state = PlayerState::Playing { decoder: decoder, + track_gain_db: track_gain_db, end_of_track: end_of_track, }; } else { @@ -250,6 +270,7 @@ impl PlayerInternal { self.state = PlayerState::Paused { decoder: decoder, + track_gain_db: track_gain_db, end_of_track: end_of_track, }; } @@ -343,7 +364,7 @@ impl PlayerInternal { } } - fn load_track(&self, track_id: SpotifyId, position: i64) -> Option { + fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> { let track = Track::get(&self.session, track_id).wait().unwrap(); info!("Loading track \"{}\"", track.name); @@ -373,7 +394,17 @@ impl PlayerInternal { let key = self.session.audio_key().request(track.id, file_id).wait().unwrap(); let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap(); - let audio_file = Subfile::new(AudioDecrypt::new(key, encrypted_file), 0xa7); + let mut track_gain_float_bytes = [0; 4]; + + let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); + decrypted_file.seek(SeekFrom::Start(144)).unwrap(); // 4 bytes as LE float + decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + let track_gain_db: f32; + unsafe { + track_gain_db = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); + info!("Track gain: {}db", track_gain_db); + } + let audio_file = Subfile::new(decrypted_file, 0xa7); let mut decoder = VorbisDecoder::new(audio_file).unwrap(); @@ -384,7 +415,7 @@ impl PlayerInternal { info!("Track \"{}\" loaded", track.name); - Some(decoder) + Some((decoder, track_gain_db)) } } From f5989f4362742e471768d911218f113bf40d8181 Mon Sep 17 00:00:00 2001 From: herrernst Date: Fri, 8 Dec 2017 23:58:49 +0100 Subject: [PATCH 2/6] enable normalization only if flag is passed --- core/src/config.rs | 2 ++ src/main.rs | 4 +++- src/player.rs | 33 +++++++++++++++++++++------------ 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index 7dcb97d33..1dea4720a 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -105,6 +105,7 @@ pub struct PlayerConfig { pub bitrate: Bitrate, pub onstart: Option, pub onstop: Option, + pub normalization: bool, } impl Default for PlayerConfig { @@ -113,6 +114,7 @@ impl Default for PlayerConfig { bitrate: Bitrate::default(), onstart: None, onstop: None, + normalization: false, } } } diff --git a/src/main.rs b/src/main.rs index c2850cdf4..cbd830f50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,7 +100,8 @@ fn setup(args: &[String]) -> Setup { .optflag("", "disable-discovery", "Disable discovery mode") .optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND") .optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE") - .optopt("", "mixer", "Mixer to use", "MIXER"); + .optopt("", "mixer", "Mixer to use", "MIXER") + .optflag("", "enable-volume-normalization", "Play all tracks at the same volume"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, @@ -169,6 +170,7 @@ fn setup(args: &[String]) -> Setup { bitrate: bitrate, onstart: matches.opt_str("onstart"), onstop: matches.opt_str("onstop"), + normalization: matches.opt_present("enable-volume-normalization") } }; diff --git a/src/player.rs b/src/player.rs index 3e61d2b5c..7468a265f 100644 --- a/src/player.rs +++ b/src/player.rs @@ -219,14 +219,18 @@ impl PlayerInternal { editor.modify_stream(&mut packet.data_mut()) }; - let normalization_factor = f32::powf(10.0, track_gain_db / 20.0); + if self.config.normalization { - // info!("Use gain: {}, factor: {}", track_gain_db, normalization_factor); + let normalization_factor = f32::powf(10.0, track_gain_db / 20.0); - if normalization_factor != 1.0 { - for x in packet.data_mut().iter_mut() { - *x = (*x as f32 * normalization_factor) as i16; + // info!("Use gain: {}, factor: {}", track_gain_db, normalization_factor); + + if normalization_factor != 1.0 { + for x in packet.data_mut().iter_mut() { + *x = (*x as f32 * normalization_factor) as i16; + } } + } self.sink.write(&packet.data()).unwrap(); @@ -394,16 +398,21 @@ impl PlayerInternal { let key = self.session.audio_key().request(track.id, file_id).wait().unwrap(); let encrypted_file = AudioFile::open(&self.session, file_id).wait().unwrap(); - let mut track_gain_float_bytes = [0; 4]; let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - decrypted_file.seek(SeekFrom::Start(144)).unwrap(); // 4 bytes as LE float - decrypted_file.read(&mut track_gain_float_bytes).unwrap(); - let track_gain_db: f32; - unsafe { - track_gain_db = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); - info!("Track gain: {}db", track_gain_db); + + let mut track_gain_db: f32 = 0.0; + + if self.config.normalization { + let mut track_gain_float_bytes = [0; 4]; + decrypted_file.seek(SeekFrom::Start(144)).unwrap(); // 4 bytes as LE float + decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + unsafe { + track_gain_db = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); + info!("Track gain: {}db", track_gain_db); + } } + let audio_file = Subfile::new(decrypted_file, 0xa7); let mut decoder = VorbisDecoder::new(audio_file).unwrap(); From 2eabb930e0a8ee1e02dd62c46780fb33a7a19521 Mon Sep 17 00:00:00 2001 From: herrernst Date: Sat, 9 Dec 2017 00:34:12 +0100 Subject: [PATCH 3/6] log other gain info --- src/player.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/player.rs b/src/player.rs index 7468a265f..2084d1a8e 100644 --- a/src/player.rs +++ b/src/player.rs @@ -410,7 +410,24 @@ impl PlayerInternal { unsafe { track_gain_db = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); info!("Track gain: {}db", track_gain_db); - } + } + decrypted_file.seek(SeekFrom::Start(148)).unwrap(); // 4 bytes as LE float + decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + unsafe { + // track peak, 1.0 represents dbfs + info!("Track peak: {}", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); + } + decrypted_file.seek(SeekFrom::Start(152)).unwrap(); // 4 bytes as LE float + decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + unsafe { + info!("Album gain: {}db", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); + } + decrypted_file.seek(SeekFrom::Start(156)).unwrap(); // 4 bytes as LE float + decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + unsafe { + // album peak, 1.0 represents dbfs + info!("Album peak: {}", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); + } } let audio_file = Subfile::new(decrypted_file, 0xa7); From 38f18e3adb523f18fc390485fe03dea72da6adb0 Mon Sep 17 00:00:00 2001 From: herrernst Date: Thu, 25 Jan 2018 21:12:18 +0100 Subject: [PATCH 4/6] warn on clipping add option to apply pre-gain --- core/src/config.rs | 4 ++++ src/main.rs | 8 ++++++-- src/player.rs | 11 +++++++++-- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/core/src/config.rs b/core/src/config.rs index 1dea4720a..4fb0d3670 100644 --- a/core/src/config.rs +++ b/core/src/config.rs @@ -106,6 +106,7 @@ pub struct PlayerConfig { pub onstart: Option, pub onstop: Option, pub normalization: bool, + pub normalization_pre_gain: f32, } impl Default for PlayerConfig { @@ -115,6 +116,9 @@ impl Default for PlayerConfig { onstart: None, onstop: None, normalization: false, + normalization_pre_gain: 0.0, //replaygain target is -14dbfs, may not be enough headroom + // macOS Spotify client adds 3.0 + // https://community.spotify.com/t5/Social-Off-Topic/How-does-the-loudness-normalization-algorithm-work/td-p/1603671 } } } diff --git a/src/main.rs b/src/main.rs index cbd830f50..dfe2baef4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,7 +101,8 @@ fn setup(args: &[String]) -> Setup { .optopt("", "backend", "Audio backend to use. Use '?' to list options", "BACKEND") .optopt("", "device", "Audio device to use. Use '?' to list options", "DEVICE") .optopt("", "mixer", "Mixer to use", "MIXER") - .optflag("", "enable-volume-normalization", "Play all tracks at the same volume"); + .optflag("", "enable-volume-normalization", "Play all tracks at the same volume") + .optopt("", "normalization-pre-gain", "Pre-gain (dB) applied by volume normalization", "PREGAIN"); let matches = match opts.parse(&args[1..]) { Ok(m) => m, @@ -170,7 +171,10 @@ fn setup(args: &[String]) -> Setup { bitrate: bitrate, onstart: matches.opt_str("onstart"), onstop: matches.opt_str("onstop"), - normalization: matches.opt_present("enable-volume-normalization") + normalization: matches.opt_present("enable-volume-normalization"), + normalization_pre_gain: matches.opt_str("normalization-pre-gain") + .map(|pre_gain| pre_gain.parse::().expect("Invalid pre-gain float value")) + .unwrap_or(PlayerConfig::default().normalization_pre_gain), } }; diff --git a/src/player.rs b/src/player.rs index 2084d1a8e..24fd664c1 100644 --- a/src/player.rs +++ b/src/player.rs @@ -221,7 +221,8 @@ impl PlayerInternal { if self.config.normalization { - let normalization_factor = f32::powf(10.0, track_gain_db / 20.0); + // see http://wiki.hydrogenaud.io/index.php?title=ReplayGain_specification#Loudness_normalization + let normalization_factor = f32::powf(10.0, (track_gain_db + self.config.normalization_pre_gain) / 20.0); // info!("Use gain: {}, factor: {}", track_gain_db, normalization_factor); @@ -413,9 +414,15 @@ impl PlayerInternal { } decrypted_file.seek(SeekFrom::Start(148)).unwrap(); // 4 bytes as LE float decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + let normalization_factor = f32::powf(10.0, (track_gain_db + self.config.normalization_pre_gain) / 20.0); + let mut track_peak: f32 = 1.0; unsafe { // track peak, 1.0 represents dbfs - info!("Track peak: {}", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); + track_peak = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); + info!("Track peak: {}", track_peak); + if normalization_factor * track_peak > 1.0 { + warn!("Track will clip, please add negative pre-gain"); + } } decrypted_file.seek(SeekFrom::Start(152)).unwrap(); // 4 bytes as LE float decrypted_file.read(&mut track_gain_float_bytes).unwrap(); From 67765f747863ce63485a53971ec0e30b8a8ba96c Mon Sep 17 00:00:00 2001 From: herrernst Date: Tue, 30 Jan 2018 22:06:02 +0100 Subject: [PATCH 5/6] prevent clipping; add option to apply pre-gain --- src/player.rs | 69 +++++++++++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/player.rs b/src/player.rs index 24fd664c1..d860a1040 100644 --- a/src/player.rs +++ b/src/player.rs @@ -106,12 +106,12 @@ enum PlayerState { Stopped, Paused { decoder: Decoder, - track_gain_db: f32, + normalization_factor: f32, end_of_track: oneshot::Sender<()>, }, Playing { decoder: Decoder, - track_gain_db: f32, + normalization_factor: f32, end_of_track: oneshot::Sender<()>, }, @@ -154,10 +154,10 @@ impl PlayerState { fn paused_to_playing(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Paused { decoder, track_gain_db, end_of_track } => { + Paused { decoder, normalization_factor, end_of_track } => { *self = Playing { decoder: decoder, - track_gain_db: track_gain_db, + normalization_factor: normalization_factor, end_of_track: end_of_track, }; } @@ -168,10 +168,10 @@ impl PlayerState { fn playing_to_paused(&mut self) { use self::PlayerState::*; match ::std::mem::replace(self, Invalid) { - Playing { decoder, track_gain_db, end_of_track } => { + Playing { decoder, normalization_factor, end_of_track } => { *self = Paused { decoder: decoder, - track_gain_db: track_gain_db, + normalization_factor: normalization_factor, end_of_track: end_of_track, }; } @@ -200,19 +200,19 @@ impl PlayerInternal { self.handle_command(cmd); } - let mut current_track_gain_db: f32 = 0.0; - let packet = if let PlayerState::Playing { ref mut decoder, track_gain_db, .. } = self.state { - current_track_gain_db = track_gain_db; + let mut current_normalization_factor: f32 = 1.0; + let packet = if let PlayerState::Playing { ref mut decoder, normalization_factor, .. } = self.state { + current_normalization_factor = normalization_factor; Some(decoder.next_packet().expect("Vorbis error")) } else { None }; if let Some(packet) = packet { - self.handle_packet(packet, current_track_gain_db); + self.handle_packet(packet, current_normalization_factor); } } } - fn handle_packet(&mut self, packet: Option, track_gain_db: f32) { + fn handle_packet(&mut self, packet: Option, normalization_factor: f32) { match packet { Some(mut packet) => { if let Some(ref editor) = self.audio_filter { @@ -221,11 +221,6 @@ impl PlayerInternal { if self.config.normalization { - // see http://wiki.hydrogenaud.io/index.php?title=ReplayGain_specification#Loudness_normalization - let normalization_factor = f32::powf(10.0, (track_gain_db + self.config.normalization_pre_gain) / 20.0); - - // info!("Use gain: {}, factor: {}", track_gain_db, normalization_factor); - if normalization_factor != 1.0 { for x in packet.data_mut().iter_mut() { *x = (*x as f32 * normalization_factor) as i16; @@ -256,7 +251,7 @@ impl PlayerInternal { } match self.load_track(track_id, position as i64) { - Some((decoder, track_gain_db)) => { + Some((decoder, normalization_factor)) => { if play { if !self.state.is_playing() { self.run_onstart(); @@ -265,7 +260,7 @@ impl PlayerInternal { self.state = PlayerState::Playing { decoder: decoder, - track_gain_db: track_gain_db, + normalization_factor: normalization_factor, end_of_track: end_of_track, }; } else { @@ -275,7 +270,7 @@ impl PlayerInternal { self.state = PlayerState::Paused { decoder: decoder, - track_gain_db: track_gain_db, + normalization_factor: normalization_factor, end_of_track: end_of_track, }; } @@ -402,38 +397,52 @@ impl PlayerInternal { let mut decrypted_file = AudioDecrypt::new(key, encrypted_file); - let mut track_gain_db: f32 = 0.0; + let mut normalization_factor: f32 = 1.0; if self.config.normalization { + //buffer for float bytes let mut track_gain_float_bytes = [0; 4]; + decrypted_file.seek(SeekFrom::Start(144)).unwrap(); // 4 bytes as LE float decrypted_file.read(&mut track_gain_float_bytes).unwrap(); + let track_gain_db: f32; unsafe { track_gain_db = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); - info!("Track gain: {}db", track_gain_db); + debug!("Track gain: {}db", track_gain_db); } + decrypted_file.seek(SeekFrom::Start(148)).unwrap(); // 4 bytes as LE float decrypted_file.read(&mut track_gain_float_bytes).unwrap(); - let normalization_factor = f32::powf(10.0, (track_gain_db + self.config.normalization_pre_gain) / 20.0); - let mut track_peak: f32 = 1.0; + let track_peak: f32; unsafe { // track peak, 1.0 represents dbfs track_peak = mem::transmute::<[u8; 4], f32>(track_gain_float_bytes); - info!("Track peak: {}", track_peak); - if normalization_factor * track_peak > 1.0 { - warn!("Track will clip, please add negative pre-gain"); - } + debug!("Track peak: {}", track_peak); } + + // see http://wiki.hydrogenaud.io/index.php?title=ReplayGain_specification#Loudness_normalization + normalization_factor = f32::powf(10.0, (track_gain_db + self.config.normalization_pre_gain) / 20.0); + + if normalization_factor * track_peak > 1.0 { + warn!("Track would clip, reducing normalisation factor. \ + Please add negative pre-gain to avoid."); + normalization_factor = 1.0/track_peak; + } + + info!("Applying normalization factor: {}", normalization_factor); + + // TODO there are also values for album gain/peak, which should be used if an album is playing + // but I don't know how to determine if album is playing decrypted_file.seek(SeekFrom::Start(152)).unwrap(); // 4 bytes as LE float decrypted_file.read(&mut track_gain_float_bytes).unwrap(); unsafe { - info!("Album gain: {}db", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); + debug!("Album gain: {}db", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); } decrypted_file.seek(SeekFrom::Start(156)).unwrap(); // 4 bytes as LE float decrypted_file.read(&mut track_gain_float_bytes).unwrap(); unsafe { // album peak, 1.0 represents dbfs - info!("Album peak: {}", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); + debug!("Album peak: {}", mem::transmute::<[u8; 4], f32>(track_gain_float_bytes)); } } @@ -448,7 +457,7 @@ impl PlayerInternal { info!("Track \"{}\" loaded", track.name); - Some((decoder, track_gain_db)) + Some((decoder, normalization_factor)) } } From d309ddd289181e1b071e4aceb921ab34301faa4c Mon Sep 17 00:00:00 2001 From: Colm Date: Wed, 31 Jan 2018 22:26:44 +0000 Subject: [PATCH 6/6] Accidentally removed a `;` --- src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 0e1fb0e34..d2c5e78fb 100644 --- a/src/main.rs +++ b/src/main.rs @@ -103,7 +103,7 @@ fn setup(args: &[String]) -> Setup { .optflag("", "enable-volume-normalization", "Play all tracks at the same volume") .optopt("", "normalization-pre-gain", "Pre-gain (dB) applied by volume normalization", "PREGAIN") .optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME") - .optopt("z", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT") + .optopt("z", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT"); let matches = match opts.parse(&args[1..]) { Ok(m) => m,