Przeglądaj źródła

Add volume normalisation support

Sasha Hilton 7 lat temu
rodzic
commit
f8db550e5e
6 zmienionych plików z 91 dodań i 10 usunięć
  1. 1 0
      Cargo.lock
  2. 1 0
      playback/Cargo.toml
  3. 4 0
      playback/src/config.rs
  4. 1 0
      playback/src/lib.rs
  5. 77 9
      playback/src/player.rs
  6. 7 1
      src/main.rs

+ 1 - 0
Cargo.lock

@@ -413,6 +413,7 @@ name = "librespot-playback"
 version = "0.1.0"
 dependencies = [
  "alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)",
+ "byteorder 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "futures 0.1.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "jack 0.5.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",

+ 1 - 0
playback/Cargo.toml

@@ -13,6 +13,7 @@ path = "../metadata"
 [dependencies]
 futures = "0.1.8"
 log = "0.3.5"
+byteorder = "1.2.1"
 
 alsa            = { git = "https://github.com/plietar/rust-alsa", optional = true }
 portaudio-rs    = { version = "0.3.0", optional = true }

+ 4 - 0
playback/src/config.rs

@@ -30,6 +30,8 @@ pub struct PlayerConfig {
     pub bitrate: Bitrate,
     pub onstart: Option<String>,
     pub onstop: Option<String>,
+    pub normalisation: bool,
+    pub normalisation_pregain: f32,
 }
 
 impl Default for PlayerConfig {
@@ -38,6 +40,8 @@ impl Default for PlayerConfig {
             bitrate: Bitrate::default(),
             onstart: None,
             onstop: None,
+            normalisation: false,
+            normalisation_pregain: 0.0,
         }
     }
 }

+ 1 - 0
playback/src/lib.rs

@@ -1,6 +1,7 @@
 #[macro_use] extern crate log;
 
 extern crate futures;
+extern crate byteorder;
 
 #[cfg(feature = "alsa-backend")]
 extern crate alsa;

+ 77 - 9
playback/src/player.rs

@@ -1,3 +1,4 @@
+use byteorder::{LittleEndian, ReadBytesExt};
 use futures::sync::oneshot;
 use futures::{future, Future};
 use std;
@@ -43,6 +44,51 @@ enum PlayerCommand {
     Seek(u32),
 }
 
