Skip to content

Instantly share code, notes, and snippets.

@JasonLG1979
Last active June 16, 2023 09:58
Show Gist options
  • Save JasonLG1979/8e9f4a9af489b68cc0ab463d6b32b6f6 to your computer and use it in GitHub Desktop.
Save JasonLG1979/8e9f4a9af489b68cc0ab463d6b32b6f6 to your computer and use it in GitHub Desktop.
Dual threaded Stereo Linear or Windowed Sinc Resampler from 44.1kHz to 48kHz, 88.2kHz or 96kHz with no dependencies outside std.
// This is free and unencumbered software released into the public domain.
//
// Anyone is free to copy, modify, publish, use, compile, sell, or
// distribute this software, either in source code form or as a compiled
// binary, for any purpose, commercial or non-commercial, and by any
// means.
//
// In jurisdictions that recognize copyright laws, the author or authors
// of this software dedicate any and all copyright interest in the
// software to the public domain. We make this dedication for the benefit
// of the public at large and to the detriment of our heirs and
// successors. We intend this dedication to be an overt act of
// relinquishment in perpetuity of all present and future rights to this
// software under copyright law.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
// IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
// OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
// ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.
//
// For more information, please refer to <http://unlicense.org/>
const INPUT_SIZE: usize = 147;
const SOURCE_SAMPLE_RATE: usize = 44_100;
// Reciprocals allow us to multiply instead of divide during interpolation.
const HZ48000_RESAMPLE_FACTOR_RECIPROCAL: f64 = SOURCE_SAMPLE_RATE as f64 / 48_000.0;
const HZ88200_RESAMPLE_FACTOR_RECIPROCAL: f64 = SOURCE_SAMPLE_RATE as f64 / 88_200.0;
const HZ96000_RESAMPLE_FACTOR_RECIPROCAL: f64 = SOURCE_SAMPLE_RATE as f64 / 96_000.0;
const HZ48000_INTERPOLATION_OUTPUT_SIZE: usize =
(INPUT_SIZE as f64 * (1.0 / HZ48000_RESAMPLE_FACTOR_RECIPROCAL)) as usize;
const HZ88200_INTERPOLATION_OUTPUT_SIZE: usize =
(INPUT_SIZE as f64 * (1.0 / HZ88200_RESAMPLE_FACTOR_RECIPROCAL)) as usize;
const HZ96000_INTERPOLATION_OUTPUT_SIZE: usize =
(INPUT_SIZE as f64 * (1.0 / HZ96000_RESAMPLE_FACTOR_RECIPROCAL)) as usize;
// Hann coefficients
const HANN_A0: f64 = 0.5;
const HANN_A1: f64 = 1.0;
// Hamming coefficients
const HAMMING_A0: f64 = 0.53836;
const HAMMING_A1: f64 = 0.46164;
// Nuttall coefficients
const NUTTALL_A0: f64 = 0.355768;
const NUTTALL_A1: f64 = 0.487396;
const NUTTALL_A2: f64 = 0.144232;
const NUTTALL_A3: f64 = 0.012604;
// Blackman coefficients
const BLACKMAN_A0: f64 = 0.42;
const BLACKMAN_A1: f64 = 0.5;
const BLACKMAN_A2: f64 = 0.08;
// Blackman-Harris coefficients
const BLACKMAN_HARRIS_A0: f64 = 0.355768;
const BLACKMAN_HARRIS_A1: f64 = 0.487396;
const BLACKMAN_HARRIS_A2: f64 = 0.144232;
const BLACKMAN_HARRIS_A3: f64 = 0.012604;
// Blackman-Nuttall coefficients
const BLACKMAN_NUTTALL_A0: f64 = 0.3635819;
const BLACKMAN_NUTTALL_A1: f64 = 0.4891775;
const BLACKMAN_NUTTALL_A2: f64 = 0.1365995;
const BLACKMAN_NUTTALL_A3: f64 = 0.0106411;
// Constants for calculations
const TWO_PI: f64 = 2.0 * std::f64::consts::PI;
const FOUR_PI: f64 = 4.0 * std::f64::consts::PI;
const SIX_PI: f64 = 6.0 * std::f64::consts::PI;
/// Enum representing different levels of interpolation quality.
#[derive(Clone, Copy, Debug, Default)]
pub enum InterpolationQuality {
/// Low interpolation quality using Linear Interpolation.
#[default]
Low,
/// Medium interpolation quality using Windowed Sinc Interpolation.
Medium,
/// High interpolation quality using Windowed Sinc Interpolation.
High,
}
impl std::str::FromStr for InterpolationQuality {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use InterpolationQuality::*;
let lowercase_input = s.to_lowercase();
match lowercase_input.as_str() {
"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 {
/// Returns the length of interpolation coefficients based on the quality level.
pub fn get_interpolation_coefficients_length(&self) -> usize {
use InterpolationQuality::*;
match self {
Low => 0,
Medium => 129,
High => 257,
}
}
}
/// Enum representing different sample rates.
#[derive(Clone, Copy, Debug, Default)]
pub enum SampleRate {
/// Represents a sample rate of 44.1kHz. Signifies a bypass.
#[default]
Hz44100,
/// Represents a sample rate of 48kHz.
Hz48000,
/// Represents a sample rate of 88.2kHz.
Hz88200,
/// Represents a sample rate of 96kHz.
Hz96000,
}
impl std::ops::Deref for SampleRate {
type Target = u32;
fn deref(&self) -> &Self::Target {
use SampleRate::*;
match self {
Hz44100 => &44100,
Hz48000 => &48000,
Hz88200 => &88200,
Hz96000 => &96000,
}
}
}
impl std::str::FromStr for SampleRate {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use SampleRate::*;
let lowercase_input = s.to_lowercase();
// Match against both the actual
// stringified value and how most
// humans would write a sample rate.
match lowercase_input.as_str() {
"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"),
}
}
}
/// Specifies the resampling parameters.
///
/// Where:
/// resample factor is output sample rate / 44100
/// We use the reciprocal of that because it allows us to multiply instead of divide during interpolation.
/// And:
/// interpolation_output_size is 147 * resample factor
///
/// 147 was specifically chosen because 147 * resample factor for 48kHz, 88.2kHz, and 96kHz is an integer.
/// Having an integer for the Interpolator input and output means no fractional samples, less rounding error,
/// and perfect pitch and time continuity.
#[derive(Clone, Copy, Debug, Default)]
pub struct ResampleSpec {
resample_factor_reciprocal: f64,
interpolation_output_size: usize,
}
impl SampleRate {
/// Returns the `ResampleSpec` for the sample rate.
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: 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,
},
}
}
}
/// Enum representing different window functions.
///
/// Although how a window function affects a signal can certainly be objectively measured, There is
/// no objectively "Best Sounding" window, it's a matter of personal taste.
#[derive(Clone, Copy, Debug, Default)]
pub enum WindowFunction {
Hann,
Hamming,
Nuttall,
Blackman,
#[default]
BlackmanHarris,
BlackmanNuttall,
}
impl std::str::FromStr for WindowFunction {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
use WindowFunction::*;
let lowercase_input = s.to_lowercase();
// Match against the hyphenated
// version of the window also
// where applicable.
match lowercase_input.as_str() {
"hann" => Ok(Hann),
"hamming" => Ok(Hamming),
"nuttall" => Ok(Nuttall),
"blackman" => Ok(Blackman),
"blackmanharris" | "blackman-harris" => Ok(BlackmanHarris),
"blackmannuttall" | "blackman-nuttall" => Ok(BlackmanNuttall),
_ => Err(()),
}
}
}
impl std::fmt::Display for WindowFunction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
use WindowFunction::*;
// Add the hyphen in the window
// name where applicable.
match self {
Hann => write!(f, "Hann"),
Hamming => write!(f, "Hamming"),
Nuttall => write!(f, "Nuttall"),
Blackman => write!(f, "Blackman"),
BlackmanHarris => write!(f, "Blackman-Harris"),
BlackmanNuttall => write!(f, "Blackman-Nuttall"),
}
}
}
impl WindowFunction {
/// Returns the interpolation coefficients for the specified window function.
///
/// # Arguments
///
/// * `interpolation_coefficients_length` - The length of interpolation coefficients to generate.
/// * `resample_factor_reciprocal` - The resample factor reciprocal used in the interpolation.
///
/// # Returns
///
/// The interpolation coefficients as a vector of `f64` values.
pub fn get_interpolation_coefficients(
&self,
interpolation_coefficients_length: usize,
resample_factor_reciprocal: f64,
) -> Vec<f64> {
// Pre-calculate weighted coefficients. This saves us
// from having to calculate very possibly the same values
// over and over again on-the-fly. This saves us multiple
// math operations per sample which is a big deal when you're
// dealing with up to 192000 (sample rate * channels) samples a sec.
// With these pre-calculate weighted coefficients all we have to
// do during runtime is the temporal convolution part of the
// interpolation.
let mut coefficients = Vec::with_capacity(interpolation_coefficients_length);
let sinc_center = (interpolation_coefficients_length as f64 - 1.0) * 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);
let window_value = self.window(
interpolation_coefficient_index,
interpolation_coefficients_length,
);
let sinc_window = sinc_value * window_value;
let coefficient = sinc_window * sample_index_fractional_sinc_weight;
coefficient_sum += coefficient;
coefficient
},
));
// Normalize the window.
coefficients
.iter_mut()
.for_each(|coefficient| *coefficient /= coefficient_sum);
coefficients
}
fn sinc(x: f64) -> f64 {
// calculate the sinc value for x.
// If x is 0.0, or at least as close
// to 0.0 as can be tested with
// floating point math, return 1.0.
if x.abs() < f64::EPSILON {
1.0
} else {
let pi_x = std::f64::consts::PI * x;
pi_x.sin() / pi_x
}
}
fn window(
&self,
interpolation_coefficient_index: usize,
interpolation_coefficients_length: usize,
) -> f64 {
// Generate window values for a WindowFunction variant based on coefficient_index and interpolation_coefficients_length,
// which represent n and N in the window function equations.
use WindowFunction::*;
// Common values shared by all windows
// n
let n = interpolation_coefficient_index as f64;
// 2πn
let two_pi_n = TWO_PI * n;
// N-1
let cap_n_minus_one = interpolation_coefficients_length as f64 - 1.0;
match self {
Hann => {
// Calculate the Hann window function for the given center offset
// w(n) = 0.5 * (1 - cos(2πn / (N-1))),
// where n is the center offset and N is the window size
HANN_A0 * (HANN_A1 - (two_pi_n / cap_n_minus_one).cos())
}
Hamming => {
// Calculate the Hamming window function for the given center offset
// w(n) = A0 - A1 * cos(2πn / (N-1)),
// where n is the center offset, N is the window size,
// and A0, A1 are precalculated coefficients
HAMMING_A0 - HAMMING_A1 * (two_pi_n / cap_n_minus_one).cos()
}
Nuttall => {
// Calculate the Nuttall window function for the given center offset
// w(n) = A0 - A1*cos(2πn / (N-1)) + A2*cos(4πn / (N-1)) - A3*cos(6πn / (N-1)),
// where n is the center offset, N is the window size,
// and A0, A1, A2, A3 are precalculated coefficients
let four_pi_n = FOUR_PI * n;
let six_pi_n = SIX_PI * n;
NUTTALL_A0 - NUTTALL_A1 * (two_pi_n / cap_n_minus_one).cos()
+ NUTTALL_A2 * (four_pi_n / cap_n_minus_one).cos()
- NUTTALL_A3 * (six_pi_n / cap_n_minus_one).cos()
}
Blackman => {
// 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 four_pi_n = FOUR_PI * n;
BLACKMAN_A0 - BLACKMAN_A1 * (two_pi_n / cap_n_minus_one).cos()
+ BLACKMAN_A2 * (four_pi_n / cap_n_minus_one).cos()
}
BlackmanHarris => {
// Calculate the Blackman-Harris window function for the given center offset
// w(n) = A0 - A1*cos(2πn / (N-1)) + A2*cos(4πn / (N-1)) - A3*cos(6πn / (N-1)),
// where n is the center offset, N is the window size,
// and A0, A1, A2, A3 are precalculated coefficients
let four_pi_n = FOUR_PI * n;
let six_pi_n = SIX_PI * n;
BLACKMAN_HARRIS_A0 - BLACKMAN_HARRIS_A1 * (two_pi_n / cap_n_minus_one).cos()
+ BLACKMAN_HARRIS_A2 * (four_pi_n / cap_n_minus_one).cos()
- BLACKMAN_HARRIS_A3 * (six_pi_n / cap_n_minus_one).cos()
}
BlackmanNuttall => {
// Calculate the Blackman-Nuttall window function for the given center offset
// w(n) = A0 - A1*cos(2πn / (N-1)) + A2*cos(4πn / (N-1)) - A3*cos(6πn / (N-1)),
// where n is the center offset, N is the window size,
// and A0, A1, A2, A3 are precalculated coefficients
let four_pi_n = FOUR_PI * n;
let six_pi_n = SIX_PI * n;
BLACKMAN_NUTTALL_A0 - BLACKMAN_NUTTALL_A1 * (two_pi_n / cap_n_minus_one).cos()
+ BLACKMAN_NUTTALL_A2 * (four_pi_n / cap_n_minus_one).cos()
- BLACKMAN_NUTTALL_A3 * (six_pi_n / cap_n_minus_one).cos()
}
}
}
}
/// A delay line buffer that stores previous input samples for interpolation.
///
/// The delay line is a First-In-First-Out (FIFO) buffer that maintains a fixed-length history of
/// samples, preserving their order. This history is accessed and processed by the Interpolator to
/// perform temporal convolution and produce interpolated samples.
pub struct DelayLine {
buffer: std::collections::VecDeque<f64>,
interpolation_coefficients_length: usize,
}
impl DelayLine {
/// Creates a new `DelayLine` with the specified sinc_window_length (buffer size).
///
/// # Arguments
///
/// * `interpolation_coefficients_length` - The size of the delay line buffer.
///
/// # Returns
///
/// A new `DelayLine` instance.
pub fn new(interpolation_coefficients_length: usize) -> DelayLine {
Self {
buffer: std::collections::VecDeque::with_capacity(interpolation_coefficients_length),
interpolation_coefficients_length,
}
}
/// Adds a new sample to the delay line.
///
/// If the buffer exceeds its capacity, the oldest sample is removed.
///
/// # Arguments
///
/// * `sample` - The new sample to add to the delay line.
pub fn push(&mut self, sample: f64) {
self.buffer.push_back(sample);
while self.buffer.len() > self.interpolation_coefficients_length {
self.buffer.pop_front();
}
}
/// Clears the delay line, discarding all its contents.
pub fn clear(&mut self) {
self.buffer.clear();
}
}
impl<'a> IntoIterator for &'a DelayLine {
type Item = &'a f64;
type IntoIter = std::collections::vec_deque::Iter<'a, f64>;
fn into_iter(self) -> Self::IntoIter {
self.buffer.iter()
}
}
/// A Windowed Sinc Interpolator that performs sample interpolation using pre-calculated coefficients.
pub struct WindowedSincInterpolator {
interpolation_coefficients: Vec<f64>,
delay_line: DelayLine,
}
impl WindowedSincInterpolator {
/// Creates a new `WindowedSincInterpolator` with the specified interpolation quality, window function, and resample factor.
///
/// # Arguments
///
/// * `interpolation_quality` - The interpolation quality determining interpolation coefficients length.
/// * `window_function` - The window function to use for interpolation.
/// * `resample_factor_reciprocal` - The resampling factor reciprocal for interpolation.
///
/// # Returns
///
/// A new `WindowedSincInterpolator` instance.
pub fn new(
interpolation_quality: InterpolationQuality,
window_function: WindowFunction,
resample_factor_reciprocal: f64,
) -> Self {
let interpolation_coefficients_length =
interpolation_quality.get_interpolation_coefficients_length();
let delay_line = DelayLine::new(interpolation_coefficients_length);
let interpolation_coefficients = window_function.get_interpolation_coefficients(
interpolation_coefficients_length,
resample_factor_reciprocal,
);
Self {
interpolation_coefficients,
delay_line,
}
}
/// Interpolates a new sample using the pre-calculated interpolation coefficients.
///
/// The sample is pushed into the delay line, and then temporal convolution is performed
/// to produce the interpolated sample.
///
/// # Arguments
///
/// * `sample` - The input sample to be interpolated.
///
/// # Returns
///
/// The interpolated sample.
pub fn interpolate(&mut self, sample: f64) -> f64 {
// Since our interpolation coefficients are pre-calculated
// we can basically pretend like the Interpolator is a FIR filter.
self.delay_line.push(sample);
// Temporal convolution
self.interpolation_coefficients
.iter()
.zip(&self.delay_line)
.fold(0.0, |acc, (coefficient, delay_line_sample)| {
acc + coefficient * delay_line_sample
})
}
/// Clears the delay line, discarding all its contents.
///
/// This resets the Interpolator while keeping the existing interpolation coefficients.
pub fn clear(&mut self) {
self.delay_line.clear();
}
}
/// Trait representing a mono audio resampler.
pub trait MonoResampler {
/// Creates a new instance of the resampler with the specified sample rate, interpolation quality, and window function.
///
/// # Arguments
///
/// * `sample_rate` - The sample rate of the audio to be resampled.
/// * `interpolation_quality` - The interpolation quality to be used by the resampler (applicable for Windowed Sinc Interpolation).
/// * `window_function` - The window function to be used by the resampler (applicable for Windowed Sinc Interpolation).
///
/// # Returns
///
/// A new instance of the resampler.
///
/// # Generic Constraints
///
/// This method has a generic constraint that requires the implementor of the `MonoResampler` trait to be `Sized`.
fn new(
sample_rate: SampleRate,
interpolation_quality: InterpolationQuality,
window_function: WindowFunction,
) -> Self
where
Self: Sized;
/// Stops the resampler and clears its internal state.
///
/// This method resets the resampler by clearing the input buffer and `WindowedSincInterpolator`
/// (in the case of Windowed Sinc Interpolation).
fn stop(&mut self);
/// Resamples the provided mono audio samples.
///
/// # Arguments
///
/// * `samples` - The mono audio samples to be resampled as a slice of `f64` values.
///
/// # Returns
///
/// An `Option` containing the resampled audio samples as a vector of `f64` values.
///
/// If resampling is not possible due to insufficient samples, `None` is returned.
fn resample(&mut self, samples: &[f64]) -> Option<Vec<f64>>;
}
/// A struct representing a mono audio sinc resampler.
pub struct MonoSincResampler {
interpolator: WindowedSincInterpolator,
input_buffer: Vec<f64>,
resample_factor_reciprocal: f64,
interpolation_output_size: usize,
}
impl MonoResampler for MonoSincResampler {
/// Creates a new `MonoSincResampler` instance with the specified sample rate, interpolation quality, and window function.
///
/// # Arguments
///
/// * `sample_rate` - The sample rate of the audio to be resampled.
/// * `interpolation_quality` - The interpolation quality to be used by the resampler.
/// * `window_function` - The window function to be used by the resampler.
///
/// # Returns
///
/// A new `MonoSincResampler` instance.
fn new(
sample_rate: SampleRate,
interpolation_quality: InterpolationQuality,
window_function: WindowFunction,
) -> Self {
let spec = sample_rate.get_resample_spec();
Self {
interpolator: WindowedSincInterpolator::new(
interpolation_quality,
window_function,
spec.resample_factor_reciprocal,
),
input_buffer: Vec::with_capacity(SOURCE_SAMPLE_RATE),
resample_factor_reciprocal: spec.resample_factor_reciprocal,
interpolation_output_size: spec.interpolation_output_size,
}
}
/// Stops the resampler and clears its internal state.
///
/// This method resets the resampler by clearing the `WindowedSincInterpolator`
/// and input buffer.
fn stop(&mut self) {
self.interpolator.clear();
self.input_buffer.clear();
}
/// Resamples the provided mono audio samples using sinc interpolation.
///
/// # Arguments
///
/// * `samples` - The mono audio samples to be resampled as a slice of `f64` values.
///
/// # Returns
///
/// An `Option` containing the resampled audio samples as a vector of `f64` values.
///
/// If resampling is not possible due to insufficient samples, `None` is returned.
fn resample(&mut self, samples: &[f64]) -> Option<Vec<f64>> {
self.input_buffer.extend_from_slice(samples);
let num_buffer_chunks = self.input_buffer.len().saturating_div(INPUT_SIZE);
if num_buffer_chunks == 0 {
return None;
}
// The amount of the input buffer we are going to interpolate.
let input_size = num_buffer_chunks * INPUT_SIZE;
// The size of the output after interpolation.
let output_size = num_buffer_chunks * self.interpolation_output_size;
let mut output = Vec::with_capacity(output_size);
output.extend((0..output_size).map(|ouput_index| {
// The factional weights are already calculated and factored
// into our interpolation coefficients so all we have to
// do is pretend we're doing nearest-neighbor interpolation
// and push samples though the Interpolator and what comes
// out the other side is Sinc Windowed Interpolated samples.
let sample_index = (ouput_index as f64 * self.resample_factor_reciprocal) as usize;
let sample = self.input_buffer[sample_index];
self.interpolator.interpolate(sample)
}));
// Remove the interpolated samples from the input buffer.
self.input_buffer.drain(..input_size);
Some(output)
}
}
/// A struct representing a mono audio linear resampler.
pub struct MonoLinearResampler {
input_buffer: Vec<f64>,
resample_factor_reciprocal: f64,
interpolation_output_size: usize,
}
impl MonoResampler for MonoLinearResampler {
/// Creates a new `MonoLinearResampler` instance with the specified sample rate.
///
/// # Arguments
///
/// * `sample_rate` - The sample rate of the audio to be resampled.
/// * `interpolation_quality` - [Ignored] The interpolation quality (ignored by this resampler).
/// * `window_function` - [Ignored] The window function (ignored by this resampler).
///
/// # Returns
///
/// A new `MonoLinearResampler` instance.
fn new(sample_rate: SampleRate, _: InterpolationQuality, _: WindowFunction) -> Self {
let spec = sample_rate.get_resample_spec();
Self {
input_buffer: Vec::with_capacity(SOURCE_SAMPLE_RATE),
resample_factor_reciprocal: spec.resample_factor_reciprocal,
interpolation_output_size: spec.interpolation_output_size,
}
}
/// Stops the resampler and clears its internal state.
///
/// This method resets the resampler by clearing the input buffer.
fn stop(&mut self) {
self.input_buffer.clear();
}
/// Resamples the provided mono audio samples using linear interpolation.
///
/// # Arguments
///
/// * `samples` - The mono audio samples to be resampled as a slice of `f64` values.
///
/// # Returns
///
/// An `Option` containing the resampled audio samples as a vector of `f64` values.
///
/// If resampling is not possible due to insufficient samples, `None` is returned.
fn resample(&mut self, samples: &[f64]) -> Option<Vec<f64>> {
self.input_buffer.extend_from_slice(samples);
let num_buffer_chunks = self.input_buffer.len().saturating_div(INPUT_SIZE);
if num_buffer_chunks == 0 {
return None;
}
// The amount of the input buffer we are going to interpolate.
let input_size = num_buffer_chunks * INPUT_SIZE;
// The size of the output after interpolation.
// We have to account for the fact that to do effective linear
// interpolation we need an extra sample to be able to throw away later.
let output_size = num_buffer_chunks * self.interpolation_output_size + 1;
let mut output = Vec::with_capacity(output_size);
output.extend((0..output_size).map(|output_index| {
let sample_index = output_index as f64 * self.resample_factor_reciprocal;
let sample_index_fractional = sample_index.fract();
let sample_index = sample_index as usize;
let sample = *self.input_buffer.get(sample_index).unwrap_or(&0.0);
let next_sample = *self.input_buffer.get(sample_index + 1).unwrap_or(&0.0);
let sample_index_fractional_complementary = 1.0 - sample_index_fractional;
sample * sample_index_fractional_complementary + next_sample * sample_index_fractional
}));
// Remove the last garbage sample.
output.pop();
// Remove the interpolated samples from the input buffer.
self.input_buffer.drain(..input_size);
Some(output)
}
}
enum ResampleTask {
Stop,
Terminate,
ProcessSamples(Vec<f64>),
}
enum ResampleResult {
ProcessedSamples(Option<Vec<f64>>),
}
/// A struct representing a resample worker that performs resampling in a worker thread.
pub struct ResampleWorker {
task_sender: Option<std::sync::mpsc::Sender<ResampleTask>>,
result_receiver: Option<std::sync::mpsc::Receiver<ResampleResult>>,
handle: Option<std::thread::JoinHandle<()>>,
}
impl ResampleWorker {
/// Creates a new `ResampleWorker` instance that performs resampling in a worker thread.
///
/// # Arguments
///
/// * `resampler` - A resampler implementing the `MonoResampler` trait, which performs the actual resampling.
///
/// # Returns
///
/// A new `ResampleWorker` instance.
pub fn new(mut resampler: impl MonoResampler + std::marker::Send + 'static) -> Self {
// A thread worker for the resampler.
let (task_sender, task_receiver) = std::sync::mpsc::channel();
let (result_sender, result_receiver) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || loop {
// Handle ResampleTask's in a worker thread.
match task_receiver.recv() {
Err(_) => break,
Ok(task) => match task {
ResampleTask::Terminate => break,
ResampleTask::Stop => resampler.stop(),
ResampleTask::ProcessSamples(samples) => {
let samples = resampler.resample(&samples);
result_sender
.send(ResampleResult::ProcessedSamples(samples))
.ok();
}
},
}
});
Self {
task_sender: Some(task_sender),
result_receiver: Some(result_receiver),
handle: Some(handle),
}
}
/// Stops the resample
pub fn stop(&mut self) {
// Shout Stop into the void.
self.task_sender
.as_mut()
.and_then(|sender| sender.send(ResampleTask::Stop).ok());
}
/// Processes the samples using the resample worker in a worker thread.
///
/// # Arguments
///
/// * `samples` - The samples to be processed as a vector of `f64` values.
pub fn process(&mut self, samples: Vec<f64>) {
// Shout our task into the void.
self.task_sender
.as_mut()
.and_then(|sender| sender.send(ResampleTask::ProcessSamples(samples)).ok());
}
/// Receives the result of the resample operation performed by the worker thread.
///
/// # Returns
///
/// An `Option` containing the processed output samples as a vector of `f64` values.
///
/// If resampling is not possible due to insufficient samples, `None` is returned.
pub fn receive_result(&mut self) -> Option<Vec<f64>> {
self.result_receiver
.as_mut()
.and_then(|result_receiver| result_receiver.recv().ok())
.and_then(|result| match result {
ResampleResult::ProcessedSamples(samples) => samples,
})
}
}
impl Drop for ResampleWorker {
fn drop(&mut self) {
// Tell the worker thread to die.
self.task_sender
.take()
.and_then(|sender| sender.send(ResampleTask::Terminate).ok());
// Drain the receiver.
self.result_receiver
.take()
.and_then(|result_receiver| loop {
let drained = result_receiver.recv().ok();
if drained.is_none() {
break drained;
}
});
// Join the worker thread.
self.handle.take().and_then(|handle| handle.join().ok());
}
}
#[derive(Default)]
pub enum Resampler {
#[default]
Bypass,
Worker {
left_resampler: ResampleWorker,
right_resampler: ResampleWorker,
},
}
/// A struct representing a stereo interleaved resampler.
#[derive(Default)]
pub struct StereoInterleavedResampler {
resampler: Resampler,
}
impl StereoInterleavedResampler {
/// Creates a new `StereoInterleavedResampler` instance.
///
/// # Arguments
///
/// * `sample_rate` - An optional `SampleRate` for resampling. If not provided, the default sample rate is used.
/// * `interpolation_quality` - An optional `InterpolationQuality` for resampling. If not provided, the default interpolation quality is used.
/// * `window_function` - An optional `WindowFunction` for resampling. If not provided, the default window function is used.
///
/// # Returns
///
/// A new `StereoInterleavedResampler` instance.
pub fn new(
sample_rate: Option<SampleRate>,
interpolation_quality: Option<InterpolationQuality>,
window_function: Option<WindowFunction>,
) -> Self {
// Creates a Dual threaded resampler wherein the
// incoming sample packets of stereo interleaved
// PCM are de-interleaved into separate left and
// right channels and sent to worker threads to
// be resampled. The calling thread then waits
// for both results and re-interleaves them.
let sample_rate = sample_rate.unwrap_or_default();
let resampler = match sample_rate {
// Our sample rate is the same as our input sample rate.
// We don't need thread workers since we're not resampling.
SampleRate::Hz44100 => Resampler::Bypass,
_ => {
let interpolation_quality = interpolation_quality.unwrap_or_default();
let window_function = window_function.unwrap_or_default();
match interpolation_quality {
// Create our workers.
InterpolationQuality::Low => {
// Low = Linear Interpolation.
let left = MonoLinearResampler::new(
sample_rate,
interpolation_quality,
window_function,
);
let right = MonoLinearResampler::new(
sample_rate,
interpolation_quality,
window_function,
);
Resampler::Worker {
left_resampler: ResampleWorker::new(left),
right_resampler: ResampleWorker::new(right),
}
}
_ => {
// Medium or High = Windowed Sinc interpolation.
let left = MonoSincResampler::new(
sample_rate,
interpolation_quality,
window_function,
);
let right = MonoSincResampler::new(
sample_rate,
interpolation_quality,
window_function,
);
Resampler::Worker {
left_resampler: ResampleWorker::new(left),
right_resampler: ResampleWorker::new(right),
}
}
}
}
};
Self { resampler }
}
/// Processes the input samples using the resampler.
///
/// # Arguments
///
/// * `input_samples` - The input samples to be processed as a slice of `f64` values.
///
/// # Returns
///
/// An `Option` containing the processed output samples as a vector of `f64` values.
///
/// If resampling is not possible due to insufficient samples, `None` is returned.
pub fn process(&mut self, input_samples: &[f64]) -> Option<Vec<f64>> {
match &mut self.resampler {
// Bypass is basically a no-op.
Resampler::Bypass => Some(input_samples.to_vec()),
// Do magic.
Resampler::Worker {
left_resampler,
right_resampler,
} => {
// Split the stereo interleaved samples into left and right channels.
let (left_samples, right_samples) = Self::deinterleave_samples(input_samples);
// Send the resample tasks to the workers.
left_resampler.process(left_samples);
right_resampler.process(right_samples);
// Wait for the results.
let left_samples = left_resampler.receive_result();
let right_samples = right_resampler.receive_result();
// Re-interleave the resampled channels.
left_samples.and_then(|left_samples| {
right_samples.map(|right_samples| {
Self::interleave_samples(&left_samples, &right_samples)
})
})
}
}
}
/// Stops the resampler.
pub fn stop(&mut self) {
match &mut self.resampler {
// Stop does nothing
// if we're bypassed.
Resampler::Bypass => (),
// Shout stop into the void.
Resampler::Worker {
left_resampler,
right_resampler,
} => {
left_resampler.stop();
right_resampler.stop();
}
}
}
fn interleave_samples(left_samples: &[f64], right_samples: &[f64]) -> Vec<f64> {
// Re-interleave the resampled channels.
left_samples
.iter()
.zip(right_samples.iter())
.flat_map(|(&x, &y)| vec![x, y])
.collect()
}
fn deinterleave_samples(samples: &[f64]) -> (Vec<f64>, Vec<f64>) {
// Split the stereo interleaved samples into left and right channels.
let (left_samples, right_samples): (Vec<f64>, Vec<f64>) = samples
.chunks(2)
.map(|chunk| {
let [left_sample, right_sample] = [chunk[0], chunk[1]];
(left_sample, right_sample)
})
.unzip();
(left_samples, right_samples)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment