use std::time::Duration;
use rodio::{source::SeekError, Sample, Source};
use crate::util::{self, ToF32, ZERO_DB};
pub fn normalize<I>(
input: I,
ratio: f32,
threshold: f32,
knee_width: f32,
attack: Duration,
release: Duration,
) -> Normalize<I>
where
I: Source,
I::Item: Sample,
{
let sample_rate = input.sample_rate();
let attack = duration_to_coefficient(attack, sample_rate);
let release = duration_to_coefficient(release, sample_rate);
let channels = input.channels() as usize;
let base = NormalizeBase::new(ratio, threshold, knee_width, attack, release);
match channels {
1 => Normalize::Mono(NormalizeMono {
input,
base,
normalisation_integrator: ZERO_DB,
normalisation_peak: ZERO_DB,
}),
2 => Normalize::Stereo(NormalizeStereo {
input,
base,
normalisation_integrators: [ZERO_DB; 2],
normalisation_peaks: [ZERO_DB; 2],
position: 0,
}),
n => Normalize::MultiChannel(NormalizeMulti {
input,
base,
normalisation_integrators: vec![ZERO_DB; n],
normalisation_peaks: vec![ZERO_DB; n],
position: 0,
}),
}
}
#[must_use]
fn duration_to_coefficient(duration: Duration, sample_rate: u32) -> f32 {
f32::exp(-1.0 / (duration.as_secs_f32() * sample_rate.to_f32_lossy()))
}
#[derive(Clone, Debug)]
pub enum Normalize<I>
where
I: Source,
I::Item: Sample,
{
Mono(NormalizeMono<I>),
Stereo(NormalizeStereo<I>),
MultiChannel(NormalizeMulti<I>),
}
#[derive(Clone, Debug)]
struct NormalizeBase {
ratio: f32,
threshold: f32,
knee_width: f32,
attack: f32,
release: f32,
}
#[derive(Clone, Debug)]
pub struct NormalizeMono<I> {
input: I,
base: NormalizeBase,
normalisation_integrator: f32,
normalisation_peak: f32,
}
#[derive(Clone, Debug)]
pub struct NormalizeStereo<I> {
input: I,
base: NormalizeBase,
normalisation_integrators: [f32; 2],
normalisation_peaks: [f32; 2],
position: u8,
}
#[derive(Clone, Debug)]
pub struct NormalizeMulti<I> {
input: I,
base: NormalizeBase,
normalisation_integrators: Vec<f32>,
normalisation_peaks: Vec<f32>,
position: usize,
}
#[inline]
fn process_sample<S: Sample>(sample: S, threshold: f32, knee_width: f32) -> f32 {
let sample_f32 = sample.to_f32() + f32::MIN_POSITIVE;
let bias_db = util::ratio_to_db(sample_f32.abs()) - threshold;
let knee_boundary_db = bias_db * 2.0;
if knee_boundary_db < -knee_width {
ZERO_DB
} else if knee_boundary_db.abs() <= knee_width {
(knee_boundary_db + knee_width).powi(2) / (8.0 * knee_width)
} else {
bias_db
}
}
impl NormalizeBase {
fn new(ratio: f32, threshold: f32, knee_width: f32, attack: f32, release: f32) -> Self {
Self {
ratio,
threshold,
knee_width,
attack,
release,
}
}
#[inline]
fn process_channel<S: Sample>(&self, sample: S, integrator: &mut f32, peak: &mut f32) {
let sample = sample.amplify(self.ratio);
let limiter_db = process_sample(sample, self.threshold, self.knee_width);
*integrator = f32::max(
limiter_db,
self.release * *integrator + (1.0 - self.release) * limiter_db,
);
*peak = self.attack * *peak + (1.0 - self.attack) * *integrator;
}
}
impl<I> NormalizeMono<I>
where
I: Source,
I::Item: Sample,
{
#[inline]
fn process_next(&mut self, sample: I::Item) -> I::Item {
self.base.process_channel(
sample,
&mut self.normalisation_integrator,
&mut self.normalisation_peak,
);
sample.amplify(util::db_to_ratio(-self.normalisation_peak))
}
}
impl<I> NormalizeStereo<I>
where
I: Source,
I::Item: Sample,
{
#[inline]
fn process_next(&mut self, sample: I::Item) -> I::Item {
let channel = self.position as usize;
self.position ^= 1;
self.base.process_channel(
sample,
&mut self.normalisation_integrators[channel],
&mut self.normalisation_peaks[channel],
);
let max_peak = f32::max(self.normalisation_peaks[0], self.normalisation_peaks[1]);
sample.amplify(util::db_to_ratio(-max_peak))
}
}
impl<I> NormalizeMulti<I>
where
I: Source,
I::Item: Sample,
{
#[inline]
fn process_next(&mut self, sample: I::Item) -> I::Item {
let channel = self.position;
self.position = (self.position + 1) % self.normalisation_integrators.len();
self.base.process_channel(
sample,
&mut self.normalisation_integrators[channel],
&mut self.normalisation_peaks[channel],
);
let max_peak = self
.normalisation_peaks
.iter()
.fold(ZERO_DB, |max, &peak| f32::max(max, peak));
sample.amplify(util::db_to_ratio(-max_peak))
}
}
impl<I> Normalize<I>
where
I: Source,
I::Item: Sample,
{
#[inline]
pub fn inner(&self) -> &I {
match self {
Normalize::Mono(mono) => &mono.input,
Normalize::Stereo(stereo) => &stereo.input,
Normalize::MultiChannel(multi) => &multi.input,
}
}
#[inline]
pub fn inner_mut(&mut self) -> &mut I {
match self {
Normalize::Mono(mono) => &mut mono.input,
Normalize::Stereo(stereo) => &mut stereo.input,
Normalize::MultiChannel(multi) => &mut multi.input,
}
}
#[inline]
pub fn into_inner(self) -> I {
match self {
Normalize::Mono(mono) => mono.input,
Normalize::Stereo(stereo) => stereo.input,
Normalize::MultiChannel(multi) => multi.input,
}
}
}
impl<I> Iterator for Normalize<I>
where
I: Source,
I::Item: Sample,
{
type Item = I::Item;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
match self {
Normalize::Mono(mono) => {
let sample = mono.input.next()?;
Some(mono.process_next(sample))
}
Normalize::Stereo(stereo) => {
let sample = stereo.input.next()?;
Some(stereo.process_next(sample))
}
Normalize::MultiChannel(multi) => {
let sample = multi.input.next()?;
Some(multi.process_next(sample))
}
}
}
#[inline]
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner().size_hint()
}
}
impl<I> Source for Normalize<I>
where
I: Source,
I::Item: Sample,
{
#[inline]
fn current_frame_len(&self) -> Option<usize> {
self.inner().current_frame_len()
}
fn channels(&self) -> u16 {
self.inner().channels()
}
fn sample_rate(&self) -> u32 {
self.inner().sample_rate()
}
fn total_duration(&self) -> Option<Duration> {
self.inner().total_duration()
}
fn try_seek(&mut self, target: Duration) -> Result<(), SeekError> {
self.inner_mut().try_seek(target)?;
match self {
Normalize::Mono(mono) => {
mono.normalisation_integrator = ZERO_DB;
mono.normalisation_peak = ZERO_DB;
}
Normalize::Stereo(stereo) => {
stereo.normalisation_integrators.fill(ZERO_DB);
stereo.normalisation_peaks.fill(ZERO_DB);
}
Normalize::MultiChannel(multi) => {
multi.normalisation_integrators.fill(ZERO_DB);
multi.normalisation_peaks.fill(ZERO_DB);
}
}
Ok(())
}
}