+#[derive(Clone, Copy, Debug)]
+struct NormalisationData {
+    track_gain_db: f32,
+    track_peak: f32,
+    album_gain_db: f32,
+    album_peak: f32,
+}
+
+impl NormalisationData {
+    fn new<T: Read + Seek>(file: &mut AudioDecrypt<T>) -> NormalisationData {
+        file.seek(SeekFrom::Start(144)).unwrap();
+
+        let track_gain_db: f32 = file.read_f32::<LittleEndian>().unwrap();
+        let track_peak: f32 = file.read_f32::<LittleEndian>().unwrap();
+        let album_gain_db: f32 = file.read_f32::<LittleEndian>().unwrap();
+        let album_peak: f32 = file.read_f32::<LittleEndian>().unwrap();
+
+        NormalisationData {
+            track_gain_db: track_gain_db,
+            track_peak: track_peak,
+            album_gain_db: album_gain_db,
+            album_peak: album_peak,
+        }
+    }
+
+    fn get_factor(config: &PlayerConfig, data: NormalisationData) -> f32 {
+        let mut normalisation_factor: f32 = 1.0;
+
+        debug!("Normalization Data: {:?}", data);
+
+        if config.normalisation {
+            normalisation_factor = f32::powf(10.0, (data.track_gain_db + config.normalisation_pregain) / 20.0);
+
+            if normalisation_factor * data.track_peak > 1.0 {
+                warn!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.");
+                normalisation_factor = 1.0 / data.track_peak;
+            }
+
+            debug!("Applied normalization factor: {}", normalisation_factor);
+        }
+
+        normalisation_factor
+    }
+}
+
 impl Player {
     pub fn new<F>(config: PlayerConfig, session: Session,
                   audio_filter: Option<Box<AudioFilter + Send>>,
@@ -123,10 +169,12 @@ enum PlayerState {
     Paused {
         decoder: Decoder,
         end_of_track: oneshot::Sender<()>,
+        normalisation_factor: f32,
     },
     Playing {
         decoder: Decoder,
         end_of_track: oneshot::Sender<()>,
+        normalisation_factor: f32,
     },
 
     Invalid,
@@ -168,10 +216,11 @@ impl PlayerState {
     fn paused_to_playing(&mut self) {
         use self::PlayerState::*;
         match ::std::mem::replace(self, Invalid) {
-            Paused { decoder, end_of_track } => {
+            Paused { decoder, end_of_track, normalisation_factor } => {
                 *self = Playing {
                     decoder: decoder,
                     end_of_track: end_of_track,
+                    normalisation_factor: normalisation_factor,
                 };
             }
             _ => panic!("invalid state"),
@@ -181,10 +230,11 @@ impl PlayerState {
     fn playing_to_paused(&mut self) {
         use self::PlayerState::*;
         match ::std::mem::replace(self, Invalid) {
-            Playing { decoder, end_of_track } => {
+            Playing { decoder, end_of_track, normalisation_factor } => {
                 *self = Paused {
                     decoder: decoder,
                     end_of_track: end_of_track,
+                    normalisation_factor: normalisation_factor,
                 };
             }
             _ => panic!("invalid state"),
@@ -228,14 +278,17 @@ impl PlayerInternal {
             }
 
             if self.sink_running {
-                let packet = if let PlayerState::Playing { ref mut decoder, .. } = self.state {
+                let mut current_normalisation_factor: f32 = 1.0;
+
+                let packet = if let PlayerState::Playing { ref mut decoder, normalisation_factor, .. } = self.state {
+                    current_normalisation_factor = normalisation_factor;
                     Some(decoder.next_packet().expect("Vorbis error"))
                 } else {
                     None
                 };
 
                 if let Some(packet) = packet {
-                    self.handle_packet(packet);
+                    self.handle_packet(packet, current_normalisation_factor);
                 }
             }
         }
@@ -259,13 +312,19 @@ impl PlayerInternal {
         self.sink_running = false;
     }
 
-    fn handle_packet(&mut self, packet: Option<VorbisPacket>) {
+    fn handle_packet(&mut self, packet: Option<VorbisPacket>, normalisation_factor: f32) {
         match packet {
             Some(mut packet) => {
                 if let Some(ref editor) = self.audio_filter {
                     editor.modify_stream(&mut packet.data_mut())
                 };
 
+                if self.config.normalisation && normalisation_factor != 1.0 {
+                    for x in packet.data_mut().iter_mut() {
+                        *x = (*x as f32 * normalisation_factor) as i16;
+                    }
+                }
+
                 if let Err(err) = self.sink.write(&packet.data()) {
                     error!("Could not write audio: {}", err);
                     self.stop_sink();
@@ -291,7 +350,7 @@ impl PlayerInternal {
                 }
 
                 match self.load_track(track_id, position as i64) {
-                    Some(decoder) => {
+                    Some((decoder, normalisation_factor)) => {
                         if play {
                             if !self.state.is_playing() {
                                 self.run_onstart();
@@ -301,6 +360,7 @@ impl PlayerInternal {
                             self.state = PlayerState::Playing {
                                 decoder: decoder,
                                 end_of_track: end_of_track,
+                                normalisation_factor: normalisation_factor,
                             };
                         } else {
                             if self.state.is_playing() {
@@ -310,6 +370,7 @@ impl PlayerInternal {
                             self.state = PlayerState::Paused {
                                 decoder: decoder,
                                 end_of_track: end_of_track,
+                                normalisation_factor: normalisation_factor,
                             };
                         }
                     }
@@ -402,7 +463,7 @@ impl PlayerInternal {
         }
     }
 
-    fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<Decoder> {
+    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);
@@ -432,7 +493,14 @@ 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 decrypted_file = AudioDecrypt::new(key, encrypted_file);
+
+        let normalisation_data = NormalisationData::new(&mut decrypted_file);
+
+        let normalisation_factor: f32 = NormalisationData::get_factor(&self.config, normalisation_data);
+
+        let audio_file = Subfile::new(decrypted_file, 0xa7);
 
         let mut decoder = VorbisDecoder::new(audio_file).unwrap();
 
@@ -443,7 +511,7 @@ impl PlayerInternal {
 
         info!("Track \"{}\" loaded", track.name);
 
-        Some(decoder)
+        Some((decoder, normalisation_factor))
     }
 }
 

+ 7 - 1
src/main.rs

@@ -102,7 +102,9 @@ fn setup(args: &[String]) -> Setup {
         .optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE")
         .optopt("", "mixer", "Mixer to use", "MIXER")
         .optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME")
-        .optopt("", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT");
+        .optopt("", "zeroconf-port", "The port the internal server advertised over zeroconf uses.", "ZEROCONF_PORT")
+        .optflag("", "enable-volume-normalisation", "Play all tracks at the same volume")
+        .optopt("", "normalisation-pregain", "Pregain (dB) applied by volume normalisation", "PREGAIN");
 
     let matches = match opts.parse(&args[1..]) {
         Ok(m) => m,
@@ -187,6 +189,10 @@ fn setup(args: &[String]) -> Setup {
             bitrate: bitrate,
             onstart: matches.opt_str("onstart"),
             onstop: matches.opt_str("onstop"),
+            normalisation: matches.opt_present("enable-volume-normalisation"),
+            normalisation_pregain: matches.opt_str("normalisation-pregain")
+                .map(|pregain| pregain.parse::<f32>().expect("Invalid pregain float value"))
+                .unwrap_or(PlayerConfig::default().normalisation_pregain),
         }
     };