Add volume normalisation support

This commit is contained in:
Sasha Hilton 2018-02-23 05:08:41 +01:00
parent 685fb4e345
commit ff1f7e1d58
6 changed files with 94 additions and 10 deletions

1
Cargo.lock generated
View file

@ -413,6 +413,7 @@ name = "librespot-playback"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"alsa 0.0.1 (git+https://github.com/plietar/rust-alsa)", "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)", "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)", "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)", "libc 0.2.36 (registry+https://github.com/rust-lang/crates.io-index)",

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
use byteorder::{LittleEndian, ReadBytesExt};
use futures::sync::oneshot; use futures::sync::oneshot;
use futures::{future, Future}; use futures::{future, Future};
use std; use std;
@ -43,6 +44,14 @@ enum PlayerCommand {
Seek(u32), Seek(u32),
} }
#[derive(Debug)]
struct NormalisationConfig {
track_gain_db: f32,
track_peak: f32,
album_gain_db: f32,
album_peak: f32,
}
impl Player { impl Player {
pub fn new<F>(config: PlayerConfig, session: Session, pub fn new<F>(config: PlayerConfig, session: Session,
audio_filter: Option<Box<AudioFilter + Send>>, audio_filter: Option<Box<AudioFilter + Send>>,
@ -123,10 +132,12 @@ enum PlayerState {
Paused { Paused {
decoder: Decoder, decoder: Decoder,
end_of_track: oneshot::Sender<()>, end_of_track: oneshot::Sender<()>,
normalisation_factor: f32,
}, },
Playing { Playing {
decoder: Decoder, decoder: Decoder,
end_of_track: oneshot::Sender<()>, end_of_track: oneshot::Sender<()>,
normalisation_factor: f32,
}, },
Invalid, Invalid,
@ -168,10 +179,11 @@ impl PlayerState {
fn paused_to_playing(&mut self) { fn paused_to_playing(&mut self) {
use self::PlayerState::*; use self::PlayerState::*;
match ::std::mem::replace(self, Invalid) { match ::std::mem::replace(self, Invalid) {
Paused { decoder, end_of_track } => { Paused { decoder, end_of_track, normalisation_factor } => {
*self = Playing { *self = Playing {
decoder: decoder, decoder: decoder,
end_of_track: end_of_track, end_of_track: end_of_track,
normalisation_factor: normalisation_factor,
}; };
} }
_ => panic!("invalid state"), _ => panic!("invalid state"),
@ -181,10 +193,11 @@ impl PlayerState {
fn playing_to_paused(&mut self) { fn playing_to_paused(&mut self) {
use self::PlayerState::*; use self::PlayerState::*;
match ::std::mem::replace(self, Invalid) { match ::std::mem::replace(self, Invalid) {
Playing { decoder, end_of_track } => { Playing { decoder, end_of_track, normalisation_factor } => {
*self = Paused { *self = Paused {
decoder: decoder, decoder: decoder,
end_of_track: end_of_track, end_of_track: end_of_track,
normalisation_factor: normalisation_factor,
}; };
} }
_ => panic!("invalid state"), _ => panic!("invalid state"),
@ -228,14 +241,17 @@ impl PlayerInternal {
} }
if self.sink_running { 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")) Some(decoder.next_packet().expect("Vorbis error"))
} else { } else {
None None
}; };
if let Some(packet) = packet { if let Some(packet) = packet {
self.handle_packet(packet); self.handle_packet(packet, current_normalisation_factor);
} }
} }
} }
@ -259,13 +275,19 @@ impl PlayerInternal {
self.sink_running = false; 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 { match packet {
Some(mut packet) => { Some(mut packet) => {
if let Some(ref editor) = self.audio_filter { if let Some(ref editor) = self.audio_filter {
editor.modify_stream(&mut packet.data_mut()) 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()) { if let Err(err) = self.sink.write(&packet.data()) {
error!("Could not write audio: {}", err); error!("Could not write audio: {}", err);
self.stop_sink(); self.stop_sink();
@ -291,7 +313,7 @@ impl PlayerInternal {
} }
match self.load_track(track_id, position as i64) { match self.load_track(track_id, position as i64) {
Some(decoder) => { Some((decoder, normalisation_factor)) => {
if play { if play {
if !self.state.is_playing() { if !self.state.is_playing() {
self.run_onstart(); self.run_onstart();
@ -301,6 +323,7 @@ impl PlayerInternal {
self.state = PlayerState::Playing { self.state = PlayerState::Playing {
decoder: decoder, decoder: decoder,
end_of_track: end_of_track, end_of_track: end_of_track,
normalisation_factor: normalisation_factor,
}; };
} else { } else {
if self.state.is_playing() { if self.state.is_playing() {
@ -310,6 +333,7 @@ impl PlayerInternal {
self.state = PlayerState::Paused { self.state = PlayerState::Paused {
decoder: decoder, decoder: decoder,
end_of_track: end_of_track, end_of_track: end_of_track,
normalisation_factor: normalisation_factor,
}; };
} }
} }
@ -402,7 +426,37 @@ impl PlayerInternal {
} }
} }
fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<Decoder> { fn parse_normalisation<T: Read + Seek>(&self, file: &mut AudioDecrypt<T>) -> NormalisationConfig {
let track_gain_db: f32;
let track_peak: f32;
let album_gain_db: f32;
let album_peak: f32;
file.seek(SeekFrom::Start(144)).unwrap();
track_gain_db = file.read_f32::<LittleEndian>().unwrap();
debug!("Track gain: {}db", track_gain_db);
file.seek(SeekFrom::Start(148)).unwrap();
track_peak = file.read_f32::<LittleEndian>().unwrap();
debug!("Track peak: {}", track_peak);
file.seek(SeekFrom::Start(152)).unwrap();
album_gain_db = file.read_f32::<LittleEndian>().unwrap();
debug!("Album gain: {}db", album_gain_db);
file.seek(SeekFrom::Start(156)).unwrap();
album_peak = file.read_f32::<LittleEndian>().unwrap();
debug!("Album peak: {}", album_peak);
NormalisationConfig {
track_gain_db: track_gain_db,
track_peak: track_peak,
album_gain_db: album_gain_db,
album_peak: album_peak,
}
}
fn load_track(&self, track_id: SpotifyId, position: i64) -> Option<(Decoder, f32)> {
let track = Track::get(&self.session, track_id).wait().unwrap(); let track = Track::get(&self.session, track_id).wait().unwrap();
info!("Loading track \"{}\"", track.name); info!("Loading track \"{}\"", track.name);
@ -432,7 +486,24 @@ impl PlayerInternal {
let key = self.session.audio_key().request(track.id, file_id).wait().unwrap(); 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 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 mut normalisation_factor: f32 = 1.0;
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);
if normalisation_factor * normalisation_config.track_peak > 1.0 {
debug!("Reducing normalisation factor to prevent clipping. Please add negative pregain to avoid.");
normalisation_factor = 1.0 / normalisation_config.track_peak;
}
debug!("Applied normalization factor: {}", normalisation_factor);
}
let audio_file = Subfile::new(decrypted_file, 0xa7);
let mut decoder = VorbisDecoder::new(audio_file).unwrap(); let mut decoder = VorbisDecoder::new(audio_file).unwrap();
@ -443,7 +514,7 @@ impl PlayerInternal {
info!("Track \"{}\" loaded", track.name); info!("Track \"{}\" loaded", track.name);
Some(decoder) Some((decoder, normalisation_factor))
} }
} }

View file

@ -102,7 +102,9 @@ fn setup(args: &[String]) -> Setup {
.optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE") .optopt("", "device", "Audio device to use. Use '?' to list options if using portaudio", "DEVICE")
.optopt("", "mixer", "Mixer to use", "MIXER") .optopt("", "mixer", "Mixer to use", "MIXER")
.optopt("", "initial-volume", "Initial volume in %, once connected (must be from 0 to 100)", "VOLUME") .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..]) { let matches = match opts.parse(&args[1..]) {
Ok(m) => m, Ok(m) => m,
@ -187,6 +189,10 @@ fn setup(args: &[String]) -> Setup {
bitrate: bitrate, bitrate: bitrate,
onstart: matches.opt_str("onstart"), onstart: matches.opt_str("onstart"),
onstop: matches.opt_str("onstop"), 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),
} }
}; };