1use crate::ResampleError;
9use crate::sinc::normalized_sinc;
10use crate::window::{Window, window_value};
11
12pub const DEFAULT_PHASES: usize = 256;
13pub const DEFAULT_TAPS_PER_PHASE: usize = 63;
14pub const DEFAULT_WINDOW: Window = Window::Blackman;
15
16const DOWNSAMPLE_CUTOFF_MARGIN: f64 = 0.95;
17
18#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct PolyphaseFilterParams {
27 pub phases: usize,
28 pub taps_per_phase: usize,
29 pub window: Window,
30}
31
32impl Default for PolyphaseFilterParams {
33 fn default() -> Self {
34 Self {
35 phases: DEFAULT_PHASES,
36 taps_per_phase: DEFAULT_TAPS_PER_PHASE,
37 window: DEFAULT_WINDOW,
38 }
39 }
40}
41
42impl PolyphaseFilterParams {
43 pub fn validate(&self) -> Result<(), ResampleError> {
44 if self.phases == 0 {
45 return Err(ResampleError::InvalidFilterConfig(
46 "phase count must be non-zero".into(),
47 ));
48 }
49
50 if self.taps_per_phase == 0 || self.taps_per_phase.is_multiple_of(2) {
51 return Err(ResampleError::InvalidFilterConfig(
52 "tap count must be odd and non-zero".into(),
53 ));
54 }
55
56 if let Window::Kaiser { beta } = self.window
57 && (!beta.is_finite() || beta < 0.0)
58 {
59 return Err(ResampleError::InvalidFilterConfig(
60 "kaiser beta must be non-negative and finite".into(),
61 ));
62 }
63
64 Ok(())
65 }
66}
67
68#[derive(Debug, Clone)]
69pub struct PolyphaseFilterBank {
70 phases: usize,
71 taps_per_phase: usize,
72 radius: usize,
73 cutoff: f64,
74 window: Window,
75 coeffs: Vec<f32>,
76}
77
78impl PolyphaseFilterBank {
79 pub fn new(ratio: f64) -> Self {
80 Self::try_with_params(ratio, PolyphaseFilterParams::default())
81 .expect("invalid polyphase filter configuration")
82 }
83
84 pub fn with_config(ratio: f64, phases: usize, taps_per_phase: usize, window: Window) -> Self {
85 Self::try_with_config(ratio, phases, taps_per_phase, window)
86 .expect("invalid polyphase filter configuration")
87 }
88
89 pub fn try_with_config(
90 ratio: f64,
91 phases: usize,
92 taps_per_phase: usize,
93 window: Window,
94 ) -> Result<Self, ResampleError> {
95 Self::try_with_params(
96 ratio,
97 PolyphaseFilterParams {
98 phases,
99 taps_per_phase,
100 window,
101 },
102 )
103 }
104
105 pub fn try_with_params(
106 ratio: f64,
107 params: PolyphaseFilterParams,
108 ) -> Result<Self, ResampleError> {
109 if !ratio.is_finite() || ratio <= 0.0 {
110 return Err(ResampleError::InvalidRatio);
111 }
112
113 params.validate()?;
114
115 Ok(Self::build(ratio, params))
116 }
117
118 fn build(ratio: f64, params: PolyphaseFilterParams) -> Self {
119 let cutoff = if ratio < 1.0 {
120 0.5 * ratio * DOWNSAMPLE_CUTOFF_MARGIN
121 } else {
122 0.5
123 };
124 let radius = params.taps_per_phase / 2;
125 let center = radius as f64;
126 let mut coeffs = Vec::with_capacity(params.phases * params.taps_per_phase);
127
128 for phase in 0..params.phases {
129 let frac = phase as f64 / params.phases as f64;
130 let mut phase_coeffs = Vec::with_capacity(params.taps_per_phase);
131 let mut sum = 0.0;
132
133 for tap in 0..params.taps_per_phase {
134 let x = tap as f64 - center - frac;
135 let window_t = if radius == 0 {
136 0.0
137 } else {
138 (x / center).clamp(-1.0, 1.0)
139 };
140 let coeff = normalized_sinc(x, cutoff) * window_value(params.window, window_t);
141 phase_coeffs.push(coeff);
142 sum += coeff;
143 }
144
145 if sum.abs() > f64::EPSILON {
146 let inv = 1.0 / sum;
147 for coeff in &mut phase_coeffs {
148 *coeff *= inv;
149 }
150 }
151
152 coeffs.extend(phase_coeffs.into_iter().map(|coeff| coeff as f32));
153 }
154
155 Self {
156 phases: params.phases,
157 taps_per_phase: params.taps_per_phase,
158 radius,
159 cutoff,
160 window: params.window,
161 coeffs,
162 }
163 }
164
165 pub fn params(&self) -> PolyphaseFilterParams {
166 PolyphaseFilterParams {
167 phases: self.phases,
168 taps_per_phase: self.taps_per_phase,
169 window: self.window,
170 }
171 }
172
173 pub fn phases(&self) -> usize {
174 self.phases
175 }
176
177 pub fn taps_per_phase(&self) -> usize {
178 self.taps_per_phase
179 }
180
181 pub fn radius(&self) -> usize {
182 self.radius
183 }
184
185 pub fn cutoff(&self) -> f64 {
186 self.cutoff
187 }
188
189 pub fn window(&self) -> Window {
190 self.window
191 }
192
193 pub fn left_offset(&self) -> isize {
194 -(self.radius as isize)
195 }
196
197 pub fn phase_for(&self, frac: f64) -> &[f32] {
198 let clamped = frac.clamp(0.0, 1.0);
199 let phase = ((clamped * self.phases as f64).round() as usize).min(self.phases - 1);
200 let start = phase * self.taps_per_phase;
201 let end = start + self.taps_per_phase;
202 &self.coeffs[start..end]
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn polyphase_bank_builds_normalized_phases() {
212 let bank = PolyphaseFilterBank::new(44_100.0 / 48_000.0);
213
214 assert_eq!(bank.taps_per_phase(), DEFAULT_TAPS_PER_PHASE);
215 assert_eq!(bank.radius(), DEFAULT_TAPS_PER_PHASE / 2);
216
217 for phase in 0..bank.phases() {
218 let coeffs =
219 &bank.coeffs[phase * bank.taps_per_phase()..(phase + 1) * bank.taps_per_phase()];
220 let sum: f32 = coeffs.iter().sum();
221 assert!((sum - 1.0).abs() <= 1.0e-4);
222 }
223 }
224
225 #[test]
226 fn polyphase_params_reject_invalid_values() {
227 let error = PolyphaseFilterParams {
228 phases: 0,
229 ..PolyphaseFilterParams::default()
230 }
231 .validate()
232 .unwrap_err();
233 assert!(error.to_string().contains("phase count"));
234
235 let error = PolyphaseFilterParams {
236 taps_per_phase: 32,
237 ..PolyphaseFilterParams::default()
238 }
239 .validate()
240 .unwrap_err();
241 assert!(error.to_string().contains("tap count"));
242 }
243}