Skip to content

Commit 05cb4a9

Browse files
committed
fix: aliasing in sinc downsampling
Add rate-dependent configuration to Interpolator and use it in Sinc to apply an anti-aliasing bandwidth when downsampling. The Interpolator trait now exposes set_hz_to_hz, set_playback_hz_scale, and set_sample_hz_scale (default no-ops); Converter methods call them automatically. Fixes #174
1 parent d089297 commit 05cb4a9

File tree

5 files changed

+85
-6
lines changed

5 files changed

+85
-6
lines changed

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# Unreleased
22

3+
- Fixed aliasing in Sinc interpolator when downsampling by adding automatic
4+
anti-aliasing filter cutoff adjustment. The `Interpolator` trait now includes
5+
`set_hz_to_hz`, `set_playback_hz_scale`, and `set_sample_hz_scale` methods
6+
(with default no-op implementations) that are called automatically by the
7+
corresponding `Converter` methods to configure rate-dependent parameters.
38
- Renamed `window-hanning` to `window-hann`
49
- Made `IntoInterleavedSamples` and `IntoInterleavedSamplesIterator` stop
510
yielding samples when the underlying signal gets exhausted. This is a breaking

dasp_interpolate/src/lib.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,17 @@ pub mod sinc;
3838
///
3939
/// Implementations should keep track of the necessary data both before and after the current
4040
/// frame.
41+
///
42+
/// # Rate Configuration
43+
///
44+
/// Some interpolators require sample rate information to operate correctly (e.g., sinc
45+
/// interpolation needs the rate ratio for anti-aliasing). The `set_hz_to_hz`,
46+
/// `set_playback_hz_scale`, and `set_sample_hz_scale` methods provide alternative ways
47+
/// to configure this - use whichever matches the information available. These methods
48+
/// are called automatically by the corresponding `Converter` methods.
49+
///
50+
/// Interpolators that don't need rate information (floor, linear) can use the default
51+
/// no-op implementations.
4152
pub trait Interpolator {
4253
/// The type of frame over which the interpolate may operate.
4354
type Frame: Frame;
@@ -53,4 +64,13 @@ pub trait Interpolator {
5364
///
5465
/// Call this when there's a break in the continuity of the input data stream.
5566
fn reset(&mut self);
67+
68+
/// Configures the interpolator from absolute sample rates.
69+
fn set_hz_to_hz(&mut self, _source_hz: f64, _target_hz: f64) {}
70+
71+
/// Configures the interpolator from playback rate scale (`source_hz / target_hz`).
72+
fn set_playback_hz_scale(&mut self, _scale: f64) {}
73+
74+
/// Configures the interpolator from sample rate scale (`target_hz / source_hz`).
75+
fn set_sample_hz_scale(&mut self, _scale: f64) {}
5676
}

dasp_interpolate/src/sinc/mod.rs

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod ops;
2626
pub struct Sinc<S> {
2727
frames: ring_buffer::Fixed<S>,
2828
idx: usize,
29+
bandwidth: f64,
2930
}
3031

3132
impl<S> Sinc<S> {
@@ -49,8 +50,9 @@ impl<S> Sinc<S> {
4950
{
5051
assert!(frames.len() % 2 == 0);
5152
Sinc {
52-
frames: frames,
53+
frames,
5354
idx: 0,
55+
bandwidth: 1.0,
5456
}
5557
}
5658

@@ -77,6 +79,7 @@ where
7779
let nl = self.idx;
7880
let nr = self.idx + 1;
7981
let depth = self.depth();
82+
let bandwidth = self.bandwidth;
8083

8184
let rightmost = nl + depth;
8285
let leftmost = nr as isize - depth as isize;
@@ -90,9 +93,9 @@ where
9093

9194
(0..max_depth).fold(Self::Frame::EQUILIBRIUM, |mut v, n| {
9295
v = {
93-
let a = PI * (phil + n as f64);
96+
let a = PI * bandwidth * (phil + n as f64);
9497
let first = if a == 0.0 { 1.0 } else { sin(a) / a };
95-
let second = 0.5 + 0.5 * cos(a / depth as f64);
98+
let second = 0.5 + 0.5 * cos(a / (depth as f64 * bandwidth));
9699
v.zip_map(self.frames[nl - n], |vs, r_lag| {
97100
vs.add_amp(
98101
(first * second * r_lag.to_sample::<f64>())
@@ -102,9 +105,9 @@ where
102105
})
103106
};
104107

105-
let a = PI * (phir + n as f64);
108+
let a = PI * bandwidth * (phir + n as f64);
106109
let first = if a == 0.0 { 1.0 } else { sin(a) / a };
107-
let second = 0.5 + 0.5 * cos(a / depth as f64);
110+
let second = 0.5 + 0.5 * cos(a / (depth as f64 * bandwidth));
108111
v.zip_map(self.frames[nr + n], |vs, r_lag| {
109112
vs.add_amp(
110113
(first * second * r_lag.to_sample::<f64>())
@@ -129,4 +132,16 @@ where
129132
*frame = Self::Frame::EQUILIBRIUM;
130133
}
131134
}
135+
136+
fn set_hz_to_hz(&mut self, source_hz: f64, target_hz: f64) {
137+
self.bandwidth = (target_hz / source_hz).min(1.0);
138+
}
139+
140+
fn set_playback_hz_scale(&mut self, scale: f64) {
141+
self.bandwidth = (1.0 / scale).min(1.0);
142+
}
143+
144+
fn set_sample_hz_scale(&mut self, scale: f64) {
145+
self.bandwidth = scale.min(1.0);
146+
}
132147
}

dasp_signal/src/interpolate.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,11 @@ where
3131
{
3232
/// Construct a new `Converter` from the source frames and the source and target sample rates
3333
/// (in Hz).
34+
///
35+
/// This method calls `interpolator.set_hz_to_hz(source_hz, target_hz)` internally.
3436
#[inline]
35-
pub fn from_hz_to_hz(source: S, interpolator: I, source_hz: f64, target_hz: f64) -> Self {
37+
pub fn from_hz_to_hz(source: S, mut interpolator: I, source_hz: f64, target_hz: f64) -> Self {
38+
interpolator.set_hz_to_hz(source_hz, target_hz);
3639
Self::scale_playback_hz(source, interpolator, source_hz / target_hz)
3740
}
3841

@@ -72,24 +75,33 @@ where
7275
/// Update the `source_to_target_ratio` internally given the source and target hz.
7376
///
7477
/// This method might be useful for changing the sample rate during playback.
78+
///
79+
/// This method calls `interpolator.set_hz_to_hz(source_hz, target_hz)` internally.
7580
#[inline]
7681
pub fn set_hz_to_hz(&mut self, source_hz: f64, target_hz: f64) {
82+
self.interpolator.set_hz_to_hz(source_hz, target_hz);
7783
self.set_playback_hz_scale(source_hz / target_hz)
7884
}
7985

8086
/// Update the `source_to_target_ratio` internally given a new **playback rate** multiplier.
8187
///
8288
/// This method is useful for dynamically changing rates.
89+
///
90+
/// This method calls `interpolator.set_playback_hz_scale(scale)` internally.
8391
#[inline]
8492
pub fn set_playback_hz_scale(&mut self, scale: f64) {
93+
self.interpolator.set_playback_hz_scale(scale);
8594
self.source_to_target_ratio = scale;
8695
}
8796

8897
/// Update the `source_to_target_ratio` internally given a new **sample rate** multiplier.
8998
///
9099
/// This method is useful for dynamically changing rates.
100+
///
101+
/// This method calls `interpolator.set_sample_hz_scale(scale)` internally.
91102
#[inline]
92103
pub fn set_sample_hz_scale(&mut self, scale: f64) {
104+
self.interpolator.set_sample_hz_scale(scale);
93105
self.set_playback_hz_scale(1.0 / scale);
94106
}
95107

dasp_signal/tests/interpolate.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,30 @@ fn test_sinc() {
7171
None
7272
);
7373
}
74+
75+
#[test]
76+
fn test_sinc_downsampling_antialiasing() {
77+
const SOURCE_HZ: f64 = 2000.0;
78+
const TARGET_HZ: f64 = 1500.0;
79+
const SIGNAL_FREQ: f64 = 900.0;
80+
81+
let source_signal = signal::rate(SOURCE_HZ).const_hz(SIGNAL_FREQ).sine();
82+
83+
let ring_buffer = ring_buffer::Fixed::from(vec![0.0; 100]);
84+
let sinc = Sinc::new(ring_buffer);
85+
let mut downsampled = source_signal.from_hz_to_hz(sinc, SOURCE_HZ, TARGET_HZ);
86+
87+
for _ in 0..50 {
88+
downsampled.next();
89+
}
90+
91+
let samples: Vec<f64> = downsampled.take(1500).collect();
92+
93+
let rms = (samples.iter().map(|&s| s * s).sum::<f64>() / samples.len() as f64).sqrt();
94+
95+
assert!(
96+
rms < 0.1,
97+
"Expected RMS < 0.1 (well-filtered), got {:.3}. Aliasing occurred.",
98+
rms
99+
);
100+
}

0 commit comments

Comments
 (0)