diff --git a/playback/src/config.rs b/playback/src/config.rs index 8c64191f..a9a47e7d 100644 --- a/playback/src/config.rs +++ b/playback/src/config.rs @@ -1,7 +1,248 @@ use std::{mem, str::FromStr, time::Duration}; pub use crate::dither::{mk_ditherer, DithererBuilder, TriangularDitherer}; -use crate::{convert::i24, player::duration_to_coefficient}; +use crate::{SAMPLE_RATE, RESAMPLER_INPUT_SIZE, convert::i24, player::duration_to_coefficient}; + +// 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 +// which translates to much better interpolation. +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 { + 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 { + 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 { + 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 { + resample_factor_reciprocal: f64, + interpolation_output_size: usize, +} + +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, + }, + } + } +} #[derive(Clone, Copy, Debug, Default, Hash, PartialOrd, Ord, PartialEq, Eq)] pub enum Bitrate { diff --git a/playback/src/lib.rs b/playback/src/lib.rs index 43a5b4f0..48fdb105 100644 --- a/playback/src/lib.rs +++ b/playback/src/lib.rs @@ -13,6 +13,7 @@ pub mod dither; pub mod mixer; pub mod player; +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;