Skip to main content

audiobox/
dsp.rs

1use crate::math::{cos_turns, db_to_gain, exp, exp2, sin_turns};
2use crate::{Audio, Error, Limits, Result};
3
4#[derive(Clone, Copy, Debug, Default)]
5pub enum FadeCurve {
6    #[default]
7    Linear,
8    EqualPower,
9    Exponential,
10}
11#[derive(Clone, Copy, Debug)]
12pub enum FilterType {
13    Lowpass,
14    Highpass,
15    Bandpass,
16    Notch,
17    Peaking,
18    Lowshelf,
19    Highshelf,
20    Allpass,
21}
22#[derive(Clone, Copy, Debug)]
23pub struct FilterOptions {
24    pub kind: FilterType,
25    pub frequency: f64,
26    pub q: f64,
27    pub gain_db: f64,
28}
29impl FilterOptions {
30    pub fn new(kind: FilterType, frequency: f64) -> Self {
31        Self {
32            kind,
33            frequency,
34            q: std::f64::consts::FRAC_1_SQRT_2,
35            gain_db: 0.0,
36        }
37    }
38}
39#[derive(Clone, Copy, Debug, Default)]
40pub enum NormalizeUnit {
41    #[default]
42    /// Sample peak in dBFS.
43    Peak,
44    /// Four-times oversampled, windowed-sinc true peak in dBTP.
45    TruePeak,
46    /// K-weighted integrated loudness in LUFS; requires a sample rate >= 4000 Hz.
47    Lufs,
48}
49#[derive(Clone, Copy, Debug)]
50pub struct NormalizeOptions {
51    /// Finite target level in the selected unit.
52    pub to: f64,
53    pub unit: NormalizeUnit,
54    /// Finite measured true-peak ceiling in dBTP, applied only in LUFS mode.
55    pub peak_ceiling_db: f64,
56}
57impl Default for NormalizeOptions {
58    fn default() -> Self {
59        Self {
60            to: -1.0,
61            unit: NormalizeUnit::Peak,
62            peak_ceiling_db: -1.0,
63        }
64    }
65}
66#[derive(Clone, Debug)]
67pub struct NormalizeReport {
68    /// Deterministic f64 measurement in the selected unit, without decimal rounding.
69    /// Negative infinity means silence or wholly gated material.
70    pub measured: f64,
71    /// Applied gain in dB; zero when the measurement is negative infinity.
72    pub gain_db: f64,
73    /// In LUFS mode, the requested gain exceeded the true-peak ceiling's gain.
74    /// Equality does not set this flag.
75    pub limited_by_peak: bool,
76}
77#[derive(Clone, Copy, Debug)]
78pub struct SilenceOptions {
79    pub threshold_db: f64,
80    pub min_duration_ms: f64,
81    pub window_ms: f64,
82    pub padding_ms: f64,
83}
84impl Default for SilenceOptions {
85    fn default() -> Self {
86        Self {
87            threshold_db: -50.0,
88            min_duration_ms: 100.0,
89            window_ms: 10.0,
90            padding_ms: 0.0,
91        }
92    }
93}
94#[derive(Clone, Copy, Debug)]
95pub struct SilentRange {
96    pub from: usize,
97    pub to: usize,
98}
99#[derive(Clone, Copy, Debug)]
100pub struct LimiterOptions {
101    pub ceiling_db: f64,
102    pub lookahead_ms: f64,
103    pub release_ms: f64,
104}
105impl Default for LimiterOptions {
106    fn default() -> Self {
107        Self {
108            ceiling_db: -1.0,
109            lookahead_ms: 5.0,
110            release_ms: 50.0,
111        }
112    }
113}
114
115pub(crate) fn biquad(input: &[f32], c: [f64; 5]) -> Vec<f32> {
116    let [b0, b1, b2, a1, a2] = c;
117    let (mut x1, mut x2, mut y1, mut y2) = (0.0, 0.0, 0.0, 0.0);
118    input
119        .iter()
120        .map(|&s| {
121            let x = s as f64;
122            let y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
123            x2 = x1;
124            x1 = x;
125            y2 = y1;
126            y1 = y;
127            y as f32
128        })
129        .collect()
130}
131fn i0(x: f64) -> f64 {
132    let mut sum = 1.0;
133    let mut term = 1.0;
134    for k in 1..50 {
135        term *= x * x / (4.0 * (k * k) as f64);
136        sum += term;
137        if term < sum * 1e-15 {
138            break;
139        }
140    }
141    sum
142}
143fn sinc_table() -> &'static Vec<f64> {
144    static TABLE: std::sync::OnceLock<Vec<f64>> = std::sync::OnceLock::new();
145    TABLE.get_or_init(|| {
146        let norm = i0(10.0);
147        (0..=32 * 512)
148            .map(|i| {
149                let d = i as f64 / 512.0;
150                let t = d / 32.0;
151                let sinc = if d == 0.0 {
152                    1.0
153                } else {
154                    sin_turns(d / 2.0) / (std::f64::consts::PI * d)
155                };
156                sinc * i0(10.0 * (1.0 - t * t).max(0.0).sqrt()) / norm
157            })
158            .collect()
159    })
160}
161pub(crate) fn sinc_sample(center: f64, ratio: f64, mut sample: impl FnMut(i64) -> f32) -> f32 {
162    let cutoff = ratio.min(1.0) * 0.97;
163    let half = 32.0 / cutoff;
164    let table = sinc_table();
165    let mut sum = 0.0;
166    let mut weight = 0.0;
167    for j in (center - half).ceil() as i64..=(center + half).floor() as i64 {
168        let pos = (center - j as f64).abs() * cutoff * 512.0;
169        let idx = pos as usize;
170        if idx >= table.len() - 1 {
171            continue;
172        }
173        let frac = pos - idx as f64;
174        let w = table[idx] + (table[idx + 1] - table[idx]) * frac;
175        sum += w * sample(j) as f64;
176        weight += w;
177    }
178    if weight.abs() > 1e-15 {
179        (sum / weight) as f32
180    } else {
181        0.0
182    }
183}
184pub(crate) fn resample_channel(input: &[f32], ratio: f64, n: usize) -> Vec<f32> {
185    if input.is_empty() {
186        return vec![0.0; n];
187    }
188    (0..n)
189        .map(|i| {
190            sinc_sample(i as f64 / ratio, ratio, |j| {
191                input[j.clamp(0, input.len() as i64 - 1) as usize]
192            })
193        })
194        .collect()
195}
196impl Audio {
197    pub fn gain(&self, linear: f64) -> Result<Self> {
198        if !linear.is_finite() {
199            return Err(Error::invalid("Gain must be finite"));
200        }
201        let data: Vec<Vec<f32>> = self
202            .data
203            .iter()
204            .map(|c| c.iter().map(|&s| (s as f64 * linear) as f32).collect())
205            .collect();
206        if data.iter().flatten().any(|s| !s.is_finite()) {
207            return Err(Error::invalid("Gain overflow"));
208        }
209        Ok(self.with_data(data))
210    }
211    pub fn gain_db(&self, db: f64) -> Result<Self> {
212        if !db.is_finite() {
213            return Err(Error::invalid("Gain must be finite"));
214        }
215        self.gain(db_to_gain(db))
216    }
217    /// Normalize the complete signal with deterministic measurement and gain math.
218    /// LUFS normalization requires at least 4000 Hz for the K-weighting filters.
219    /// Report-only `loudness`/`true_peak_db` retain platform math; use this method
220    /// or [`Self::normalize_with_report`] when measurements drive rendered samples.
221    pub fn normalize(&self, options: NormalizeOptions) -> Result<Self> {
222        self.normalize_with_report(options).map(|(a, _)| a)
223    }
224    /// Normalize and report the deterministic measurement and applied gain in dB.
225    /// Silence and wholly gated material return unchanged with zero gain. LUFS
226    /// uses the existing short-input and channel-weight policies. Its peak ceiling
227    /// uses four-times oversampled true peak, not a true-peak limiter.
228    pub fn normalize_with_report(
229        &self,
230        options: NormalizeOptions,
231    ) -> Result<(Self, NormalizeReport)> {
232        if !options.to.is_finite() || !options.peak_ceiling_db.is_finite() {
233            return Err(Error::invalid("Normalization levels must be finite"));
234        }
235        if matches!(options.unit, NormalizeUnit::Lufs) && self.rate < 4000 {
236            return Err(Error::invalid(
237                "LUFS normalization requires a sample rate of at least 4000 Hz",
238            ));
239        }
240        let measured = match options.unit {
241            NormalizeUnit::Peak => crate::analyze::render_db(self.peak()),
242            NormalizeUnit::TruePeak => self.render_true_peak_db(),
243            NormalizeUnit::Lufs => self.render_loudness().integrated,
244        };
245        if measured == f64::NEG_INFINITY {
246            return Ok((
247                self.clone(),
248                NormalizeReport {
249                    measured,
250                    gain_db: 0.0,
251                    limited_by_peak: false,
252                },
253            ));
254        }
255        if !measured.is_finite() {
256            return Err(Error::invalid("Normalization measurement overflow"));
257        }
258        let mut gain = options.to - measured;
259        let mut limited = false;
260        if matches!(options.unit, NormalizeUnit::Lufs) {
261            let max = options.peak_ceiling_db - self.render_true_peak_db();
262            if gain > max {
263                gain = max;
264                limited = true;
265            }
266        }
267        // Preserve peak normalization's existing ratio and sample bits. LUFS and
268        // true-peak decisions now also use the crate's deterministic analysis math.
269        let audio = if matches!(options.unit, NormalizeUnit::Peak) {
270            self.gain(db_to_gain(options.to) / self.peak())?
271        } else {
272            self.gain_db(gain)?
273        };
274        Ok((
275            audio,
276            NormalizeReport {
277                measured,
278                gain_db: gain,
279                limited_by_peak: limited,
280            },
281        ))
282    }
283    pub fn fade(&self, in_seconds: f64, out_seconds: f64, curve: FadeCurve) -> Result<Self> {
284        if !in_seconds.is_finite()
285            || !out_seconds.is_finite()
286            || in_seconds < 0.0
287            || out_seconds < 0.0
288        {
289            return Err(Error::invalid(
290                "Fade durations must be finite and non-negative",
291            ));
292        }
293        let n_in = (in_seconds * self.rate as f64)
294            .round()
295            .min(self.frames() as f64) as usize;
296        let n_out = (out_seconds * self.rate as f64)
297            .round()
298            .min(self.frames() as f64) as usize;
299        let shape = |t: f64| match curve {
300            FadeCurve::Linear => t,
301            FadeCurve::EqualPower => sin_turns(t / 4.0),
302            FadeCurve::Exponential => t * t,
303        };
304        let mut data = self.data.clone();
305        for c in &mut data {
306            let len = c.len();
307            for (i, s) in c.iter_mut().enumerate() {
308                let mut g = 1.0;
309                if i < n_in {
310                    g *= shape(i as f64 / n_in.saturating_sub(1).max(1) as f64);
311                }
312                if i >= len - n_out && n_out > 0 {
313                    g *= shape((len - 1 - i) as f64 / n_out.saturating_sub(1).max(1) as f64);
314                }
315                *s *= g as f32;
316            }
317        }
318        Ok(self.with_data(data))
319    }
320    pub fn remove_dc_offset(&self) -> Self {
321        self.with_data(
322            self.data
323                .iter()
324                .map(|c| {
325                    let mean = if c.is_empty() {
326                        0.0
327                    } else {
328                        c.iter().map(|&s| s as f64).sum::<f64>() / c.len() as f64
329                    };
330                    c.iter().map(|&s| (s as f64 - mean) as f32).collect()
331                })
332                .collect(),
333        )
334    }
335    /// Map an absolute source boundary in `0..=frames()` to the output of
336    /// [`Self::resample`], without rendering or allocating PCM.
337    ///
338    /// Uses the existing f64 policy: first `ratio = output_rate / input_rate`,
339    /// then `round(source_frame * ratio)`, with positive half ties upward.
340    /// This is not exact rational arithmetic: e.g. 500 at 1000 -> 1001 Hz maps
341    /// to 500 because the floating product is just below 500.5. The same helper
342    /// determines the actual whole-buffer output length, preserving legacy PCM.
343    /// Mapping is monotonic, maps zero to zero and the input end to output end.
344    ///
345    /// Map body/tail **absolute boundaries**, then subtract mapped boundaries;
346    /// do not sum independently rounded durations. At 48 -> 44.1 kHz, source
347    /// boundaries 0/80/160 map to 0/74/147: 74 body frames and 73 tail frames.
348    /// Invalid source boundaries return `InvalidArgument`; default output shape
349    /// limits (also used by `resample`) return `LimitExceeded`.
350    pub fn resampled_frame_index(&self, source_frame: usize, sample_rate: u32) -> Result<usize> {
351        if source_frame > self.frames() {
352            return Err(Error::invalid("Resampling boundary exceeds input frames"));
353        }
354        let ratio = sample_rate as f64 / self.rate as f64;
355        let frames = resampled_count(self.frames(), ratio);
356        Limits::default().check(frames, self.channels(), sample_rate)?;
357        Ok(resampled_count(source_frame, ratio))
358    }
359    /// Offline Kaiser-windowed sinc resampling with clamped first/last samples.
360    ///
361    /// Output length follows [`Self::resampled_frame_index`] at the input end;
362    /// empty input stays empty and identity-rate input is cloned bit for bit.
363    /// Output frame i samples source position `i / ratio`, so frame zero has
364    /// source-time zero. Symmetric filtering uses future context, with no added
365    /// leading delay or appended filter/release tail. A transient peak need not
366    /// remain at the corresponding rounded index. Short downsampled input may
367    /// round to zero frames. Additional samples beyond the mapped end are never
368    /// emitted. Default [`Limits`] bound the output before allocation.
369    pub fn resample(&self, sample_rate: u32) -> Result<Self> {
370        let ratio = sample_rate as f64 / self.rate as f64;
371        let frames = self.resampled_frame_index(self.frames(), sample_rate)?;
372        if sample_rate == self.rate {
373            return Ok(self.clone());
374        }
375        let mut audio = self.with_data(
376            self.data
377                .iter()
378                .map(|c| resample_channel(c, ratio, frames))
379                .collect(),
380        );
381        audio.rate = sample_rate;
382        Ok(audio)
383    }
384    pub fn filter(&self, options: FilterOptions) -> Result<Self> {
385        let FilterOptions {
386            kind,
387            frequency,
388            q,
389            gain_db,
390        } = options;
391        if !frequency.is_finite()
392            || frequency <= 0.0
393            || frequency >= self.rate as f64 / 2.0
394            || !q.is_finite()
395            || q <= 0.0
396            || !gain_db.is_finite()
397        {
398            return Err(Error::invalid("Invalid filter frequency, Q or gain"));
399        }
400        let turns = frequency / self.rate as f64;
401        let s = sin_turns(turns);
402        let c = cos_turns(turns);
403        let alpha = s / (2.0 * q);
404        let a = db_to_gain(gain_db / 2.0);
405        let beta = 2.0 * a.sqrt() * alpha;
406        let (b0, b1, b2, a0, a1, a2) = match kind {
407            FilterType::Lowpass => (
408                (1.0 - c) / 2.0,
409                1.0 - c,
410                (1.0 - c) / 2.0,
411                1.0 + alpha,
412                -2.0 * c,
413                1.0 - alpha,
414            ),
415            FilterType::Highpass => (
416                (1.0 + c) / 2.0,
417                -(1.0 + c),
418                (1.0 + c) / 2.0,
419                1.0 + alpha,
420                -2.0 * c,
421                1.0 - alpha,
422            ),
423            FilterType::Bandpass => (alpha, 0.0, -alpha, 1.0 + alpha, -2.0 * c, 1.0 - alpha),
424            FilterType::Notch => (1.0, -2.0 * c, 1.0, 1.0 + alpha, -2.0 * c, 1.0 - alpha),
425            FilterType::Allpass => (
426                1.0 - alpha,
427                -2.0 * c,
428                1.0 + alpha,
429                1.0 + alpha,
430                -2.0 * c,
431                1.0 - alpha,
432            ),
433            FilterType::Peaking => (
434                1.0 + alpha * a,
435                -2.0 * c,
436                1.0 - alpha * a,
437                1.0 + alpha / a,
438                -2.0 * c,
439                1.0 - alpha / a,
440            ),
441            FilterType::Lowshelf => (
442                a * ((a + 1.0) - (a - 1.0) * c + beta),
443                2.0 * a * ((a - 1.0) - (a + 1.0) * c),
444                a * ((a + 1.0) - (a - 1.0) * c - beta),
445                (a + 1.0) + (a - 1.0) * c + beta,
446                -2.0 * ((a - 1.0) + (a + 1.0) * c),
447                (a + 1.0) + (a - 1.0) * c - beta,
448            ),
449            FilterType::Highshelf => (
450                a * ((a + 1.0) + (a - 1.0) * c + beta),
451                -2.0 * a * ((a - 1.0) + (a + 1.0) * c),
452                a * ((a + 1.0) + (a - 1.0) * c - beta),
453                (a + 1.0) - (a - 1.0) * c + beta,
454                2.0 * ((a - 1.0) - (a + 1.0) * c),
455                (a + 1.0) - (a - 1.0) * c - beta,
456            ),
457        };
458        let co = [b0 / a0, b1 / a0, b2 / a0, a1 / a0, a2 / a0];
459        if co.iter().any(|x| !x.is_finite()) {
460            return Err(Error::invalid("Filter coefficient overflow"));
461        }
462        let data: Vec<Vec<f32>> = self.data.iter().map(|c| biquad(c, co)).collect();
463        if data.iter().flatten().any(|x| !x.is_finite()) {
464            return Err(Error::invalid("Filter overflow"));
465        }
466        Ok(self.with_data(data))
467    }
468    pub fn limit(&self, options: LimiterOptions) -> Result<Self> {
469        if !options.ceiling_db.is_finite()
470            || !options.lookahead_ms.is_finite()
471            || !options.release_ms.is_finite()
472            || options.lookahead_ms < 0.0
473            || options.release_ms <= 0.0
474        {
475            return Err(Error::invalid("Invalid limiter settings"));
476        }
477        let ceiling = db_to_gain(options.ceiling_db);
478        if !ceiling.is_finite() || ceiling <= 0.0 {
479            return Err(Error::invalid("Invalid limiter ceiling"));
480        }
481        let look = (options.lookahead_ms * self.rate as f64 / 1000.0)
482            .round()
483            .min(self.frames() as f64) as usize;
484        let release = exp(-1.0 / (options.release_ms * self.rate as f64 / 1000.0));
485        let mut data = self.data.clone();
486        let mut gain = 1.0;
487        let mut deque = std::collections::VecDeque::<(usize, f64)>::new();
488        let mut next = 0;
489        for i in 0..self.frames() {
490            while next < self.frames() && next <= i.saturating_add(look) {
491                let peak = self
492                    .data
493                    .iter()
494                    .map(|c| c[next].abs() as f64)
495                    .fold(0.0, f64::max);
496                while deque.back().is_some_and(|(_, p)| *p <= peak) {
497                    deque.pop_back();
498                }
499                deque.push_back((next, peak));
500                next += 1;
501            }
502            while deque.front().is_some_and(|(at, _)| *at < i) {
503                deque.pop_front();
504            }
505            let peak = deque.front().map_or(0.0, |(_, p)| *p);
506            let target = if peak > ceiling { ceiling / peak } else { 1.0 };
507            gain = if target < gain {
508                target
509            } else {
510                target + (gain - target) * release
511            };
512            for c in &mut data {
513                c[i] *= gain as f32;
514            }
515        }
516        Ok(self.with_data(data))
517    }
518    pub fn detect_silence(&self, options: SilenceOptions) -> Result<Vec<SilentRange>> {
519        if !options.threshold_db.is_finite()
520            || !options.min_duration_ms.is_finite()
521            || !options.window_ms.is_finite()
522            || !options.padding_ms.is_finite()
523            || options.min_duration_ms < 0.0
524            || options.window_ms <= 0.0
525            || options.padding_ms < 0.0
526        {
527            return Err(Error::invalid("Invalid silence settings"));
528        }
529        let window = (options.window_ms * self.rate as f64 / 1000.0)
530            .round()
531            .max(1.0)
532            .min(self.frames().max(1) as f64) as usize;
533        let min = (options.min_duration_ms * self.rate as f64 / 1000.0).round() as usize;
534        let threshold = db_to_gain(options.threshold_db * 2.0);
535        let mut ranges = Vec::new();
536        let mut start = None;
537        for i in (0..self.frames()).step_by(window) {
538            let end = (i + window).min(self.frames());
539            let silent = self.data.iter().all(|c| {
540                c[i..end].iter().map(|&s| (s as f64).powi(2)).sum::<f64>() / (end - i) as f64
541                    <= threshold
542            });
543            if silent {
544                if start.is_none() {
545                    start = Some(i);
546                }
547            } else if let Some(from) = start.take() {
548                if i - from >= min {
549                    ranges.push(SilentRange { from, to: i });
550                }
551            }
552        }
553        if let Some(from) = start {
554            if self.frames() - from >= min {
555                ranges.push(SilentRange {
556                    from,
557                    to: self.frames(),
558                });
559            }
560        }
561        Ok(ranges)
562    }
563    pub fn trim_silence(&self, options: SilenceOptions) -> Result<Self> {
564        let ranges = self.detect_silence(options)?;
565        let padding = (options.padding_ms * self.rate as f64 / 1000.0).round() as usize;
566        let start = ranges
567            .first()
568            .filter(|r| r.from == 0)
569            .map_or(0, |r| r.to.saturating_sub(padding));
570        let end = ranges
571            .last()
572            .filter(|r| r.to == self.frames())
573            .map_or(self.frames(), |r| {
574                r.from.saturating_add(padding).min(self.frames())
575            })
576            .max(start);
577        Ok(self.with_data(self.data.iter().map(|c| c[start..end].to_vec()).collect()))
578    }
579    pub fn speed(&self, factor: f64) -> Result<Self> {
580        if !factor.is_finite() || factor <= 0.0 {
581            return Err(Error::invalid("Speed must be positive and finite"));
582        }
583        let frames = (self.frames() as f64 / factor).round() as usize;
584        Limits::default().check(frames, self.channels(), self.rate)?;
585        if factor == 1.0 {
586            return Ok(self.clone());
587        }
588        Ok(self.with_data(
589            self.data
590                .iter()
591                .map(|c| resample_channel(c, 1.0 / factor, frames))
592                .collect(),
593        ))
594    }
595    /// WSOLA tempo adjustment; channel offsets are shared to preserve stereo phase.
596    pub fn tempo(&self, factor: f64) -> Result<Self> {
597        if !factor.is_finite() || factor <= 0.0 {
598            return Err(Error::invalid("Tempo must be positive and finite"));
599        }
600        if factor == 1.0 {
601            return Ok(self.clone());
602        }
603        let frames = (self.frames() as f64 / factor).round() as usize;
604        Limits::default().check(frames, self.channels(), self.rate)?;
605        let window = ((self.rate as f64 * 0.05).round() as usize).max(256) & !1;
606        let hop = window / 2;
607        let radius = window / 8;
608        let mut out = vec![vec![0.0; frames]; self.channels()];
609        let mut weights = vec![0.0; frames];
610        let mut previous = 0;
611        for (block, pos) in (0..frames).step_by(hop).enumerate() {
612            let target = (block as f64 * hop as f64 * factor).round() as usize;
613            if target >= self.frames() {
614                break;
615            }
616            let mut chosen = target;
617            if block > 0 && previous + hop < self.frames() {
618                let template = previous + hop;
619                let mut best = f64::NEG_INFINITY;
620                for candidate in target.saturating_sub(radius)
621                    ..=target.saturating_add(radius).min(self.frames() - 1)
622                {
623                    let count = hop
624                        .min(self.frames() - template)
625                        .min(self.frames() - candidate);
626                    let (mut dot, mut energy) = (0.0, 0.0);
627                    for i in (0..count).step_by(2) {
628                        let a = self.data[0][template + i] as f64;
629                        let b = self.data[0][candidate + i] as f64;
630                        dot += a * b;
631                        energy += b * b;
632                    }
633                    let score = if energy > 0.0 {
634                        dot / energy.sqrt()
635                    } else {
636                        0.0
637                    };
638                    if score > best {
639                        best = score;
640                        chosen = candidate;
641                    }
642                }
643            }
644            let count = window.min(frames - pos).min(self.frames() - chosen);
645            for i in 0..count {
646                let w = (0.5 - 0.5 * cos_turns((i as f64 + 0.5) / window as f64)) as f32;
647                weights[pos + i] += w;
648                for (c, dst) in out.iter_mut().enumerate() {
649                    dst[pos + i] += self.data[c][chosen + i] * w;
650                }
651            }
652            previous = chosen;
653        }
654        for c in &mut out {
655            for (s, w) in c.iter_mut().zip(&weights) {
656                if *w > 1e-12 {
657                    *s /= *w;
658                }
659            }
660        }
661        Ok(self.with_data(out))
662    }
663    pub fn pitch(&self, semitones: f64) -> Result<Self> {
664        if !semitones.is_finite() {
665            return Err(Error::invalid("Pitch must be finite"));
666        }
667        let ratio = exp2(semitones / 12.0);
668        if !ratio.is_finite() || ratio == 0.0 {
669            return Err(Error::invalid("Pitch out of range"));
670        }
671        let stretched = self.tempo(1.0 / ratio)?;
672        let mut shifted = stretched.speed(ratio)?;
673        for c in &mut shifted.data {
674            c.resize(self.frames(), 0.0);
675        }
676        Ok(shifted)
677    }
678}
679
680fn resampled_count(frames: usize, ratio: f64) -> usize {
681    (frames as f64 * ratio).round() as usize
682}
683
684#[cfg(test)]
685mod tests {
686    use super::*;
687
688    #[test]
689    fn the_sinc_table_matches_platform_math() {
690        let table = sinc_table();
691        let norm = i0(10.0);
692        let worst = (0..table.len())
693            .map(|i| {
694                let d = i as f64 / 512.0;
695                let t = d / 32.0;
696                let sinc = if d == 0.0 {
697                    1.0
698                } else {
699                    (std::f64::consts::PI * d).sin() / (std::f64::consts::PI * d)
700                };
701                (table[i] - sinc * i0(10.0 * (1.0 - t * t).max(0.0).sqrt()) / norm).abs()
702            })
703            .fold(0.0, f64::max);
704        assert!(worst < 1e-13, "{worst}");
705    }
706}