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 Peak,
44 TruePeak,
46 Lufs,
48}
49#[derive(Clone, Copy, Debug)]
50pub struct NormalizeOptions {
51 pub to: f64,
53 pub unit: NormalizeUnit,
54 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 pub measured: f64,
71 pub gain_db: f64,
73 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 pub fn normalize(&self, options: NormalizeOptions) -> Result<Self> {
222 self.normalize_with_report(options).map(|(a, _)| a)
223 }
224 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 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 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 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 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}