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 struct Normalizer {
51 in_sample_rate: u32,
52 in_channels: usize,
53 output: OutputFormat,
54
55 stage1_resampler: Option<ResamplerState>,
58 inner_buf: Vec<f32>,
60
61 stage2: Option<OutputStage>,
64 out_buf: Vec<f32>,
66 out_chunk_frames: usize,
68 out_channels: usize,
70
71 out_frame_origin: u64,
73
74 pts_anchor: Option<PtsAnchor>,
76
77 total_out_frames: u64,
79 total_inner_frames: u64,
81}
82
83#[derive(Clone, Copy)]
84struct PtsAnchor {
85 out_frame: u64,
87 pts_ns: i64,
89}
90
91struct ResamplerState {
97 inner: Async<f32>,
98 channels: usize,
99 chunk_in_frames: usize,
101 max_out_frames: usize,
103 in_accum: Vec<f32>,
105 out_scratch: Vec<f32>,
107}
108
109struct OutputStage {
112 out_channels: usize,
113 resampler: Option<ResamplerState>,
116 ch_scratch: Vec<f32>,
118}
119
120impl Normalizer {
121 pub fn new(in_sample_rate: u32, in_channels: u16, output: OutputFormat) -> Result<Self> {
135 let in_channels = in_channels.max(1) as usize;
136
137 let stage1_resampler = if in_sample_rate == SAMPLE_RATE {
139 None
140 } else {
141 Some(ResamplerState::new(in_sample_rate, SAMPLE_RATE, INNER_CH)?)
142 };
143
144 let out_channels = (output.channels.max(1)) as usize;
145 let out_chunk_frames = output.chunk_frames().max(1);
146
147 let stage2 = if output.sample_rate == SAMPLE_RATE && out_channels == INNER_CH {
149 None
150 } else {
151 Some(OutputStage::new(output.sample_rate, out_channels)?)
152 };
153
154 Ok(Self {
155 in_sample_rate,
156 in_channels,
157 output,
158 stage1_resampler,
159 inner_buf: Vec::with_capacity(CHUNK_FRAMES * INNER_CH * 4),
160 stage2,
161 out_buf: Vec::with_capacity(out_chunk_frames * out_channels * 4),
162 out_chunk_frames,
163 out_channels,
164 out_frame_origin: 0,
165 pts_anchor: None,
166 total_out_frames: 0,
167 total_inner_frames: 0,
168 })
169 }
170
171 pub fn in_sample_rate(&self) -> u32 {
173 self.in_sample_rate
174 }
175
176 pub fn output(&self) -> OutputFormat {
178 self.output
179 }
180
181 pub fn is_passthrough(&self) -> bool {
183 self.stage1_resampler.is_none()
184 }
185
186 pub fn is_output_passthrough(&self) -> bool {
188 self.stage2.is_none()
189 }
190
191 pub fn push(&mut self, interleaved: &[f32], device_pts_ns: i64) -> Result<()> {
199 if interleaved.is_empty() {
200 return Ok(());
201 }
202 let in_frames = interleaved.len() / self.in_channels;
203 if in_frames == 0 {
204 return Ok(());
205 }
206
207 self.update_pts_anchor(device_pts_ns);
210
211 let mut stereo = Vec::with_capacity(in_frames * INNER_CH);
213 Self::mix_to_stereo(interleaved, self.in_channels, in_frames, &mut stereo);
214
215 match &mut self.stage1_resampler {
216 None => {
217 self.total_inner_frames += in_frames as u64;
219 self.inner_buf.extend_from_slice(&stereo);
220 }
221 Some(rs) => {
222 rs.in_accum.extend_from_slice(&stereo);
223 rs.drain_into(&mut self.inner_buf, &mut self.total_inner_frames)?;
224 }
225 }
226
227 self.pump_stage2()
229 }
230
231 pub fn pop_chunk(&mut self) -> Option<(Vec<f32>, i64)> {
236 let need = self.out_chunk_frames * self.out_channels;
237 if self.out_buf.len() < need {
238 return None;
239 }
240
241 let pts = self.pts_for_out_frame(self.out_frame_origin);
242
243 let chunk: Vec<f32> = self.out_buf.drain(..need).collect();
244 self.out_frame_origin += self.out_chunk_frames as u64;
245
246 Some((chunk, pts))
247 }
248
249 pub fn buffered_out_frames(&self) -> usize {
251 self.out_buf.len() / self.out_channels
252 }
253
254 fn pump_stage2(&mut self) -> Result<()> {
259 let inner_chunk = CHUNK_FRAMES * INNER_CH;
260
261 match &mut self.stage2 {
262 None => {
263 if !self.inner_buf.is_empty() {
266 let n_frames = (self.inner_buf.len() / INNER_CH) as u64;
267 self.total_out_frames += n_frames;
268 self.out_buf.append(&mut self.inner_buf);
269 }
270 }
271 Some(stage) => {
272 while self.inner_buf.len() >= inner_chunk {
273 let chunk: Vec<f32> = self.inner_buf.drain(..inner_chunk).collect();
275 stage.process_inner_chunk(
276 &chunk,
277 &mut self.out_buf,
278 &mut self.total_out_frames,
279 )?;
280 }
281 }
282 }
283 Ok(())
284 }
285
286 fn mix_to_stereo(src: &[f32], in_ch: usize, in_frames: usize, dst: &mut Vec<f32>) {
288 match in_ch {
289 1 => {
290 for &s in &src[..in_frames] {
292 dst.push(s);
293 dst.push(s);
294 }
295 }
296 2 => {
297 dst.extend_from_slice(&src[..in_frames * 2]);
299 }
300 _ => {
301 for f in 0..in_frames {
304 let base = f * in_ch;
305 dst.push(src[base]);
306 dst.push(src[base + 1]);
307 }
308 }
309 }
310 }
311
312 fn update_pts_anchor(&mut self, device_pts_ns: i64) {
317 let ratio = self.output.sample_rate as f64 / self.in_sample_rate as f64;
319 let projected_out_frame = (self.total_in_frames_estimate() as f64 * ratio) as u64;
320 self.pts_anchor = Some(PtsAnchor {
321 out_frame: projected_out_frame,
322 pts_ns: device_pts_ns,
323 });
324 }
325
326 fn total_in_frames_estimate(&self) -> u64 {
331 let inv = self.in_sample_rate as f64 / SAMPLE_RATE as f64;
332 (self.total_inner_frames as f64 * inv) as u64
333 }
334
335 fn pts_for_out_frame(&self, out_frame: u64) -> i64 {
338 match self.pts_anchor {
339 None => crate::clock::monotonic_now_ns(),
340 Some(anchor) => {
341 let frame_delta = out_frame as i64 - anchor.out_frame as i64;
342 let ns_per_out_frame = 1_000_000_000_i64 / self.output.sample_rate as i64;
343 anchor.pts_ns + frame_delta * ns_per_out_frame
344 }
345 }
346 }
347}
348
349impl ResamplerState {
350 fn new(in_sr: u32, out_sr: u32, channels: usize) -> Result<Self> {
355 let ratio = out_sr as f64 / in_sr as f64;
356 let chunk_in_frames = (in_sr as usize / 50).max(64);
358
359 let params = SincInterpolationParameters {
360 sinc_len: 128,
361 f_cutoff: 0.95,
362 interpolation: SincInterpolationType::Linear,
363 oversampling_factor: 128,
364 window: WindowFunction::BlackmanHarris2,
365 };
366
367 let inner = Async::<f32>::new_sinc(
368 ratio,
369 1.0, ¶ms,
371 chunk_in_frames,
372 channels,
373 FixedAsync::Input,
374 )
375 .map_err(|e| Error::Backend(format!("rubato sinc resampler construction failed: {e}")))?;
376
377 let max_out_frames = inner.output_frames_max();
378
379 Ok(Self {
380 inner,
381 channels,
382 chunk_in_frames,
383 max_out_frames,
384 in_accum: Vec::with_capacity(chunk_in_frames * channels * 4),
385 out_scratch: vec![0.0; max_out_frames * channels],
386 })
387 }
388
389 fn drain_into(&mut self, out_buf: &mut Vec<f32>, total_out_frames: &mut u64) -> Result<()> {
395 let step = self.chunk_in_frames * self.channels;
396
397 while self.in_accum.len() >= step {
398 let in_adapter =
399 InterleavedSlice::new(&self.in_accum[..step], self.channels, self.chunk_in_frames)
400 .map_err(|e| {
401 Error::Backend(format!("rubato interleaved input adapter failed: {e}"))
402 })?;
403
404 let mut out_adapter = InterleavedSlice::new_mut(
405 &mut self.out_scratch[..],
406 self.channels,
407 self.max_out_frames,
408 )
409 .map_err(|e| {
410 Error::Backend(format!("rubato interleaved output adapter failed: {e}"))
411 })?;
412
413 let indexing = Indexing {
414 input_offset: 0,
415 output_offset: 0,
416 partial_len: None,
417 active_channels_mask: None,
418 };
419
420 let (_in_used, out_written) = self
421 .inner
422 .process_into_buffer(&in_adapter, &mut out_adapter, Some(&indexing))
423 .map_err(|e| Error::Backend(format!("rubato process_into_buffer failed: {e}")))?;
424
425 let n_samples = out_written * self.channels;
426 out_buf.extend_from_slice(&self.out_scratch[..n_samples]);
427 *total_out_frames += out_written as u64;
428
429 self.in_accum.drain(..step);
431 }
432 Ok(())
433 }
434}
435
436impl OutputStage {
437 fn new(out_sample_rate: u32, out_channels: usize) -> Result<Self> {
442 let resampler = if out_sample_rate == SAMPLE_RATE {
443 None
444 } else {
445 Some(ResamplerState::new(
447 SAMPLE_RATE,
448 out_sample_rate,
449 out_channels,
450 )?)
451 };
452 Ok(Self {
453 out_channels,
454 resampler,
455 ch_scratch: Vec::with_capacity(CHUNK_FRAMES * out_channels),
456 })
457 }
458
459 fn process_inner_chunk(
462 &mut self,
463 inner_stereo: &[f32],
464 out_buf: &mut Vec<f32>,
465 total_out_frames: &mut u64,
466 ) -> Result<()> {
467 debug_assert_eq!(inner_stereo.len(), CHUNK_FRAMES * INNER_CH);
468
469 self.ch_scratch.clear();
471 match self.out_channels {
472 1 => {
473 for f in 0..CHUNK_FRAMES {
475 let l = inner_stereo[f * 2];
476 let r = inner_stereo[f * 2 + 1];
477 self.ch_scratch.push((l + r) * 0.5);
478 }
479 }
480 2 => {
481 self.ch_scratch.extend_from_slice(inner_stereo);
482 }
483 _ => {
484 for f in 0..CHUNK_FRAMES {
486 let l = inner_stereo[f * 2];
487 for _ in 0..self.out_channels {
488 self.ch_scratch.push(l);
489 }
490 }
491 }
492 }
493
494 match &mut self.resampler {
496 None => {
497 let n_frames = (self.ch_scratch.len() / self.out_channels) as u64;
498 *total_out_frames += n_frames;
499 out_buf.extend_from_slice(&self.ch_scratch);
500 }
501 Some(rs) => {
502 rs.in_accum.extend_from_slice(&self.ch_scratch);
503 rs.drain_into(out_buf, total_out_frames)?;
504 }
505 }
506 Ok(())
507 }
508}
509
510#[cfg(test)]
511mod tests {
512 use super::*;
513 use std::f32::consts::PI;
514
515 fn default_out() -> OutputFormat {
517 OutputFormat::default()
518 }
519
520 #[test]
521 fn mono_48k_to_stereo_duplicates_channels() {
522 let mut n = Normalizer::new(48_000, 1, default_out()).expect("normalizer");
523 assert!(n.is_passthrough());
524 assert!(n.is_output_passthrough());
525 let mono: Vec<f32> = (0..CHUNK_FRAMES).map(|i| (i as f32) * 0.001).collect();
527 n.push(&mono, 0).expect("push");
528 let (chunk, _pts) = n.pop_chunk().expect("one chunk");
529 assert_eq!(chunk.len(), CHUNK_FRAMES * 2);
530 for f in 0..CHUNK_FRAMES {
532 assert_eq!(chunk[f * 2], chunk[f * 2 + 1], "L==R at frame {f}");
533 assert_eq!(chunk[f * 2], mono[f]);
534 }
535 }
536
537 #[test]
538 fn passthrough_preserves_frame_count() {
539 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
540 assert!(n.is_passthrough());
541 assert!(n.is_output_passthrough());
542 let frames = CHUNK_FRAMES * 2 + 100;
544 let stereo: Vec<f32> = (0..frames * 2).map(|i| (i as f32) * 1e-4).collect();
545 n.push(&stereo, 0).expect("push");
546
547 let mut got_frames = 0usize;
548 while let Some((c, _)) = n.pop_chunk() {
549 assert_eq!(c.len(), CHUNK_FRAMES * 2);
550 got_frames += CHUNK_FRAMES;
551 }
552 assert_eq!(got_frames, CHUNK_FRAMES * 2);
554 assert_eq!(n.buffered_out_frames(), 100);
555 }
556
557 #[test]
558 fn stereo_44100_to_48000_yields_about_50_chunks_per_second() {
559 let mut n = Normalizer::new(44_100, 2, default_out()).expect("normalizer");
560 assert!(!n.is_passthrough());
561
562 let in_frames = 44_100;
564 let freq = 440.0_f32;
565 let mut interleaved = Vec::with_capacity(in_frames * 2);
566 for i in 0..in_frames {
567 let s = (2.0 * PI * freq * (i as f32) / 44_100.0).sin() * 0.5;
568 interleaved.push(s); interleaved.push(s); }
571
572 let mut pts = 0i64;
574 for block in interleaved.chunks(441 * 2) {
575 n.push(block, pts).expect("push");
576 pts += (block.len() as i64 / 2) * 1_000_000_000 / 44_100;
577 }
578
579 let mut chunks = 0usize;
580 while let Some((c, _pts)) = n.pop_chunk() {
581 assert_eq!(c.len(), CHUNK_FRAMES * 2);
582 chunks += 1;
583 }
584 assert!(
585 (47..=50).contains(&chunks),
586 "expected ~50 chunks, got {chunks}"
587 );
588 }
589
590 #[test]
591 fn pts_increases_monotonically_across_chunks() {
592 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
593 let frames = CHUNK_FRAMES * 3;
594 let stereo = vec![0.0f32; frames * 2];
595 n.push(&stereo, 100_000_000).expect("push");
596
597 let mut last = i64::MIN;
598 let mut count = 0;
599 while let Some((_, pts)) = n.pop_chunk() {
600 assert!(pts >= last, "pts must be non-decreasing");
601 last = pts;
602 count += 1;
603 }
604 assert_eq!(count, 3);
605 }
606
607 #[test]
611 fn output_16k_mono_yields_320_frame_mono_chunks() {
612 let out = OutputFormat {
613 sample_rate: 16_000,
614 channels: 1,
615 };
616 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
617 assert!(n.is_passthrough()); assert!(!n.is_output_passthrough()); let in_frames = 48_000;
622 let freq = 440.0_f32;
623 let mut pts = 0i64;
624 for blk in 0..(in_frames / 480) {
625 let mut block = Vec::with_capacity(480 * 2);
626 for j in 0..480 {
627 let i = blk * 480 + j;
628 let s = (2.0 * PI * freq * (i as f32) / 48_000.0).sin() * 0.5;
629 block.push(s);
630 block.push(s);
631 }
632 n.push(&block, pts).expect("push");
633 pts += 480 * 1_000_000_000 / 48_000;
634 }
635
636 let mut chunks = 0usize;
637 while let Some((c, _)) = n.pop_chunk() {
638 assert_eq!(c.len(), 320, "16k mono 20ms = 320 sample (mono)");
639 chunks += 1;
640 }
641 assert!(
643 (47..=50).contains(&chunks),
644 "expected ~50 chunks, got {chunks}"
645 );
646 }
647
648 #[test]
650 fn output_16k_stereo_yields_320_frame_640_sample_chunks() {
651 let out = OutputFormat {
652 sample_rate: 16_000,
653 channels: 2,
654 };
655 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
656 let in_frames = 48_000;
657 let stereo: Vec<f32> = (0..in_frames * 2)
658 .map(|i| ((i / 2) as f32 * 0.0001).sin() * 0.3)
659 .collect();
660 for block in stereo.chunks(480 * 2) {
661 n.push(block, 0).expect("push");
662 }
663 let mut chunks = 0usize;
664 while let Some((c, _)) = n.pop_chunk() {
665 assert_eq!(c.len(), 640, "16k stereo 20ms = 320 frame * 2 = 640 sample");
666 chunks += 1;
667 }
668 assert!(
669 (47..=50).contains(&chunks),
670 "expected ~50 chunks, got {chunks}"
671 );
672 }
673
674 #[test]
676 fn output_8k_stereo_yields_160_frame_chunks() {
677 let out = OutputFormat {
678 sample_rate: 8_000,
679 channels: 2,
680 };
681 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
682 let stereo: Vec<f32> = (0..48_000 * 2)
683 .map(|i| (i as f32 * 1e-5).sin() * 0.2)
684 .collect();
685 for block in stereo.chunks(480 * 2) {
686 n.push(block, 0).expect("push");
687 }
688 let mut chunks = 0usize;
689 while let Some((c, _)) = n.pop_chunk() {
690 assert_eq!(c.len(), 320, "8k stereo 20ms = 160 frame * 2 = 320 sample");
691 chunks += 1;
692 }
693 assert!(
694 (47..=50).contains(&chunks),
695 "expected ~50 chunks, got {chunks}"
696 );
697 }
698
699 #[test]
701 fn stereo_to_mono_is_lr_average() {
702 let out = OutputFormat {
704 sample_rate: 48_000,
705 channels: 1,
706 };
707 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
708 let mut stereo = Vec::with_capacity(CHUNK_FRAMES * 2);
710 for _ in 0..CHUNK_FRAMES {
711 stereo.push(0.5);
712 stereo.push(-0.5);
713 }
714 n.push(&stereo, 0).expect("push");
715 let (chunk, _) = n.pop_chunk().expect("one mono chunk");
716 assert_eq!(chunk.len(), CHUNK_FRAMES); for &s in &chunk {
718 assert!(s.abs() < 1e-6, "逆相の平均は 0 付近のはず: {s}");
719 }
720 }
721
722 fn rms(samples: &[f32]) -> f32 {
726 if samples.is_empty() {
727 return 0.0;
728 }
729 let sum_sq: f64 = samples.iter().map(|&x| (x as f64) * (x as f64)).sum();
730 (sum_sq / samples.len() as f64).sqrt() as f32
731 }
732
733 fn zero_crossings(samples: &[f32]) -> usize {
736 let mut crossings = 0;
737 for w in samples.windows(2) {
738 if (w[0] < 0.0 && w[1] >= 0.0) || (w[0] >= 0.0 && w[1] < 0.0) {
740 crossings += 1;
741 }
742 }
743 crossings
744 }
745
746 #[test]
750 fn resample_44100_to_48000_preserves_amplitude_and_frequency() {
751 let mut n = Normalizer::new(44_100, 1, default_out()).expect("normalizer");
752 let freq = 440.0_f32;
753 let amp = 0.5_f32;
754 let in_rate = 44_100usize;
755 let total_frames = in_rate * 2;
757 let mut pts = 0i64;
758 for blk in 0..(total_frames / 441) {
759 let mut block = Vec::with_capacity(441);
760 for j in 0..441 {
761 let i = blk * 441 + j;
762 block.push((2.0 * PI * freq * (i as f32) / in_rate as f32).sin() * amp);
763 }
764 n.push(&block, pts).expect("push");
765 pts += 441 * 1_000_000_000 / in_rate as i64;
766 }
767
768 let mut left: Vec<f32> = Vec::new();
770 while let Some((c, _)) = n.pop_chunk() {
771 assert_eq!(c.len(), CHUNK_FRAMES * 2);
772 for f in 0..CHUNK_FRAMES {
774 assert_eq!(c[f * 2], c[f * 2 + 1], "mono 入力なので L==R");
775 left.push(c[f * 2]);
776 }
777 }
778 assert!(left.len() >= 48_000, "1 秒以上の出力が必要: {}", left.len());
779
780 let start = 12_000;
782 let mid = &left[start..start + 48_000];
783
784 let got_rms = rms(mid);
786 let expect_rms = amp / std::f32::consts::SQRT_2;
787 let rms_err = ((got_rms - expect_rms) / expect_rms).abs();
788 assert!(
789 rms_err < 0.05,
790 "RMS 保存誤差が大きい: got={got_rms} expect={expect_rms} err={rms_err}"
791 );
792
793 let crossings = zero_crossings(mid);
795 let est_freq = crossings as f32 / 2.0; let freq_err = ((est_freq - freq) / freq).abs();
797 assert!(
798 freq_err < 0.02,
799 "周波数 保存誤差が大きい: 交差={crossings} 推定={est_freq}Hz err={freq_err}"
800 );
801 }
802
803 #[test]
806 fn output_16k_mono_preserves_values() {
807 let out = OutputFormat {
808 sample_rate: 16_000,
809 channels: 1,
810 };
811 let mut n = Normalizer::new(48_000, 2, out).expect("normalizer");
812 let freq = 440.0_f32;
813 let amp = 0.5_f32;
814 let in_rate = 48_000usize;
815 let total_frames = in_rate * 2;
816 let mut pts = 0i64;
817 for blk in 0..(total_frames / 480) {
818 let mut block = Vec::with_capacity(480 * 2);
819 for j in 0..480 {
820 let i = blk * 480 + j;
821 let s = (2.0 * PI * freq * (i as f32) / in_rate as f32).sin() * amp;
822 block.push(s); block.push(s); }
825 n.push(&block, pts).expect("push");
826 pts += 480 * 1_000_000_000 / in_rate as i64;
827 }
828
829 let mut mono: Vec<f32> = Vec::new();
830 while let Some((c, _)) = n.pop_chunk() {
831 assert_eq!(c.len(), 320, "16k/mono 20ms = 320 sample(1ch)");
832 mono.extend_from_slice(&c);
833 }
834 assert!(mono.len() >= 16_000, "1 秒以上必要: {}", mono.len());
835
836 let start = 4_000;
838 let mid = &mono[start..start + 16_000];
839
840 let got_rms = rms(mid);
842 let expect_rms = amp / std::f32::consts::SQRT_2;
843 let rms_err = ((got_rms - expect_rms) / expect_rms).abs();
844 assert!(
845 rms_err < 0.05,
846 "16k/mono RMS 保存誤差: got={got_rms} expect={expect_rms} err={rms_err}"
847 );
848
849 let est_freq = zero_crossings(mid) as f32 / 2.0;
851 let freq_err = ((est_freq - freq) / freq).abs();
852 assert!(
853 freq_err < 0.02,
854 "16k/mono 周波数 保存誤差: 推定={est_freq}Hz err={freq_err}"
855 );
856 }
857
858 #[test]
861 fn pts_delta_is_about_20ms_between_chunks() {
862 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
863 let mut device_pts = 1_000_000_000i64; let block_frames = 480usize;
866 for _ in 0..20 {
867 let stereo = vec![0.1f32; block_frames * 2];
868 n.push(&stereo, device_pts).expect("push");
869 device_pts += block_frames as i64 * 1_000_000_000 / 48_000;
870 }
871
872 let mut pts_list = Vec::new();
873 while let Some((_, pts)) = n.pop_chunk() {
874 pts_list.push(pts);
875 }
876 assert!(
877 pts_list.len() >= 5,
878 "十分なチャンク数が必要: {}",
879 pts_list.len()
880 );
881
882 for w in pts_list.windows(2) {
884 let delta = w[1] - w[0];
885 assert!(delta > 0, "PTS は厳密に増加: {} -> {}", w[0], w[1]);
886 assert!(
887 (delta - 20_000_000).abs() <= 1_000_000,
888 "隣接 PTS delta が ~20ms でない: {delta} ns"
889 );
890 }
891 }
892
893 #[test]
896 fn push_empty_and_subframe_are_noops() {
897 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
898 n.push(&[], 0).expect("empty push ok");
900 n.push(&[0.5], 0).expect("subframe push ok");
902 assert!(n.pop_chunk().is_none(), "端数だけでは 1 チャンクも出ない");
903 assert_eq!(n.buffered_out_frames(), 0);
904 }
905
906 #[test]
908 fn silence_input_yields_zero_output() {
909 let mut n = Normalizer::new(48_000, 2, default_out()).expect("normalizer");
910 let stereo = vec![0.0f32; CHUNK_FRAMES * 2];
911 n.push(&stereo, 0).expect("push");
912 let (chunk, _) = n.pop_chunk().expect("one chunk");
913 assert!(chunk.iter().all(|&s| s == 0.0), "無音入力は無音出力");
914 }
915}