2022-01-14 22:31:29 +00:00
|
|
|
use std::{mem, str::FromStr, time::Duration};
|
2021-05-16 20:30:35 +00:00
|
|
|
|
2022-01-14 22:31:29 +00:00
|
|
|
pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer};
|
2023-06-22 01:29:22 +00:00
|
|
|
use crate::{convert::i24, player::duration_to_coefficient, RESAMPLER_INPUT_SIZE, SAMPLE_RATE};
|
2023-06-22 00:51:14 +00:00
|
|
|
|
|
|
|
// Reciprocals allow us to multiply instead of divide during interpolation.
|
|
|
|
const HZ48000_RESAMPLE_FACTOR_RECIPROCAL: f64 = SAMPLE_RATE as f64 / 48_000.0;
|
|
|
|
const HZ88200_RESAMPLE_FACTOR_RECIPROCAL: f64 = SAMPLE_RATE as f64 / 88_200.0;
|
|
|
|
const HZ96000_RESAMPLE_FACTOR_RECIPROCAL: f64 = SAMPLE_RATE as f64 / 96_000.0;
|
|
|
|
|
|
|
|
// sample rate * channels
|
|
|
|
const HZ44100_SAMPLES_PER_SECOND: f64 = 44_100.0 * 2.0;
|
|
|
|
const HZ48000_SAMPLES_PER_SECOND: f64 = 48_000.0 * 2.0;
|
|
|
|
const HZ88200_SAMPLES_PER_SECOND: f64 = 88_200.0 * 2.0;
|
|
|
|
const HZ96000_SAMPLES_PER_SECOND: f64 = 96_000.0 * 2.0;
|
|
|
|
|
|
|
|
// Given a RESAMPLER_INPUT_SIZE of 147 all of our output sizes work out
|
|
|
|
// to be integers, which is a very good thing. That means no fractional samples
|
2023-06-22 01:29:22 +00:00
|
|
|
// which translates to much better interpolation.
|
2023-06-22 00:51:14 +00:00
|
|
|
const HZ48000_INTERPOLATION_OUTPUT_SIZE: usize =
|
|
|
|
(RESAMPLER_INPUT_SIZE as f64 * (1.0 / HZ48000_RESAMPLE_FACTOR_RECIPROCAL)) as usize;
|
|
|
|
const HZ88200_INTERPOLATION_OUTPUT_SIZE: usize =
|
|
|
|
(RESAMPLER_INPUT_SIZE as f64 * (1.0 / HZ88200_RESAMPLE_FACTOR_RECIPROCAL)) as usize;
|
|
|
|
const HZ96000_INTERPOLATION_OUTPUT_SIZE: usize =
|
|
|
|
(RESAMPLER_INPUT_SIZE as f64 * (1.0 / HZ96000_RESAMPLE_FACTOR_RECIPROCAL)) as usize;
|
|
|
|
|
|
|
|
// Blackman Window coefficients
|
|
|
|
const BLACKMAN_A0: f64 = 0.42;
|
|
|
|
const BLACKMAN_A1: f64 = 0.5;
|
|
|
|
const BLACKMAN_A2: f64 = 0.08;
|
|
|
|
|
|
|
|
// Constants for calculations
|
|
|
|
const TWO_TIMES_PI: f64 = 2.0 * std::f64::consts::PI;
|
|
|
|
const FOUR_TIMES_PI: f64 = 4.0 * std::f64::consts::PI;
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
|
|
pub enum InterpolationQuality {
|
|
|
|
#[default]
|
|
|
|
Low,
|
|
|
|
Medium,
|
|
|
|
High,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for InterpolationQuality {
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
use InterpolationQuality::*;
|
|
|
|
|
|
|
|
match s.to_lowercase().as_ref() {
|
|
|
|
"low" => Ok(Low),
|
|
|
|
"medium" => Ok(Medium),
|
|
|
|
"high" => Ok(High),
|
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for InterpolationQuality {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
use InterpolationQuality::*;
|
|
|
|
|
|
|
|
match self {
|
|
|
|
Low => write!(f, "Low"),
|
|
|
|
Medium => write!(f, "Medium"),
|
|
|
|
High => write!(f, "High"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl InterpolationQuality {
|
|
|
|
pub fn get_interpolation_coefficients(&self, resample_factor_reciprocal: f64) -> Vec<f64> {
|
|
|
|
let interpolation_coefficients_length = self.get_interpolation_coefficients_length();
|
|
|
|
|
|
|
|
let mut coefficients = Vec::with_capacity(interpolation_coefficients_length);
|
|
|
|
|
|
|
|
let last_index = interpolation_coefficients_length as f64 - 1.0;
|
|
|
|
|
|
|
|
let sinc_center = last_index * 0.5;
|
|
|
|
|
|
|
|
let mut coefficient_sum = 0.0;
|
|
|
|
|
|
|
|
coefficients.extend((0..interpolation_coefficients_length).map(
|
|
|
|
|interpolation_coefficient_index| {
|
|
|
|
let index_float = interpolation_coefficient_index as f64;
|
|
|
|
let sample_index_fractional = (index_float * resample_factor_reciprocal).fract();
|
|
|
|
let sinc_center_offset = index_float - sinc_center;
|
|
|
|
|
|
|
|
let sample_index_fractional_sinc_weight = Self::sinc(sample_index_fractional);
|
|
|
|
|
|
|
|
let sinc_value = Self::sinc(sinc_center_offset);
|
|
|
|
// Calculate the Blackman window function for the given center offset
|
|
|
|
// w(n) = A0 - A1*cos(2πn / (N-1)) + A2*cos(4πn / (N-1)),
|
|
|
|
// where n is the center offset, N is the window size,
|
|
|
|
// and A0, A1, A2 are precalculated coefficients
|
|
|
|
|
|
|
|
let two_pi_n = TWO_TIMES_PI * index_float;
|
|
|
|
let four_pi_n = FOUR_TIMES_PI * index_float;
|
|
|
|
|
|
|
|
let blackman_window_value = BLACKMAN_A0
|
|
|
|
- BLACKMAN_A1 * (two_pi_n / last_index).cos()
|
|
|
|
+ BLACKMAN_A2 * (four_pi_n / last_index).cos();
|
|
|
|
|
|
|
|
let sinc_window = sinc_value * blackman_window_value;
|
|
|
|
|
|
|
|
let coefficient = sinc_window * sample_index_fractional_sinc_weight;
|
|
|
|
|
|
|
|
coefficient_sum += coefficient;
|
|
|
|
|
|
|
|
coefficient
|
|
|
|
},
|
|
|
|
));
|
|
|
|
|
|
|
|
coefficients
|
|
|
|
.iter_mut()
|
|
|
|
.for_each(|coefficient| *coefficient /= coefficient_sum);
|
|
|
|
|
|
|
|
coefficients
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_interpolation_coefficients_length(&self) -> usize {
|
|
|
|
use InterpolationQuality::*;
|
|
|
|
match self {
|
|
|
|
Low => 0,
|
|
|
|
Medium => 129,
|
|
|
|
High => 257,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn sinc(x: f64) -> f64 {
|
|
|
|
if x.abs() < f64::EPSILON {
|
|
|
|
1.0
|
|
|
|
} else {
|
|
|
|
let pi_x = std::f64::consts::PI * x;
|
|
|
|
pi_x.sin() / pi_x
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
|
|
pub enum SampleRate {
|
|
|
|
#[default]
|
|
|
|
Hz44100,
|
|
|
|
Hz48000,
|
|
|
|
Hz88200,
|
|
|
|
Hz96000,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for SampleRate {
|
|
|
|
type Err = ();
|
|
|
|
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
use SampleRate::*;
|
|
|
|
|
|
|
|
// Match against both the actual
|
|
|
|
// stringified value and how most
|
|
|
|
// humans would write a sample rate.
|
|
|
|
match s.to_uppercase().as_ref() {
|
|
|
|
"hz44100" | "44100hz" | "44100" | "44.1khz" => Ok(Hz44100),
|
|
|
|
"hz48000" | "48000hz" | "48000" | "48khz" => Ok(Hz48000),
|
|
|
|
"hz88200" | "88200hz" | "88200" | "88.2khz" => Ok(Hz88200),
|
|
|
|
"hz96000" | "96000hz" | "96000" | "96khz" => Ok(Hz96000),
|
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl std::fmt::Display for SampleRate {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
use SampleRate::*;
|
|
|
|
|
|
|
|
match self {
|
|
|
|
// Let's make these more human readable.
|
|
|
|
// "Hz44100" is just awkward.
|
|
|
|
Hz44100 => write!(f, "44.1kHz"),
|
|
|
|
Hz48000 => write!(f, "48kHz"),
|
|
|
|
Hz88200 => write!(f, "88.2kHz"),
|
|
|
|
Hz96000 => write!(f, "96kHz"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Default)]
|
|
|
|
pub struct ResampleSpec {
|
2023-06-22 01:29:22 +00:00
|
|
|
pub resample_factor_reciprocal: f64,
|
|
|
|
pub interpolation_output_size: usize,
|
2023-06-22 00:51:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SampleRate {
|
|
|
|
pub fn as_u32(&self) -> u32 {
|
|
|
|
use SampleRate::*;
|
|
|
|
|
|
|
|
match self {
|
|
|
|
Hz44100 => 44100,
|
|
|
|
Hz48000 => 48000,
|
|
|
|
Hz88200 => 88200,
|
|
|
|
Hz96000 => 96000,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn duration_to_normalisation_coefficient(&self, duration: Duration) -> f64 {
|
|
|
|
(-1.0 / (duration.as_secs_f64() * self.samples_per_second())).exp()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn normalisation_coefficient_to_duration(&self, coefficient: f64) -> Duration {
|
|
|
|
Duration::from_secs_f64(-1.0 / coefficient.ln() / self.samples_per_second())
|
|
|
|
}
|
|
|
|
|
|
|
|
fn samples_per_second(&self) -> f64 {
|
|
|
|
use SampleRate::*;
|
|
|
|
|
|
|
|
match self {
|
|
|
|
Hz44100 => HZ44100_SAMPLES_PER_SECOND,
|
|
|
|
Hz48000 => HZ48000_SAMPLES_PER_SECOND,
|
|
|
|
Hz88200 => HZ88200_SAMPLES_PER_SECOND,
|
|
|
|
Hz96000 => HZ96000_SAMPLES_PER_SECOND,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn get_resample_spec(&self) -> ResampleSpec {
|
|
|
|
use SampleRate::*;
|
|
|
|
|
|
|
|
match self {
|
|
|
|
// Dummy values to satisfy
|
|
|
|
// the match statement.
|
|
|
|
// 44.1kHz will be bypassed.
|
|
|
|
Hz44100 => ResampleSpec {
|
|
|
|
resample_factor_reciprocal: 1.0,
|
|
|
|
interpolation_output_size: RESAMPLER_INPUT_SIZE,
|
|
|
|
},
|
|
|
|
Hz48000 => ResampleSpec {
|
|
|
|
resample_factor_reciprocal: HZ48000_RESAMPLE_FACTOR_RECIPROCAL,
|
|
|
|
interpolation_output_size: HZ48000_INTERPOLATION_OUTPUT_SIZE,
|
|
|
|
},
|
|
|
|
Hz88200 => ResampleSpec {
|
|
|
|
resample_factor_reciprocal: HZ88200_RESAMPLE_FACTOR_RECIPROCAL,
|
|
|
|
interpolation_output_size: HZ88200_INTERPOLATION_OUTPUT_SIZE,
|
|
|
|
},
|
|
|
|
Hz96000 => ResampleSpec {
|
|
|
|
resample_factor_reciprocal: HZ96000_RESAMPLE_FACTOR_RECIPROCAL,
|
|
|
|
interpolation_output_size: HZ96000_INTERPOLATION_OUTPUT_SIZE,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-02-11 15:13:42 +00:00
|
|
|
|
2023-06-22 00:24:40 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
2018-02-11 15:13:42 +00:00
|
|
|
pub enum Bitrate {
|
|
|
|
Bitrate96,
|
2023-06-22 00:24:40 +00:00
|
|
|
#[default]
|
2018-02-11 15:13:42 +00:00
|
|
|
Bitrate160,
|
|
|
|
Bitrate320,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for Bitrate {
|
|
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
match s {
|
2021-03-18 21:06:43 +00:00
|
|
|
"96" => Ok(Self::Bitrate96),
|
|
|
|
"160" => Ok(Self::Bitrate160),
|
|
|
|
"320" => Ok(Self::Bitrate320),
|
2018-02-11 15:13:42 +00:00
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-22 00:24:40 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
2021-03-12 22:05:38 +00:00
|
|
|
pub enum AudioFormat {
|
2021-05-30 18:09:39 +00:00
|
|
|
F64,
|
2021-03-12 22:05:38 +00:00
|
|
|
F32,
|
2021-03-13 22:43:24 +00:00
|
|
|
S32,
|
2021-03-16 23:00:27 +00:00
|
|
|
S24,
|
|
|
|
S24_3,
|
2023-06-22 00:24:40 +00:00
|
|
|
#[default]
|
2021-03-12 22:05:38 +00:00
|
|
|
S16,
|
|
|
|
}
|
|
|
|
|
2021-05-31 20:32:39 +00:00
|
|
|
impl FromStr for AudioFormat {
|
|
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
match s.to_uppercase().as_ref() {
|
2021-05-30 18:09:39 +00:00
|
|
|
"F64" => Ok(Self::F64),
|
2021-03-16 23:00:27 +00:00
|
|
|
"F32" => Ok(Self::F32),
|
|
|
|
"S32" => Ok(Self::S32),
|
|
|
|
"S24" => Ok(Self::S24),
|
|
|
|
"S24_3" => Ok(Self::S24_3),
|
|
|
|
"S16" => Ok(Self::S16),
|
|
|
|
_ => Err(()),
|
2021-03-12 22:05:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-16 23:00:27 +00:00
|
|
|
impl AudioFormat {
|
|
|
|
// not used by all backends
|
|
|
|
#[allow(dead_code)]
|
|
|
|
pub fn size(&self) -> usize {
|
|
|
|
match self {
|
2021-05-30 18:09:39 +00:00
|
|
|
Self::F64 => mem::size_of::<f64>(),
|
|
|
|
Self::F32 => mem::size_of::<f32>(),
|
2021-03-16 23:00:27 +00:00
|
|
|
Self::S24_3 => mem::size_of::<i24>(),
|
|
|
|
Self::S16 => mem::size_of::<i16>(),
|
2021-03-18 21:06:43 +00:00
|
|
|
_ => mem::size_of::<i32>(), // S32 and S24 are both stored in i32
|
2021-03-16 23:00:27 +00:00
|
|
|
}
|
2021-03-12 22:05:38 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-22 00:24:40 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
2021-01-21 19:16:05 +00:00
|
|
|
pub enum NormalisationType {
|
|
|
|
Album,
|
|
|
|
Track,
|
2023-06-22 00:24:40 +00:00
|
|
|
#[default]
|
2021-09-20 17:22:02 +00:00
|
|
|
Auto,
|
2021-01-21 19:16:05 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for NormalisationType {
|
|
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
2021-05-24 13:53:32 +00:00
|
|
|
match s.to_lowercase().as_ref() {
|
2021-03-18 21:06:43 +00:00
|
|
|
"album" => Ok(Self::Album),
|
|
|
|
"track" => Ok(Self::Track),
|
2021-09-20 17:22:02 +00:00
|
|
|
"auto" => Ok(Self::Auto),
|
2021-01-21 19:16:05 +00:00
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-22 00:24:40 +00:00
|
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
2021-02-24 20:39:42 +00:00
|
|
|
pub enum NormalisationMethod {
|
|
|
|
Basic,
|
2023-06-22 00:24:40 +00:00
|
|
|
#[default]
|
2021-02-24 20:39:42 +00:00
|
|
|
Dynamic,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for NormalisationMethod {
|
|
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
2021-05-24 13:53:32 +00:00
|
|
|
match s.to_lowercase().as_ref() {
|
2021-03-18 21:06:43 +00:00
|
|
|
"basic" => Ok(Self::Basic),
|
|
|
|
"dynamic" => Ok(Self::Dynamic),
|
2021-02-24 20:39:42 +00:00
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
Implement dithering (#694)
Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion.
Guidance:
- On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss.
- Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you.
- Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
|
|
|
#[derive(Clone)]
|
2018-02-11 15:13:42 +00:00
|
|
|
pub struct PlayerConfig {
|
|
|
|
pub bitrate: Bitrate,
|
2021-05-24 13:53:32 +00:00
|
|
|
pub gapless: bool,
|
Implement dithering (#694)
Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion.
Guidance:
- On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss.
- Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you.
- Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
|
|
|
pub passthrough: bool,
|
|
|
|
|
2018-02-23 19:08:20 +00:00
|
|
|
pub normalisation: bool,
|
2021-01-21 19:16:05 +00:00
|
|
|
pub normalisation_type: NormalisationType,
|
2021-02-24 20:39:42 +00:00
|
|
|
pub normalisation_method: NormalisationMethod,
|
2022-01-17 21:57:30 +00:00
|
|
|
pub normalisation_pregain_db: f64,
|
2022-01-14 22:31:29 +00:00
|
|
|
pub normalisation_threshold_dbfs: f64,
|
|
|
|
pub normalisation_attack_cf: f64,
|
|
|
|
pub normalisation_release_cf: f64,
|
|
|
|
pub normalisation_knee_db: f64,
|
Implement dithering (#694)
Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion.
Guidance:
- On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss.
- Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you.
- Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
|
|
|
|
|
|
|
// pass function pointers so they can be lazily instantiated *after* spawning a thread
|
|
|
|
// (thereby circumventing Send bounds that they might not satisfy)
|
|
|
|
pub ditherer: Option<DithererBuilder>,
|
2018-02-11 15:13:42 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for PlayerConfig {
|
Implement dithering (#694)
Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion.
Guidance:
- On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss.
- Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you.
- Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
|
|
|
fn default() -> Self {
|
|
|
|
Self {
|
2018-02-11 15:13:42 +00:00
|
|
|
bitrate: Bitrate::default(),
|
2021-05-24 13:53:32 +00:00
|
|
|
gapless: true,
|
2018-02-23 19:08:20 +00:00
|
|
|
normalisation: false,
|
2021-01-21 19:16:05 +00:00
|
|
|
normalisation_type: NormalisationType::default(),
|
2021-02-24 20:39:42 +00:00
|
|
|
normalisation_method: NormalisationMethod::default(),
|
2022-01-14 22:31:29 +00:00
|
|
|
normalisation_pregain_db: 0.0,
|
|
|
|
normalisation_threshold_dbfs: -2.0,
|
|
|
|
normalisation_attack_cf: duration_to_coefficient(Duration::from_millis(5)),
|
|
|
|
normalisation_release_cf: duration_to_coefficient(Duration::from_millis(100)),
|
|
|
|
normalisation_knee_db: 5.0,
|
2021-01-07 06:42:38 +00:00
|
|
|
passthrough: false,
|
Implement dithering (#694)
Dithering lowers digital-to-analog conversion ("requantization") error, linearizing output, lowering distortion and replacing it with a constant, fixed noise level, which is more pleasant to the ear than the distortion.
Guidance:
- On S24, S24_3 and S24, the default is to use triangular dithering. Depending on personal preference you may use Gaussian dithering instead; it's not as good objectively, but it may be preferred subjectively if you are looking for a more "analog" sound akin to tape hiss.
- Advanced users who know that they have a DAC without noise shaping have a third option: high-passed dithering, which is like triangular dithering except that it moves dithering noise up in frequency where it is less audible. Note: 99% of DACs are of delta-sigma design with noise shaping, so unless you have a multibit / R2R DAC, or otherwise know what you are doing, this is not for you.
- Don't dither or shape noise on S32 or F32. On F32 it's not supported anyway (there are no integer conversions and so no rounding errors) and on S32 the noise level is so far down that it is simply inaudible even after volume normalisation and control.
New command line option:
--dither DITHER Specify the dither algorithm to use - [none, gpdf,
tpdf, tpdf_hp]. Defaults to 'tpdf' for formats S16
S24, S24_3 and 'none' for other formats.
Notes:
This PR also features some opportunistic improvements. Worthy of mention are:
- matching reference Vorbis sample conversion techniques for lower noise
- a cleanup of the convert API
2021-05-26 19:19:17 +00:00
|
|
|
ditherer: Some(mk_ditherer::<TriangularDitherer>),
|
2018-02-11 15:13:42 +00:00
|
|
|
}
|
|
|
|
}
|
2018-02-26 01:50:41 +00:00
|
|
|
}
|
2021-05-24 13:53:32 +00:00
|
|
|
|
|
|
|
// fields are intended for volume control range in dB
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
|
|
pub enum VolumeCtrl {
|
2021-05-30 18:09:39 +00:00
|
|
|
Cubic(f64),
|
2021-05-24 13:53:32 +00:00
|
|
|
Fixed,
|
|
|
|
Linear,
|
2021-05-30 18:09:39 +00:00
|
|
|
Log(f64),
|
2021-05-24 13:53:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl FromStr for VolumeCtrl {
|
|
|
|
type Err = ();
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
Self::from_str_with_range(s, Self::DEFAULT_DB_RANGE)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Default for VolumeCtrl {
|
|
|
|
fn default() -> VolumeCtrl {
|
|
|
|
VolumeCtrl::Log(Self::DEFAULT_DB_RANGE)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl VolumeCtrl {
|
2021-05-31 20:32:39 +00:00
|
|
|
pub const MAX_VOLUME: u16 = u16::MAX;
|
2021-05-24 13:53:32 +00:00
|
|
|
|
|
|
|
// Taken from: https://www.dr-lex.be/info-stuff/volumecontrols.html
|
2021-05-30 18:09:39 +00:00
|
|
|
pub const DEFAULT_DB_RANGE: f64 = 60.0;
|
2021-05-24 13:53:32 +00:00
|
|
|
|
2021-05-30 18:09:39 +00:00
|
|
|
pub fn from_str_with_range(s: &str, db_range: f64) -> Result<Self, <Self as FromStr>::Err> {
|
2021-05-24 13:53:32 +00:00
|
|
|
use self::VolumeCtrl::*;
|
|
|
|
match s.to_lowercase().as_ref() {
|
|
|
|
"cubic" => Ok(Cubic(db_range)),
|
|
|
|
"fixed" => Ok(Fixed),
|
|
|
|
"linear" => Ok(Linear),
|
|
|
|
"log" => Ok(Log(db_range)),
|
|
|
|
_ => Err(()),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|