mirror of
https://github.com/librespot-org/librespot.git
synced 2024-12-18 17:11:53 +00:00
Add normaliser
This commit is contained in:
parent
3b64f25286
commit
375f83797a
2 changed files with 354 additions and 0 deletions
|
@ -11,12 +11,23 @@ pub mod convert;
|
|||
pub mod decoder;
|
||||
pub mod dither;
|
||||
pub mod mixer;
|
||||
pub mod normaliser;
|
||||
pub mod player;
|
||||
pub mod resampler;
|
||||
|
||||
pub const DB_VOLTAGE_RATIO: f64 = 20.0;
|
||||
pub const PCM_AT_0DBFS: f64 = 1.0;
|
||||
pub const RESAMPLER_INPUT_SIZE: usize = 147;
|
||||
pub const SAMPLE_RATE: u32 = 44100;
|
||||
pub const NUM_CHANNELS: u8 = 2;
|
||||
pub const SAMPLES_PER_SECOND: u32 = SAMPLE_RATE * NUM_CHANNELS as u32;
|
||||
pub const PAGES_PER_MS: f64 = SAMPLE_RATE as f64 / 1000.0;
|
||||
pub const MS_PER_PAGE: f64 = 1000.0 / SAMPLE_RATE as f64;
|
||||
|
||||
pub fn db_to_ratio(db: f64) -> f64 {
|
||||
f64::powf(10.0, db / DB_VOLTAGE_RATIO)
|
||||
}
|
||||
|
||||
pub fn ratio_to_db(ratio: f64) -> f64 {
|
||||
ratio.log10() * DB_VOLTAGE_RATIO
|
||||
}
|
||||
|
|
343
playback/src/normaliser.rs
Normal file
343
playback/src/normaliser.rs
Normal file
|
@ -0,0 +1,343 @@
|
|||
use crate::{
|
||||
config::{NormalisationMethod, NormalisationType, PlayerConfig},
|
||||
db_to_ratio,
|
||||
decoder::AudioPacket,
|
||||
mixer::VolumeGetter,
|
||||
player::NormalisationData,
|
||||
ratio_to_db, PCM_AT_0DBFS,
|
||||
};
|
||||
|
||||
struct NoNormalisation;
|
||||
|
||||
impl NoNormalisation {
|
||||
fn normalise(samples: &[f64], volume: f64) -> Vec<f64> {
|
||||
if volume < 1.0 {
|
||||
let mut output = Vec::with_capacity(samples.len());
|
||||
|
||||
output.extend(samples.iter().map(|sample| sample * volume));
|
||||
|
||||
output
|
||||
} else {
|
||||
samples.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BasicNormalisation;
|
||||
|
||||
impl BasicNormalisation {
|
||||
fn normalise(samples: &[f64], volume: f64, factor: f64) -> Vec<f64> {
|
||||
if volume < 1.0 || factor < 1.0 {
|
||||
let mut output = Vec::with_capacity(samples.len());
|
||||
|
||||
output.extend(samples.iter().map(|sample| sample * factor * volume));
|
||||
|
||||
output
|
||||
} else {
|
||||
samples.to_vec()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
struct DynamicNormalisation {
|
||||
threshold_db: f64,
|
||||
attack_cf: f64,
|
||||
release_cf: f64,
|
||||
knee_db: f64,
|
||||
integrator: f64,
|
||||
peak: f64,
|
||||
}
|
||||
|
||||
impl DynamicNormalisation {
|
||||
fn new(config: &PlayerConfig) -> Self {
|
||||
// as_millis() has rounding errors (truncates)
|
||||
debug!(
|
||||
"Normalisation Attack: {:.0} ms",
|
||||
config
|
||||
.sample_rate
|
||||
.normalisation_coefficient_to_duration(config.normalisation_attack_cf)
|
||||
.as_secs_f64()
|
||||
* 1000.
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Normalisation Release: {:.0} ms",
|
||||
config
|
||||
.sample_rate
|
||||
.normalisation_coefficient_to_duration(config.normalisation_release_cf)
|
||||
.as_secs_f64()
|
||||
* 1000.
|
||||
);
|
||||
|
||||
Self {
|
||||
threshold_db: config.normalisation_threshold_dbfs,
|
||||
attack_cf: config.normalisation_attack_cf,
|
||||
release_cf: config.normalisation_release_cf,
|
||||
knee_db: config.normalisation_knee_db,
|
||||
integrator: 0.0,
|
||||
peak: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
self.integrator = 0.0;
|
||||
self.peak = 0.0;
|
||||
}
|
||||
|
||||
fn normalise(&mut self, samples: &[f64], volume: f64, factor: f64) -> Vec<f64> {
|
||||
let mut output = Vec::with_capacity(samples.len());
|
||||
|
||||
output.extend(samples.iter().map(|sample| {
|
||||
let mut sample = sample * 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()) - self.threshold_db;
|
||||
let knee_boundary_db = bias_db * 2.0;
|
||||
|
||||
if knee_boundary_db < -self.knee_db {
|
||||
0.0
|
||||
} else if knee_boundary_db.abs() <= self.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 + self.knee_db).powi(2) / (8.0 * self.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.integrator > 0.0 || self.peak > 0.0 {
|
||||
// step 5: smooth, decoupled peak detector
|
||||
// Textbook:
|
||||
// release_cf * integrator + (1.0 - release_cf) * limiter_db
|
||||
// Simplifies to:
|
||||
// release_cf * integrator - release_cf * limiter_db + limiter_db
|
||||
self.integrator = limiter_db.max(
|
||||
self.release_cf * self.integrator - self.release_cf * limiter_db + limiter_db,
|
||||
);
|
||||
// Textbook:
|
||||
// attack_cf * peak + (1.0 - attack_cf) * integrator
|
||||
// Simplifies to:
|
||||
// attack_cf * peak - attack_cf * integrator + integrator
|
||||
self.peak =
|
||||
self.attack_cf * self.peak - self.attack_cf * self.integrator + self.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.peak);
|
||||
}
|
||||
|
||||
sample * volume
|
||||
}));
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
enum Normalisation {
|
||||
None,
|
||||
Basic,
|
||||
Dynamic(DynamicNormalisation),
|
||||
}
|
||||
|
||||
impl Normalisation {
|
||||
fn new(config: &PlayerConfig) -> Self {
|
||||
if !config.normalisation {
|
||||
Normalisation::None
|
||||
} else {
|
||||
debug!("Normalisation Type: {:?}", config.normalisation_type);
|
||||
debug!(
|
||||
"Normalisation Pregain: {:.1} dB",
|
||||
config.normalisation_pregain_db
|
||||
);
|
||||
|
||||
debug!(
|
||||
"Normalisation Threshold: {:.1} dBFS",
|
||||
config.normalisation_threshold_dbfs
|
||||
);
|
||||
|
||||
debug!("Normalisation Method: {:?}", config.normalisation_method);
|
||||
|
||||
match config.normalisation_method {
|
||||
NormalisationMethod::Dynamic => {
|
||||
Normalisation::Dynamic(DynamicNormalisation::new(config))
|
||||
}
|
||||
NormalisationMethod::Basic => Normalisation::Basic,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stop(&mut self) {
|
||||
if let Normalisation::Dynamic(ref mut d) = self {
|
||||
d.stop()
|
||||
}
|
||||
}
|
||||
|
||||
fn normalise(&mut self, samples: &[f64], volume: f64, factor: f64) -> Vec<f64> {
|
||||
use Normalisation::*;
|
||||
|
||||
match self {
|
||||
None => NoNormalisation::normalise(samples, volume),
|
||||
Basic => BasicNormalisation::normalise(samples, volume, factor),
|
||||
Dynamic(ref mut d) => d.normalise(samples, volume, factor),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Normaliser {
|
||||
normalisation: Normalisation,
|
||||
volume_getter: Box<dyn VolumeGetter>,
|
||||
normalisation_type: NormalisationType,
|
||||
pregain_db: f64,
|
||||
threshold_dbfs: f64,
|
||||
factor: f64,
|
||||
}
|
||||
|
||||
impl Normaliser {
|
||||
pub fn new(config: &PlayerConfig, volume_getter: Box<dyn VolumeGetter>) -> Self {
|
||||
Self {
|
||||
normalisation: Normalisation::new(config),
|
||||
volume_getter,
|
||||
normalisation_type: config.normalisation_type,
|
||||
pregain_db: config.normalisation_pregain_db,
|
||||
threshold_dbfs: config.normalisation_threshold_dbfs,
|
||||
factor: 1.0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn normalise(&mut self, samples: &[f64]) -> AudioPacket {
|
||||
let volume = self.volume_getter.attenuation_factor();
|
||||
|
||||
AudioPacket::Samples(self.normalisation.normalise(samples, volume, self.factor))
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
self.normalisation.stop();
|
||||
}
|
||||
|
||||
pub fn set_factor(&mut self, auto_normalise_as_album: bool, data: NormalisationData) {
|
||||
if self.normalisation != Normalisation::None {
|
||||
self.factor = self.get_factor(auto_normalise_as_album, data);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_factor(&self, auto_normalise_as_album: bool, data: NormalisationData) -> f64 {
|
||||
let (gain_db, gain_peak, norm_type) = match self.normalisation_type {
|
||||
NormalisationType::Album => (
|
||||
data.album_gain_db,
|
||||
data.album_peak,
|
||||
NormalisationType::Album,
|
||||
),
|
||||
NormalisationType::Track => (
|
||||
data.track_gain_db,
|
||||
data.track_peak,
|
||||
NormalisationType::Track,
|
||||
),
|
||||
NormalisationType::Auto => {
|
||||
if auto_normalise_as_album {
|
||||
(
|
||||
data.album_gain_db,
|
||||
data.album_peak,
|
||||
NormalisationType::Album,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
data.track_gain_db,
|
||||
data.track_peak,
|
||||
NormalisationType::Track,
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// As per the ReplayGain 1.0 & 2.0 (proposed) spec:
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Clipping_prevention
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Clipping_prevention
|
||||
let normalisation_factor = if self.normalisation == Normalisation::Basic {
|
||||
// For Basic Normalisation, factor = min(ratio of (ReplayGain + PreGain), 1.0 / peak level).
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_1.0_specification#Peak_amplitude
|
||||
// https://wiki.hydrogenaud.io/index.php?title=ReplayGain_2.0_specification#Peak_amplitude
|
||||
// We then limit that to 1.0 as not to exceed dBFS (0.0 dB).
|
||||
let factor = f64::min(
|
||||
db_to_ratio(gain_db + self.pregain_db),
|
||||
PCM_AT_0DBFS / gain_peak,
|
||||
);
|
||||
|
||||
if factor > PCM_AT_0DBFS {
|
||||
info!(
|
||||
"Lowering gain by {:.2} dB for the duration of this track to avoid potentially exceeding dBFS.",
|
||||
ratio_to_db(factor)
|
||||
);
|
||||
|
||||
PCM_AT_0DBFS
|
||||
} else {
|
||||
factor
|
||||
}
|
||||
} else {
|
||||
// For Dynamic Normalisation it's up to the player to decide,
|
||||
// factor = ratio of (ReplayGain + PreGain).
|
||||
// We then let the dynamic limiter handle gain reduction.
|
||||
let factor = db_to_ratio(gain_db + self.pregain_db);
|
||||
let threshold_ratio = db_to_ratio(self.threshold_dbfs);
|
||||
|
||||
if factor > PCM_AT_0DBFS {
|
||||
let factor_db = gain_db + self.pregain_db;
|
||||
let limiting_db = factor_db + self.threshold_dbfs.abs();
|
||||
|
||||
warn!(
|
||||
"This track may exceed dBFS by {:.2} dB and be subject to {:.2} dB of dynamic limiting at it's peak.",
|
||||
factor_db, limiting_db
|
||||
);
|
||||
} else if factor > threshold_ratio {
|
||||
let limiting_db = gain_db + self.pregain_db + self.threshold_dbfs.abs();
|
||||
|
||||
info!(
|
||||
"This track may be subject to {:.2} dB of dynamic limiting at it's peak.",
|
||||
limiting_db
|
||||
);
|
||||
}
|
||||
|
||||
factor
|
||||
};
|
||||
|
||||
debug!("Normalisation Data: {:?}", data);
|
||||
debug!(
|
||||
"Calculated Normalisation Factor for {:?}: {:.2}%",
|
||||
norm_type,
|
||||
normalisation_factor * 100.0
|
||||
);
|
||||
|
||||
normalisation_factor
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue