1use rubato::audioadapter_buffers::direct::InterleavedSlice;
31use rubato::{
32 Async, FixedAsync, Indexing, Resampler, SincInterpolationParameters, SincInterpolationType,
33 WindowFunction,
34};
35
36use crate::types::{Error, OutputFormat, Result, CHANNELS, SAMPLE_RATE};
37
38pub const CHUNK_FRAMES: usize = 960;
40
41const INNER_CH: usize = CHANNELS as usize; pub trait InnerProcessor: Send {
58 fn process(&mut self, samples: &mut [f32]);
61 fn flush(&mut self) -> Vec<f32>;
64}
65
66pub struct Normalizer {
77 in_sample_rate: u32,
78 in_channels: usize,
79
80 stage1_resampler: Option<ResamplerState>,
83 inner_scratch: Vec<f32>,
86 total_inner_frames: u64,
88
89 inner_processor: Option<Box<dyn InnerProcessor>>,
92
93 primary: OutputTap,
96 secondary: Option<OutputTap>,
98}
99
100struct OutputTap {
104 output: OutputFormat,
105 stage2: Option<OutputStage>,
107 out_buf: Vec<f32>,
109 out_chunk_frames: usize,
111 out_channels: usize,
113 out_frame_origin: u64,
115 pts_anchor: Option<PtsAnchor>,
117}
118
119#[derive(Clone, Copy)]
120struct PtsAnchor {
121 out_frame: u64,
123 pts_ns: i64,
125}
126
127struct ResamplerState {
133 inner: Async<f32>,
134 channels: usize,
135 chunk_in_frames: usize,
137 max_out_frames: usize,
139 in_accum: Vec<f32>,
141 out_scratch: Vec<f32>,
143}
144
145struct OutputStage {
148 out_channels: usize,
149 resampler: Option<ResamplerState>,
152 ch_scratch: Vec<f32>,
154}
155
156impl Normalizer {
157 pub fn new(in_sample_rate: u32, in_channels: u16, output: OutputFormat) -> Result<Self> {
171 let in_channels = in_channels.max(1) as usize;
172
173 let stage1_resampler = if in_sample_rate == SAMPLE_RATE {
175 None
176 } else {
177 Some(ResamplerState::new(in_sample_rate, SAMPLE_RATE, INNER_CH)?)
178 };
179
180 Ok(Self {
181 in_sample_rate,
182 in_channels,
183 stage1_resampler,
184 inner_scratch: Vec::with_capacity(CHUNK_FRAMES * INNER_CH * 4),
185 total_inner_frames: 0,
186 inner_processor: None,
187 primary: OutputTap::new(output)?,
188 secondary: None,
189 })
190 }
191
192 pub fn with_secondary(mut self, secondary: OutputFormat) -> Result<Self> {
198 self.secondary = Some(OutputTap::new(secondary)?);
199 Ok(self)
200 }
201
202 pub fn with_inner_processor(mut self, processor: Box<dyn InnerProcessor>) -> Self {
204 self.inner_processor = Some(processor);
205 self
206 }
207
208 pub fn in_sample_rate(&self) -> u32 {
210 self.in_sample_rate
211 }
212
213 pub fn output(&self) -> OutputFormat {
215 self.primary.output
216 }
217
218 pub fn secondary_output(&self) -> Option<OutputFormat> {
220 self.secondary.as_ref().map(|t| t.output)
221 }
222
223 pub fn has_secondary(&self) -> bool {
225 self.secondary.is_some()
226 }
227
228 pub fn is_passthrough(&self) -> bool {
230 self.stage1_resampler.is_none()
231 }
232
233 pub fn is_output_passthrough(&self) -> bool {
235 self.primary.stage2.is_none()
236 }
237
238 pub fn push(&mut self, interleaved: &[f32], device_pts_ns: i64) -> Result<()> {
246 if interleaved.is_empty() {
247 return Ok(());
248 }
249 let in_frames = interleaved.len() / self.in_channels;
250 if in_frames == 0 {
251 return Ok(());
252 }
253
254 self.primary
258 .update_pts_anchor(self.total_inner_frames, device_pts_ns);
259 if let Some(sec) = self.secondary.as_mut() {
260 sec.update_pts_anchor(self.total_inner_frames, device_pts_ns);
261 }
262
263 self.inner_scratch.clear();
266 let mut stereo = Vec::with_capacity(in_frames * INNER_CH);
267 Self::mix_to_stereo(interleaved, self.in_channels, in_frames, &mut stereo);
268 match &mut self.stage1_resampler {
269 None => {
270 self.total_inner_frames += in_frames as u64;
272 self.inner_scratch.extend_from_slice(&stereo);
273 }
274 Some(rs) => {
275 rs.in_accum.extend_from_slice(&stereo);
276 let produced = rs.drain_into(&mut self.inner_scratch)?;
277 self.total_inner_frames += produced;
278 }
279 }
280
281 if let Some(proc) = self.inner_processor.as_mut() {
283 proc.process(&mut self.inner_scratch);
284 }
285
286 self.distribute_inner()
288 }
289
290 pub fn pop_chunk(&mut self) -> Option<(Vec<f32>, i64)> {
295 self.primary.pop()
296 }
297
298 pub fn pop_secondary(&mut self) -> Option<(Vec<f32>, i64)> {
300 self.secondary.as_mut().and_then(OutputTap::pop)
301 }
302
303 pub fn flush(&mut self) {
310 if let Some(proc) = self.inner_processor.as_mut() {
312 let tail = proc.flush();
313 if !tail.is_empty() {
314 self.inner_scratch.clear();
315 self.inner_scratch.extend_from_slice(&tail);
316 let _ = self.distribute_inner();
317 }
318 }
319 self.primary.flush();
321 if let Some(sec) = self.secondary.as_mut() {
322 sec.flush();
323 }
324 }
325
326 pub fn buffered_out_frames(&self) -> usize {
328 self.primary.buffered_out_frames()
329 }
330
331 fn distribute_inner(&mut self) -> Result<()> {
336 if self.inner_scratch.is_empty() {
337 return Ok(());
338 }
339 let inner = std::mem::take(&mut self.inner_scratch);
340 let r_primary = self.primary.feed_inner(&inner);
341 let r_secondary = self
342 .secondary
343 .as_mut()
344 .map(|sec| sec.feed_inner(&inner))
345 .unwrap_or(Ok(()));
346 self.inner_scratch = inner;
348 self.inner_scratch.clear();
349 r_primary.and(r_secondary)
350 }
351
352 fn mix_to_stereo(src: &[f32], in_ch: usize, in_frames: usize, dst: &mut Vec<f32>) {
354 match in_ch {
355 1 => {
356 for &s in &src[..in_frames] {
358 dst.push(s);
359 dst.push(s);
360 }
361 }
362 2 => {
363 dst.extend_from_slice(&src[..in_frames * 2]);
365 }
366 _ => {
367 for f in 0..in_frames {
370 let base = f * in_ch;
371 dst.push(src[base]);
372 dst.push(src[base + 1]);
373 }
374 }
375 }
376 }
377}
378
379impl OutputTap {
380 fn new(output: OutputFormat) -> Result<Self> {
382 let out_channels = (output.channels.max(1)) as usize;
383 let out_chunk_frames = output.chunk_frames().max(1);
384
385 let stage2 = if output.sample_rate == SAMPLE_RATE && out_channels == INNER_CH {
387 None
388 } else {
389 Some(OutputStage::new(output.sample_rate, out_channels)?)
390 };
391
392 Ok(Self {
393 output,
394 stage2,
395 out_buf: Vec::with_capacity(out_chunk_frames * out_channels * 4),
396 out_chunk_frames,
397 out_channels,
398 out_frame_origin: 0,
399 pts_anchor: None,
400 })
401 }
402
403 fn feed_inner(&mut self, inner_stereo: &[f32]) -> Result<()> {
406 if inner_stereo.is_empty() {
407 return Ok(());
408 }
409 match &mut self.stage2 {
410 None => {
411 self.out_buf.extend_from_slice(inner_stereo);
413 }
414 Some(stage) => stage.process_inner(inner_stereo, &mut self.out_buf)?,
415 }
416 Ok(())
417 }
418
419 fn pop(&mut self) -> Option<(Vec<f32>, i64)> {
421 let need = self.out_chunk_frames * self.out_channels;
422 if self.out_buf.len() < need {
423 return None;
424 }
425 let pts = self.pts_for_out_frame(self.out_frame_origin);
426 let chunk: Vec<f32> = self.out_buf.drain(..need).collect();
427 self.out_frame_origin += self.out_chunk_frames as u64;
428 Some((chunk, pts))
429 }
430
431 fn flush(&mut self) {
434 if let Some(stage) = self.stage2.as_mut() {
435 let _ = stage.flush_into(&mut self.out_buf);
437 }
438 let need = self.out_chunk_frames * self.out_channels;
439 let rem = self.out_buf.len() % need;
440 if rem != 0 {
441 let pad = need - rem;
442 self.out_buf.resize(self.out_buf.len() + pad, 0.0);
443 }
444 }
445
446 fn buffered_out_frames(&self) -> usize {
448 self.out_buf.len() / self.out_channels
449 }
450
451 fn update_pts_anchor(&mut self, total_inner_frames: u64, device_pts_ns: i64) {
457 let projected_out_frame = (total_inner_frames as f64 * self.output.sample_rate as f64
458 / SAMPLE_RATE as f64) as u64;
459 self.pts_anchor = Some(PtsAnchor {
460 out_frame: projected_out_frame,
461 pts_ns: device_pts_ns,
462 });
463 }
464
465 fn pts_for_out_frame(&self, out_frame: u64) -> i64 {
468 match self.pts_anchor {
469 None => crate::clock::monotonic_now_ns(),
470 Some(anchor) => {
471 let frame_delta = out_frame as i64 - anchor.out_frame as i64;
472 let ns_per_out_frame = 1_000_000_000_i64 / self.output.sample_rate as i64;
473 anchor.pts_ns + frame_delta * ns_per_out_frame
474 }
475 }
476 }
477}
478
479impl ResamplerState {
480 fn new(in_sr: u32, out_sr: u32, channels: usize) -> Result<Self> {
485 let ratio = out_sr as f64 / in_sr as f64;
486 let chunk_in_frames = (in_sr as usize / 50).max(64);
488
489 let params = SincInterpolationParameters {
490 sinc_len: 128,
491 f_cutoff: 0.95,
492 interpolation: SincInterpolationType::Linear,
493 oversampling_factor: 128,
494 window: WindowFunction::BlackmanHarris2,
495 };
496
497 let inner = Async::<f32>::new_sinc(
498 ratio,
499 1.0, ¶ms,
501 chunk_in_frames,
502 channels,
503 FixedAsync::Input,
504 )
505 .map_err(|e| Error::Backend(format!("rubato sinc resampler construction failed: {e}")))?;
506
507 let max_out_frames = inner.output_frames_max();
508
509 Ok(Self {
510 inner,
511 channels,
512 chunk_in_frames,
513 max_out_frames,
514 in_accum: Vec::with_capacity(chunk_in_frames * channels * 4),
515 out_scratch: vec![0.0; max_out_frames * channels],
516 })
517 }
518
519 fn drain_into(&mut self, out_buf: &mut Vec<f32>) -> Result<u64> {
525 let step = self.chunk_in_frames * self.channels;
526 let mut produced = 0u64;
527
528 while self.in_accum.len() >= step {
529 let in_adapter =
530 InterleavedSlice::new(&self.in_accum[..step], self.channels, self.chunk_in_frames)
531 .map_err(|e| {
532 Error::Backend(format!("rubato interleaved input adapter failed: {e}"))
533 })?;
534
535 let mut out_adapter = InterleavedSlice::new_mut(
536 &mut self.out_scratch[..],
537 self.channels,
538 self.max_out_frames,
539 )
540 .map_err(|e| {
541 Error::Backend(format!("rubato interleaved output adapter failed: {e}"))
542 })?;
543
544 let indexing = Indexing {
545 input_offset: 0,
546 output_offset: 0,
547 partial_len: None,
548 active_channels_mask: None,
549 };
550
551 let (_in_used, out_written) = self
552 .inner
553 .process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
554 .map_err(|e| Error::Backend(format!("rubato process_into_buffer failed: {e}")))?;
555
556 let n_samples = out_written * self.channels;
557 out_buf.extend_from_slice(&self.out_scratch[..n_samples]);
558 produced += out_written as u64;
559
560 self.in_accum.drain(..step);
562 }
563 Ok(produced)
564 }
565
566 fn flush_into(&mut self, out_buf: &mut Vec<f32>) -> Result<u64> {
572 let remaining = self.in_accum.len() / self.channels;
573 if remaining == 0 {
574 return Ok(0);
575 }
576 self.in_accum
578 .resize(self.chunk_in_frames * self.channels, 0.0);
579
580 let in_adapter = InterleavedSlice::new(
581 &self.in_accum[..self.chunk_in_frames * self.channels],
582 self.channels,
583 self.chunk_in_frames,
584 )
585 .map_err(|e| Error::Backend(format!("rubato interleaved input adapter failed: {e}")))?;
586
587 let mut out_adapter = InterleavedSlice::new_mut(
588 &mut self.out_scratch[..],
589 self.channels,
590 self.max_out_frames,
591 )
592 .map_err(|e| Error::Backend(format!("rubato interleaved output adapter failed: {e}")))?;
593
594 let indexing = Indexing {
595 input_offset: 0,
596 output_offset: 0,
597 partial_len: Some(remaining),
598 active_channels_mask: None,
599 };
600
601 let (_in_used, out_written) = self
602 .inner
603 .process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
604 .map_err(|e| Error::Backend(format!("rubato flush process_into_buffer failed: {e}")))?;
605
606 let n_samples = out_written * self.channels;
607 out_buf.extend_from_slice(&self.out_scratch[..n_samples]);
608 self.in_accum.clear();
609 Ok(out_written as u64)
610 }
611}
612
613impl OutputStage {
614 fn new(out_sample_rate: u32, out_channels: usize) -> Result<Self> {
619 let resampler = if out_sample_rate == SAMPLE_RATE {
620 None
621 } else {
622 Some(ResamplerState::new(
624 SAMPLE_RATE,
625 out_sample_rate,
626 out_channels,
627 )?)
628 };
629 Ok(Self {
630 out_channels,
631 resampler,
632 ch_scratch: Vec::with_capacity(CHUNK_FRAMES * out_channels),
633 })
634 }
635
636 fn process_inner(&mut self, inner_stereo: &[f32], out_buf: &mut Vec<f32>) -> Result<()> {
640 let frames = inner_stereo.len() / INNER_CH;
641 if frames == 0 {
642 return Ok(());
643 }
644
645 self.ch_scratch.clear();
647 match self.out_channels {
648 1 => {
649 for f in 0..frames {
651 let l = inner_stereo[f * 2];
652 let r = inner_stereo[f * 2 + 1];
653 self.ch_scratch.push((l + r) * 0.5);
654 }
655 }
656 2 => {
657 self.ch_scratch
658 .extend_from_slice(&inner_stereo[..frames * 2]);
659 }
660 _ => {
661 for f in 0..frames {
663 let l = inner_stereo[f * 2];
664 for _ in 0..self.out_channels {
665 self.ch_scratch.push(l);
666 }
667 }
668 }
669 }
670
671 match &mut self.resampler {
673 None => {
674 out_buf.extend_from_slice(&self.ch_scratch);
675 }
676 Some(rs) => {
677 rs.in_accum.extend_from_slice(&self.ch_scratch);
678 rs.drain_into(out_buf)?;
679 }
680 }
681 Ok(())
682 }
683
684 fn flush_into(&mut self, out_buf: &mut Vec<f32>) -> Result<()> {
687 if let Some(rs) = self.resampler.as_mut() {
688 rs.flush_into(out_buf)?;
689 }
690 Ok(())
691 }
692}
693
694#[cfg(test)]
695mod tests {
696 use super::*;
697 use std::f32::consts::PI;
698
699 fn default_out() -> OutputFormat {
701 OutputFormat::default()
702 }
703
704 #[test]
705 fn mono_48k_to_stereo_duplicates_channels() {
706 let mut n = Normalizer::new(48_000, 1, default_out()).expect("normalizer");
707 assert!(n.is_passthrough());
708 assert!(n.is_output_passthrough());
709 let mono: Vec<f32> = (0..CHUNK_FRAMES).map(|i| (i as f32) * 0.001).collect();
711 n.push(&mono, 0).expect("push");
712 let (chunk, _pts) = n.pop_chunk().expect("one chunk");
713 assert_eq!(chunk.len(), CHUNK_FRAMES * 2);
714 for f in 0..CHUNK_FRAMES {
716 assert_eq!(chunk[f * 2], chunk[f * 2 + 1], "L==R at frame {f}");
717 assert_eq!(chunk[f * 2], mono[f]);
718 }
719 }
720
721 #[test]
722 fn passthrough_preserves_frame_count() {
723 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
724 assert!(n.is_passthrough());
725 assert!(n.is_output_passthrough());
726 let frames = CHUNK_FRAMES * 2 + 100;
728 let stereo: Vec<f32> = (0..frames * 2).map(|i| (i as f32) * 1e-4).collect();
729 n.push(&stereo, 0).expect("push");
730
731 let mut got_frames = 0usize;
732 while let Some((c, _)) = n.pop_chunk() {
733 assert_eq!(c.len(), CHUNK_FRAMES * 2);
734 got_frames += CHUNK_FRAMES;
735 }
736 assert_eq!(got_frames, CHUNK_FRAMES * 2);
738 assert_eq!(n.buffered_out_frames(), 100);
739 }
740
741 #[test]
742 fn stereo_44100_to_48000_yields_about_50_chunks_per_second() {
743 let mut n = Normalizer::new(44_100, 2, default_out()).expect("normalizer");
744 assert!(!n.is_passthrough());
745
746 let in_frames = 44_100;
748 let freq = 440.0_f32;
749 let mut interleaved = Vec::with_capacity(in_frames * 2);
750 for i in 0..in_frames {
751 let s = (2.0 * PI * freq * (i as f32) / 44_100.0).sin() * 0.5;
752 interleaved.push(s); interleaved.push(s); }
755
756 let mut pts = 0i64;
758 for block in interleaved.chunks(441 * 2) {
759 n.push(block, pts).expect("push");
760 pts += (block.len() as i64 / 2) * 1_000_000_000 / 44_100;
761 }
762
763 let mut chunks = 0usize;
764 while let Some((c, _pts)) = n.pop_chunk() {
765 assert_eq!(c.len(), CHUNK_FRAMES * 2);
766 chunks += 1;
767 }
768 assert!(
769 (47..=50).contains(&chunks),
770 "expected ~50 chunks, got {chunks}"
771 );
772 }
773
774 #[test]
775 fn pts_increases_monotonically_across_chunks() {
776 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
777 let frames = CHUNK_FRAMES * 3;
778 let stereo = vec![0.0f32; frames * 2];
779 n.push(&stereo, 100_000_000).expect("push");
780
781 let mut last = i64::MIN;
782 let mut count = 0;
783 while let Some((_, pts)) = n.pop_chunk() {
784 assert!(pts >= last, "pts must be non-decreasing");
785 last = pts;
786 count += 1;
787 }
788 assert_eq!(count, 3);
789 }
790
791 #[test]
795 fn output_16k_mono_yields_320_frame_mono_chunks() {
796 let out = OutputFormat {
797 sample_rate: 16_000,
798 channels: 1,
799 };
800 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
801 assert!(n.is_passthrough()); assert!(!n.is_output_passthrough()); let in_frames = 48_000;
806 let freq = 440.0_f32;
807 let mut pts = 0i64;
808 for blk in 0..(in_frames / 480) {
809 let mut block = Vec::with_capacity(480 * 2);
810 for j in 0..480 {
811 let i = blk * 480 + j;
812 let s = (2.0 * PI * freq * (i as f32) / 48_000.0).sin() * 0.5;
813 block.push(s);
814 block.push(s);
815 }
816 n.push(&block, pts).expect("push");
817 pts += 480 * 1_000_000_000 / 48_000;
818 }
819
820 let mut chunks = 0usize;
821 while let Some((c, _)) = n.pop_chunk() {
822 assert_eq!(c.len(), 320, "16k mono 20ms = 320 sample (mono)");
823 chunks += 1;
824 }
825 assert!(
827 (47..=50).contains(&chunks),
828 "expected ~50 chunks, got {chunks}"
829 );
830 }
831
832 #[test]
834 fn output_16k_stereo_yields_320_frame_640_sample_chunks() {
835 let out = OutputFormat {
836 sample_rate: 16_000,
837 channels: 2,
838 };
839 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
840 let in_frames = 48_000;
841 let stereo: Vec<f32> = (0..in_frames * 2)
842 .map(|i| ((i / 2) as f32 * 0.0001).sin() * 0.3)
843 .collect();
844 for block in stereo.chunks(480 * 2) {
845 n.push(block, 0).expect("push");
846 }
847 let mut chunks = 0usize;
848 while let Some((c, _)) = n.pop_chunk() {
849 assert_eq!(c.len(), 640, "16k stereo 20ms = 320 frame * 2 = 640 sample");
850 chunks += 1;
851 }
852 assert!(
853 (47..=50).contains(&chunks),
854 "expected ~50 chunks, got {chunks}"
855 );
856 }
857
858 #[test]
860 fn output_8k_stereo_yields_160_frame_chunks() {
861 let out = OutputFormat {
862 sample_rate: 8_000,
863 channels: 2,
864 };
865 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
866 let stereo: Vec<f32> = (0..48_000 * 2)
867 .map(|i| (i as f32 * 1e-5).sin() * 0.2)
868 .collect();
869 for block in stereo.chunks(480 * 2) {
870 n.push(block, 0).expect("push");
871 }
872 let mut chunks = 0usize;
873 while let Some((c, _)) = n.pop_chunk() {
874 assert_eq!(c.len(), 320, "8k stereo 20ms = 160 frame * 2 = 320 sample");
875 chunks += 1;
876 }
877 assert!(
878 (47..=50).contains(&chunks),
879 "expected ~50 chunks, got {chunks}"
880 );
881 }
882
883 #[test]
885 fn stereo_to_mono_is_lr_average() {
886 let out = OutputFormat {
888 sample_rate: 48_000,
889 channels: 1,
890 };
891 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
892 let mut stereo = Vec::with_capacity(CHUNK_FRAMES * 2);
894 for _ in 0..CHUNK_FRAMES {
895 stereo.push(0.5);
896 stereo.push(-0.5);
897 }
898 n.push(&stereo, 0).expect("push");
899 let (chunk, _) = n.pop_chunk().expect("one mono chunk");
900 assert_eq!(chunk.len(), CHUNK_FRAMES); for &s in &chunk {
902 assert!(
903 s.abs() < 1e-6,
904 "opposite-phase average should be near 0: {s}"
905 );
906 }
907 }
908
909 fn rms(samples: &[f32]) -> f32 {
913 if samples.is_empty() {
914 return 0.0;
915 }
916 let sum_sq: f64 = samples.iter().map(|&x| (x as f64) * (x as f64)).sum();
917 (sum_sq / samples.len() as f64).sqrt() as f32
918 }
919
920 fn zero_crossings(samples: &[f32]) -> usize {
924 let mut crossings = 0;
925 for w in samples.windows(2) {
926 if (w[0] < 0.0 && w[1] >= 0.0) || (w[0] >= 0.0 && w[1] < 0.0) {
928 crossings += 1;
929 }
930 }
931 crossings
932 }
933
934 #[test]
938 fn resample_44100_to_48000_preserves_amplitude_and_frequency() {
939 let mut n = Normalizer::new(44_100, 1, default_out()).expect("normalizer");
940 let freq = 440.0_f32;
941 let amp = 0.5_f32;
942 let in_rate = 44_100usize;
943 let total_frames = in_rate * 2;
945 let mut pts = 0i64;
946 for blk in 0..(total_frames / 441) {
947 let mut block = Vec::with_capacity(441);
948 for j in 0..441 {
949 let i = blk * 441 + j;
950 block.push((2.0 * PI * freq * (i as f32) / in_rate as f32).sin() * amp);
951 }
952 n.push(&block, pts).expect("push");
953 pts += 441 * 1_000_000_000 / in_rate as i64;
954 }
955
956 let mut left: Vec<f32> = Vec::new();
958 while let Some((c, _)) = n.pop_chunk() {
959 assert_eq!(c.len(), CHUNK_FRAMES * 2);
960 for f in 0..CHUNK_FRAMES {
962 assert_eq!(c[f * 2], c[f * 2 + 1], "L==R for mono input");
963 left.push(c[f * 2]);
964 }
965 }
966 assert!(
967 left.len() >= 48_000,
968 "need at least 1 second of output: {}",
969 left.len()
970 );
971
972 let start = 12_000;
974 let mid = &left[start..start + 48_000];
975
976 let got_rms = rms(mid);
978 let expect_rms = amp / std::f32::consts::SQRT_2;
979 let rms_err = ((got_rms - expect_rms) / expect_rms).abs();
980 assert!(
981 rms_err < 0.05,
982 "RMS preservation error too large: got={got_rms} expect={expect_rms} err={rms_err}"
983 );
984
985 let crossings = zero_crossings(mid);
987 let est_freq = crossings as f32 / 2.0; let freq_err = ((est_freq - freq) / freq).abs();
989 assert!(
990 freq_err < 0.02,
991 "Frequency preservation error too large: crossings={crossings} estimate={est_freq}Hz err={freq_err}"
992 );
993 }
994
995 #[test]
998 fn output_16k_mono_preserves_values() {
999 let out = OutputFormat {
1000 sample_rate: 16_000,
1001 channels: 1,
1002 };
1003 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
1004 let freq = 440.0_f32;
1005 let amp = 0.5_f32;
1006 let in_rate = 48_000usize;
1007 let total_frames = in_rate * 2;
1008 let mut pts = 0i64;
1009 for blk in 0..(total_frames / 480) {
1010 let mut block = Vec::with_capacity(480 * 2);
1011 for j in 0..480 {
1012 let i = blk * 480 + j;
1013 let s = (2.0 * PI * freq * (i as f32) / in_rate as f32).sin() * amp;
1014 block.push(s); block.push(s); }
1017 n.push(&block, pts).expect("push");
1018 pts += 480 * 1_000_000_000 / in_rate as i64;
1019 }
1020
1021 let mut mono: Vec<f32> = Vec::new();
1022 while let Some((c, _)) = n.pop_chunk() {
1023 assert_eq!(c.len(), 320, "16k/mono 20ms = 320 samples (1ch)");
1024 mono.extend_from_slice(&c);
1025 }
1026 assert!(
1027 mono.len() >= 16_000,
1028 "need at least 1 second: {}",
1029 mono.len()
1030 );
1031
1032 let start = 4_000;
1034 let mid = &mono[start..start + 16_000];
1035
1036 let got_rms = rms(mid);
1038 let expect_rms = amp / std::f32::consts::SQRT_2;
1039 let rms_err = ((got_rms - expect_rms) / expect_rms).abs();
1040 assert!(
1041 rms_err < 0.05,
1042 "16k/mono RMS preservation error: got={got_rms} expect={expect_rms} err={rms_err}"
1043 );
1044
1045 let est_freq = zero_crossings(mid) as f32 / 2.0;
1047 let freq_err = ((est_freq - freq) / freq).abs();
1048 assert!(
1049 freq_err < 0.02,
1050 "16k/mono frequency preservation error: estimate={est_freq}Hz err={freq_err}"
1051 );
1052 }
1053
1054 #[test]
1058 fn pts_delta_is_about_20ms_between_chunks() {
1059 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
1060 let mut device_pts = 1_000_000_000i64; let block_frames = 480usize;
1063 for _ in 0..20 {
1064 let stereo = vec![0.1f32; block_frames * 2];
1065 n.push(&stereo, device_pts).expect("push");
1066 device_pts += block_frames as i64 * 1_000_000_000 / 48_000;
1067 }
1068
1069 let mut pts_list = Vec::new();
1070 while let Some((_, pts)) = n.pop_chunk() {
1071 pts_list.push(pts);
1072 }
1073 assert!(
1074 pts_list.len() >= 5,
1075 "need enough chunks: {}",
1076 pts_list.len()
1077 );
1078
1079 for w in pts_list.windows(2) {
1081 let delta = w[1] - w[0];
1082 assert!(
1083 delta > 0,
1084 "PTS must increase strictly: {} -> {}",
1085 w[0],
1086 w[1]
1087 );
1088 assert!(
1089 (delta - 20_000_000).abs() <= 1_000_000,
1090 "adjacent PTS delta is not about 20ms: {delta} ns"
1091 );
1092 }
1093 }
1094
1095 #[test]
1098 fn push_empty_and_subframe_are_noops() {
1099 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
1100 n.push(&[], 0).expect("empty push ok");
1102 n.push(&[0.5], 0).expect("subframe push ok");
1104 assert!(
1105 n.pop_chunk().is_none(),
1106 "partial input alone must not produce a chunk"
1107 );
1108 assert_eq!(n.buffered_out_frames(), 0);
1109 }
1110
1111 #[test]
1113 fn silence_input_yields_zero_output() {
1114 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
1115 let stereo = vec![0.0f32; CHUNK_FRAMES * 2];
1116 n.push(&stereo, 0).expect("push");
1117 let (chunk, _) = n.pop_chunk().expect("one chunk");
1118 assert!(
1119 chunk.iter().all(|&s| s == 0.0),
1120 "silent input must produce silent output"
1121 );
1122 }
1123
1124 #[test]
1128 fn no_secondary_tap_by_default() {
1129 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
1130 assert!(!n.has_secondary());
1131 assert_eq!(n.secondary_output(), None);
1132 let stereo = vec![0.1f32; CHUNK_FRAMES * 2];
1133 n.push(&stereo, 0).expect("push");
1134 assert!(n.pop_secondary().is_none(), "no secondary tap means None");
1135 }
1136
1137 #[test]
1141 fn dual_output_primary_and_secondary_shapes() {
1142 let secondary = OutputFormat {
1143 sample_rate: 16_000,
1144 channels: 1,
1145 };
1146 let mut n = Normalizer::new(48_000, 2, default_out())
1147 .expect("normalizer")
1148 .with_secondary(secondary)
1149 .expect("secondary");
1150 assert!(n.has_secondary());
1151 assert_eq!(n.secondary_output(), Some(secondary));
1152
1153 let mut pts = 0i64;
1155 for _ in 0..100 {
1156 let block = vec![0.2f32; 480 * 2];
1157 n.push(&block, pts).expect("push");
1158 pts += 480 * 1_000_000_000 / 48_000;
1159 }
1160
1161 let mut primary_chunks = 0usize;
1162 while let Some((c, _)) = n.pop_chunk() {
1163 assert_eq!(
1164 c.len(),
1165 CHUNK_FRAMES * 2,
1166 "primary is 48k/stereo = 1920 samples"
1167 );
1168 primary_chunks += 1;
1169 }
1170 let mut secondary_chunks = 0usize;
1171 while let Some((c, _)) = n.pop_secondary() {
1172 assert_eq!(c.len(), 320, "secondary is 16k/mono = 320 samples");
1173 secondary_chunks += 1;
1174 }
1175 assert!(
1176 (47..=50).contains(&primary_chunks),
1177 "primary ~50 chunks: {primary_chunks}"
1178 );
1179 assert!(
1180 (47..=50).contains(&secondary_chunks),
1181 "secondary ~50 chunks: {secondary_chunks}"
1182 );
1183 }
1184
1185 #[test]
1188 fn dual_output_taps_share_pts_axis() {
1189 let secondary = OutputFormat {
1190 sample_rate: 16_000,
1191 channels: 1,
1192 };
1193 let mut n = Normalizer::new(48_000, 2, default_out())
1194 .expect("normalizer")
1195 .with_secondary(secondary)
1196 .expect("secondary");
1197
1198 let mut device_pts = 1_000_000_000i64;
1199 for _ in 0..40 {
1200 let block = vec![0.1f32; 480 * 2];
1201 n.push(&block, device_pts).expect("push");
1202 device_pts += 480 * 1_000_000_000 / 48_000;
1203 }
1204
1205 let mut primary_pts = Vec::new();
1206 while let Some((_, p)) = n.pop_chunk() {
1207 primary_pts.push(p);
1208 }
1209 let mut secondary_pts = Vec::new();
1210 while let Some((_, p)) = n.pop_secondary() {
1211 secondary_pts.push(p);
1212 }
1213 assert!(primary_pts.len() >= 5 && secondary_pts.len() >= 5);
1214 for w in primary_pts.windows(2) {
1215 assert!((w[1] - w[0] - 20_000_000).abs() <= 1_000_000);
1216 }
1217 for w in secondary_pts.windows(2) {
1218 assert!((w[1] - w[0] - 20_000_000).abs() <= 1_000_000);
1219 }
1220 assert!(
1222 (primary_pts[0] - secondary_pts[0]).abs() < 100_000_000,
1223 "primary and secondary start PTS should be close: {} vs {}",
1224 primary_pts[0],
1225 secondary_pts[0]
1226 );
1227 }
1228
1229 struct DoubleProcessor;
1233 impl InnerProcessor for DoubleProcessor {
1234 fn process(&mut self, s: &mut [f32]) {
1235 for x in s.iter_mut() {
1236 *x *= 2.0;
1237 }
1238 }
1239 fn flush(&mut self) -> Vec<f32> {
1240 Vec::new()
1241 }
1242 }
1243
1244 struct DelayProcessor {
1248 held: Vec<f32>,
1249 }
1250 impl DelayProcessor {
1251 fn new(hold: usize) -> Self {
1252 Self {
1253 held: vec![0.0; hold],
1254 }
1255 }
1256 }
1257 impl InnerProcessor for DelayProcessor {
1258 fn process(&mut self, s: &mut [f32]) {
1259 self.held.extend_from_slice(s);
1260 let n = s.len();
1261 s.copy_from_slice(&self.held[..n]);
1262 self.held.drain(..n);
1263 }
1264 fn flush(&mut self) -> Vec<f32> {
1265 std::mem::take(&mut self.held)
1266 }
1267 }
1268
1269 #[test]
1272 fn inner_processor_applies_before_stage2() {
1273 let mut n = Normalizer::new(48_000, 2, default_out())
1274 .expect("normalizer")
1275 .with_inner_processor(Box::new(DoubleProcessor));
1276 let stereo: Vec<f32> = (0..CHUNK_FRAMES * 2).map(|i| (i as f32) * 1e-4).collect();
1277 n.push(&stereo, 0).expect("push");
1278 let (chunk, _) = n.pop_chunk().expect("one chunk");
1279 for (i, &s) in chunk.iter().enumerate() {
1280 assert!(
1281 (s - stereo[i] * 2.0).abs() < 1e-6,
1282 "sample {i} should be doubled"
1283 );
1284 }
1285 }
1286
1287 #[test]
1289 fn inner_processor_affects_both_taps() {
1290 let secondary = OutputFormat {
1291 sample_rate: 48_000,
1292 channels: 2,
1293 };
1294 let mut n = Normalizer::new(48_000, 2, default_out())
1296 .expect("normalizer")
1297 .with_secondary(secondary)
1298 .expect("secondary")
1299 .with_inner_processor(Box::new(DoubleProcessor));
1300 let stereo = vec![0.25f32; CHUNK_FRAMES * 2];
1301 n.push(&stereo, 0).expect("push");
1302 let (p, _) = n.pop_chunk().expect("primary chunk");
1303 let (s, _) = n.pop_secondary().expect("secondary chunk");
1304 assert!(
1305 p.iter().all(|&x| (x - 0.5).abs() < 1e-6),
1306 "primary is doubled"
1307 );
1308 assert!(
1309 s.iter().all(|&x| (x - 0.5).abs() < 1e-6),
1310 "secondary is doubled"
1311 );
1312 }
1313
1314 #[test]
1317 fn stop_flush_emits_processor_tail() {
1318 let mut n = Normalizer::new(48_000, 2, default_out())
1320 .expect("normalizer")
1321 .with_inner_processor(Box::new(DelayProcessor::new(4)));
1322 let stereo: Vec<f32> = (0..CHUNK_FRAMES * 2)
1324 .map(|i| (i as f32 + 1.0) * 1e-4)
1325 .collect();
1326 n.push(&stereo, 0).expect("push");
1327
1328 let (c0, _) = n.pop_chunk().expect("first chunk");
1330 assert_eq!(c0.len(), CHUNK_FRAMES * 2);
1331 assert!(
1332 c0[..4].iter().all(|&x| x == 0.0),
1333 "first 4 samples should be silent due to the delay"
1334 );
1335 assert!(
1336 n.pop_chunk().is_none(),
1337 "only one chunk should be available before flush"
1338 );
1339
1340 n.flush();
1342 let (c1, _) = n.pop_chunk().expect("flushed tail chunk");
1343 assert_eq!(c1.len(), CHUNK_FRAMES * 2, "final chunk is padded to 20ms");
1344 let last4 = &stereo[stereo.len() - 4..];
1346 for (i, &x) in c1[..4].iter().enumerate() {
1347 assert!(
1348 (x - last4[i]).abs() < 1e-6,
1349 "flush tail should match the end of the input"
1350 );
1351 }
1352 }
1353
1354 #[test]
1357 fn stop_flush_drains_secondary_resampler() {
1358 let secondary = OutputFormat {
1359 sample_rate: 16_000,
1360 channels: 1,
1361 };
1362 let mut n = Normalizer::new(48_000, 2, default_out())
1363 .expect("normalizer")
1364 .with_secondary(secondary)
1365 .expect("secondary");
1366 let stereo = vec![0.3f32; CHUNK_FRAMES * 2];
1368 n.push(&stereo, 0).expect("push");
1369
1370 let mut before = 0usize;
1372 while n.pop_secondary().is_some() {
1373 before += 1;
1374 }
1375 n.flush();
1376 let mut after = 0usize;
1378 while let Some((c, _)) = n.pop_secondary() {
1379 assert_eq!(c.len(), 320, "secondary aligns to the fixed 20ms boundary");
1380 after += 1;
1381 }
1382 assert!(after >= 1, "flush should emit the secondary tap's tail");
1383 let _ = before;
1384 }
1385}