1#[allow(clippy::wildcard_imports)]
4use super::*;
5
6impl FilterGraphBuilder {
7 #[must_use]
15 pub fn afade_in(mut self, start_sec: f64, duration_sec: f64) -> Self {
16 self.steps.push(FilterStep::AFadeIn {
17 start: start_sec,
18 duration: duration_sec,
19 });
20 self
21 }
22
23 #[must_use]
29 pub fn afade_out(mut self, start_sec: f64, duration_sec: f64) -> Self {
30 self.steps.push(FilterStep::AFadeOut {
31 start: start_sec,
32 duration: duration_sec,
33 });
34 self
35 }
36
37 #[must_use]
42 pub fn areverse(mut self) -> Self {
43 self.steps.push(FilterStep::AReverse);
44 self
45 }
46
47 #[must_use]
61 pub fn loudness_normalize(mut self, target_lufs: f32, true_peak_db: f32, lra: f32) -> Self {
62 self.steps.push(FilterStep::LoudnessNormalize {
63 target_lufs,
64 true_peak_db,
65 lra,
66 });
67 self
68 }
69
70 #[must_use]
80 pub fn normalize_peak(mut self, target_db: f32) -> Self {
81 self.steps.push(FilterStep::NormalizePeak { target_db });
82 self
83 }
84
85 #[must_use]
94 pub fn agate(mut self, threshold_db: f32, attack_ms: f32, release_ms: f32) -> Self {
95 self.steps.push(FilterStep::ANoiseGate {
96 threshold_db,
97 attack_ms,
98 release_ms,
99 });
100 self
101 }
102
103 #[must_use]
112 pub fn compressor(
113 mut self,
114 threshold_db: f32,
115 ratio: f32,
116 attack_ms: f32,
117 release_ms: f32,
118 makeup_db: f32,
119 ) -> Self {
120 self.steps.push(FilterStep::ACompressor {
121 threshold_db,
122 ratio,
123 attack_ms,
124 release_ms,
125 makeup_db,
126 });
127 self
128 }
129
130 #[must_use]
135 pub fn stereo_to_mono(mut self) -> Self {
136 self.steps.push(FilterStep::StereoToMono);
137 self
138 }
139
140 #[must_use]
148 pub fn channel_map(mut self, mapping: &str) -> Self {
149 self.steps.push(FilterStep::ChannelMap {
150 mapping: mapping.to_string(),
151 });
152 self
153 }
154
155 #[must_use]
162 pub fn audio_delay(mut self, ms: f64) -> Self {
163 self.steps.push(FilterStep::AudioDelay { ms });
164 self
165 }
166
167 #[must_use]
173 pub fn concat_audio(mut self, n_segments: u32) -> Self {
174 self.steps.push(FilterStep::ConcatAudio { n: n_segments });
175 self
176 }
177
178 #[must_use]
180 pub fn volume(mut self, gain_db: f64) -> Self {
181 self.steps.push(FilterStep::Volume(gain_db));
182 self
183 }
184
185 #[must_use]
187 pub fn amix(mut self, inputs: usize) -> Self {
188 self.steps.push(FilterStep::Amix(inputs));
189 self
190 }
191
192 #[must_use]
202 pub fn equalizer(mut self, bands: Vec<EqBand>) -> Self {
203 self.steps.push(FilterStep::ParametricEq { bands });
204 self
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn filter_step_volume_should_produce_correct_args() {
214 let step = FilterStep::Volume(-6.0);
215 assert_eq!(step.filter_name(), "volume");
216 assert_eq!(step.args(), "volume=-6dB");
217 }
218
219 #[test]
220 fn volume_should_convert_db_to_ffmpeg_string() {
221 assert_eq!(FilterStep::Volume(-6.0).args(), "volume=-6dB");
222 assert_eq!(FilterStep::Volume(6.0).args(), "volume=6dB");
223 assert_eq!(FilterStep::Volume(0.0).args(), "volume=0dB");
224 }
225
226 #[test]
227 fn filter_step_amix_should_produce_correct_args() {
228 let step = FilterStep::Amix(3);
229 assert_eq!(step.filter_name(), "amix");
230 assert_eq!(step.args(), "inputs=3");
231 }
232
233 #[test]
234 fn filter_step_parametric_eq_should_have_filter_name_equalizer() {
235 let step = FilterStep::ParametricEq {
236 bands: vec![EqBand::Peak {
237 freq_hz: 1000.0,
238 gain_db: 3.0,
239 q: 1.0,
240 }],
241 };
242 assert_eq!(step.filter_name(), "equalizer");
243 }
244
245 #[test]
246 fn eq_band_peak_should_produce_correct_args() {
247 let band = EqBand::Peak {
248 freq_hz: 1000.0,
249 gain_db: 3.0,
250 q: 1.0,
251 };
252 assert_eq!(band.args(), "f=1000:g=3:width_type=q:width=1");
253 }
254
255 #[test]
256 fn eq_band_low_shelf_should_produce_correct_args() {
257 let band = EqBand::LowShelf {
258 freq_hz: 200.0,
259 gain_db: -3.0,
260 slope: 1.0,
261 };
262 assert_eq!(band.args(), "f=200:g=-3:s=1");
263 }
264
265 #[test]
266 fn eq_band_high_shelf_should_produce_correct_args() {
267 let band = EqBand::HighShelf {
268 freq_hz: 8000.0,
269 gain_db: 2.0,
270 slope: 0.5,
271 };
272 assert_eq!(band.args(), "f=8000:g=2:s=0.5");
273 }
274
275 #[test]
276 fn builder_equalizer_with_single_peak_band_should_succeed() {
277 let result = FilterGraph::builder()
278 .equalizer(vec![EqBand::Peak {
279 freq_hz: 1000.0,
280 gain_db: 3.0,
281 q: 1.0,
282 }])
283 .build();
284 assert!(
285 result.is_ok(),
286 "equalizer with single Peak band must build successfully, got {result:?}"
287 );
288 }
289
290 #[test]
291 fn builder_equalizer_with_multiple_bands_should_succeed() {
292 let result = FilterGraph::builder()
293 .equalizer(vec![
294 EqBand::LowShelf {
295 freq_hz: 200.0,
296 gain_db: -2.0,
297 slope: 1.0,
298 },
299 EqBand::Peak {
300 freq_hz: 1000.0,
301 gain_db: 3.0,
302 q: 1.4,
303 },
304 EqBand::HighShelf {
305 freq_hz: 8000.0,
306 gain_db: 1.0,
307 slope: 0.5,
308 },
309 ])
310 .build();
311 assert!(
312 result.is_ok(),
313 "equalizer with three bands must build successfully, got {result:?}"
314 );
315 }
316
317 #[test]
318 fn builder_equalizer_with_empty_bands_should_return_invalid_config() {
319 let result = FilterGraph::builder().equalizer(vec![]).build();
320 assert!(
321 matches!(result, Err(FilterError::InvalidConfig { .. })),
322 "expected InvalidConfig for empty bands, got {result:?}"
323 );
324 }
325
326 #[test]
327 fn filter_step_afade_in_should_have_correct_filter_name() {
328 let step = FilterStep::AFadeIn {
329 start: 0.0,
330 duration: 1.0,
331 };
332 assert_eq!(step.filter_name(), "afade");
333 }
334
335 #[test]
336 fn filter_step_afade_out_should_have_correct_filter_name() {
337 let step = FilterStep::AFadeOut {
338 start: 4.0,
339 duration: 1.0,
340 };
341 assert_eq!(step.filter_name(), "afade");
342 }
343
344 #[test]
345 fn filter_step_afade_in_should_produce_correct_args() {
346 let step = FilterStep::AFadeIn {
347 start: 0.0,
348 duration: 1.0,
349 };
350 assert_eq!(step.args(), "type=in:start_time=0:duration=1");
351 }
352
353 #[test]
354 fn filter_step_afade_out_should_produce_correct_args() {
355 let step = FilterStep::AFadeOut {
356 start: 4.0,
357 duration: 1.0,
358 };
359 assert_eq!(step.args(), "type=out:start_time=4:duration=1");
360 }
361
362 #[test]
363 fn builder_afade_in_with_valid_params_should_succeed() {
364 let result = FilterGraph::builder().afade_in(0.0, 1.0).build();
365 assert!(
366 result.is_ok(),
367 "afade_in(0.0, 1.0) must build successfully, got {result:?}"
368 );
369 }
370
371 #[test]
372 fn builder_afade_out_with_valid_params_should_succeed() {
373 let result = FilterGraph::builder().afade_out(4.0, 1.0).build();
374 assert!(
375 result.is_ok(),
376 "afade_out(4.0, 1.0) must build successfully, got {result:?}"
377 );
378 }
379
380 #[test]
381 fn builder_afade_in_with_zero_duration_should_return_invalid_config() {
382 let result = FilterGraph::builder().afade_in(0.0, 0.0).build();
383 assert!(
384 matches!(result, Err(FilterError::InvalidConfig { .. })),
385 "expected InvalidConfig for duration=0.0, got {result:?}"
386 );
387 }
388
389 #[test]
390 fn builder_afade_out_with_negative_duration_should_return_invalid_config() {
391 let result = FilterGraph::builder().afade_out(4.0, -1.0).build();
392 assert!(
393 matches!(result, Err(FilterError::InvalidConfig { .. })),
394 "expected InvalidConfig for duration=-1.0, got {result:?}"
395 );
396 }
397
398 #[test]
399 fn filter_step_areverse_should_produce_correct_filter_name_and_empty_args() {
400 let step = FilterStep::AReverse;
401 assert_eq!(step.filter_name(), "areverse");
402 assert_eq!(step.args(), "");
403 }
404
405 #[test]
406 fn builder_areverse_should_succeed() {
407 let result = FilterGraph::builder().areverse().build();
408 assert!(
409 result.is_ok(),
410 "areverse must build successfully, got {result:?}"
411 );
412 }
413
414 #[test]
415 fn filter_step_loudness_normalize_should_produce_correct_filter_name() {
416 let step = FilterStep::LoudnessNormalize {
417 target_lufs: -23.0,
418 true_peak_db: -1.0,
419 lra: 7.0,
420 };
421 assert_eq!(step.filter_name(), "ebur128");
422 }
423
424 #[test]
425 fn filter_step_loudness_normalize_should_produce_correct_args() {
426 let step = FilterStep::LoudnessNormalize {
427 target_lufs: -23.0,
428 true_peak_db: -1.0,
429 lra: 7.0,
430 };
431 assert_eq!(step.args(), "peak=true:metadata=1");
432 }
433
434 #[test]
435 fn builder_loudness_normalize_with_valid_params_should_succeed() {
436 let result = FilterGraph::builder()
437 .loudness_normalize(-23.0, -1.0, 7.0)
438 .build();
439 assert!(
440 result.is_ok(),
441 "loudness_normalize(-23.0, -1.0, 7.0) must build successfully, got {result:?}"
442 );
443 }
444
445 #[test]
446 fn builder_loudness_normalize_with_zero_target_lufs_should_return_invalid_config() {
447 let result = FilterGraph::builder()
448 .loudness_normalize(0.0, -1.0, 7.0)
449 .build();
450 assert!(
451 matches!(result, Err(FilterError::InvalidConfig { .. })),
452 "expected InvalidConfig for target_lufs=0.0, got {result:?}"
453 );
454 }
455
456 #[test]
457 fn builder_loudness_normalize_with_positive_target_lufs_should_return_invalid_config() {
458 let result = FilterGraph::builder()
459 .loudness_normalize(5.0, -1.0, 7.0)
460 .build();
461 assert!(
462 matches!(result, Err(FilterError::InvalidConfig { .. })),
463 "expected InvalidConfig for target_lufs=5.0, got {result:?}"
464 );
465 }
466
467 #[test]
468 fn builder_loudness_normalize_with_positive_true_peak_should_return_invalid_config() {
469 let result = FilterGraph::builder()
470 .loudness_normalize(-23.0, 1.0, 7.0)
471 .build();
472 assert!(
473 matches!(result, Err(FilterError::InvalidConfig { .. })),
474 "expected InvalidConfig for true_peak_db=1.0, got {result:?}"
475 );
476 }
477
478 #[test]
479 fn builder_loudness_normalize_with_zero_lra_should_return_invalid_config() {
480 let result = FilterGraph::builder()
481 .loudness_normalize(-23.0, -1.0, 0.0)
482 .build();
483 assert!(
484 matches!(result, Err(FilterError::InvalidConfig { .. })),
485 "expected InvalidConfig for lra=0.0, got {result:?}"
486 );
487 }
488
489 #[test]
490 fn builder_loudness_normalize_with_negative_lra_should_return_invalid_config() {
491 let result = FilterGraph::builder()
492 .loudness_normalize(-23.0, -1.0, -7.0)
493 .build();
494 assert!(
495 matches!(result, Err(FilterError::InvalidConfig { .. })),
496 "expected InvalidConfig for lra=-7.0, got {result:?}"
497 );
498 }
499
500 #[test]
501 fn filter_step_normalize_peak_should_have_correct_filter_name() {
502 let step = FilterStep::NormalizePeak { target_db: -1.0 };
503 assert_eq!(step.filter_name(), "astats");
504 }
505
506 #[test]
507 fn filter_step_normalize_peak_should_have_correct_args() {
508 let step = FilterStep::NormalizePeak { target_db: -1.0 };
509 assert_eq!(step.args(), "metadata=1");
510 }
511
512 #[test]
513 fn builder_normalize_peak_valid_should_build_successfully() {
514 let result = FilterGraph::builder().normalize_peak(-1.0).build();
515 assert!(
516 result.is_ok(),
517 "normalize_peak(-1.0) must build successfully, got {result:?}"
518 );
519 }
520
521 #[test]
522 fn builder_normalize_peak_with_zero_target_db_should_build_successfully() {
523 let result = FilterGraph::builder().normalize_peak(0.0).build();
525 assert!(
526 result.is_ok(),
527 "normalize_peak(0.0) must build successfully, got {result:?}"
528 );
529 }
530
531 #[test]
532 fn builder_normalize_peak_with_positive_target_db_should_return_invalid_config() {
533 let result = FilterGraph::builder().normalize_peak(1.0).build();
534 assert!(
535 matches!(result, Err(FilterError::InvalidConfig { .. })),
536 "expected InvalidConfig for target_db=1.0, got {result:?}"
537 );
538 }
539
540 #[test]
541 fn filter_step_agate_should_have_correct_filter_name() {
542 let step = FilterStep::ANoiseGate {
543 threshold_db: -40.0,
544 attack_ms: 10.0,
545 release_ms: 100.0,
546 };
547 assert_eq!(step.filter_name(), "agate");
548 }
549
550 #[test]
551 fn filter_step_agate_should_produce_correct_args_for_minus_40_db() {
552 let step = FilterStep::ANoiseGate {
553 threshold_db: -40.0,
554 attack_ms: 10.0,
555 release_ms: 100.0,
556 };
557 let args = step.args();
559 assert!(
560 args.starts_with("threshold=0.010000:"),
561 "expected args to start with threshold=0.010000:, got {args}"
562 );
563 assert!(
564 args.contains("attack=10:"),
565 "expected attack=10: in args, got {args}"
566 );
567 assert!(
568 args.contains("release=100"),
569 "expected release=100 in args, got {args}"
570 );
571 }
572
573 #[test]
574 fn filter_step_agate_should_produce_correct_args_for_zero_db() {
575 let step = FilterStep::ANoiseGate {
576 threshold_db: 0.0,
577 attack_ms: 5.0,
578 release_ms: 50.0,
579 };
580 let args = step.args();
582 assert!(
583 args.starts_with("threshold=1.000000:"),
584 "expected threshold=1.000000: in args, got {args}"
585 );
586 }
587
588 #[test]
589 fn builder_agate_valid_should_build_successfully() {
590 let result = FilterGraph::builder().agate(-40.0, 10.0, 100.0).build();
591 assert!(
592 result.is_ok(),
593 "agate(-40.0, 10.0, 100.0) must build successfully, got {result:?}"
594 );
595 }
596
597 #[test]
598 fn builder_agate_with_zero_attack_should_return_invalid_config() {
599 let result = FilterGraph::builder().agate(-40.0, 0.0, 100.0).build();
600 assert!(
601 matches!(result, Err(FilterError::InvalidConfig { .. })),
602 "expected InvalidConfig for attack_ms=0.0, got {result:?}"
603 );
604 }
605
606 #[test]
607 fn builder_agate_with_negative_attack_should_return_invalid_config() {
608 let result = FilterGraph::builder().agate(-40.0, -1.0, 100.0).build();
609 assert!(
610 matches!(result, Err(FilterError::InvalidConfig { .. })),
611 "expected InvalidConfig for attack_ms=-1.0, got {result:?}"
612 );
613 }
614
615 #[test]
616 fn builder_agate_with_zero_release_should_return_invalid_config() {
617 let result = FilterGraph::builder().agate(-40.0, 10.0, 0.0).build();
618 assert!(
619 matches!(result, Err(FilterError::InvalidConfig { .. })),
620 "expected InvalidConfig for release_ms=0.0, got {result:?}"
621 );
622 }
623
624 #[test]
625 fn builder_agate_with_negative_release_should_return_invalid_config() {
626 let result = FilterGraph::builder().agate(-40.0, 10.0, -50.0).build();
627 assert!(
628 matches!(result, Err(FilterError::InvalidConfig { .. })),
629 "expected InvalidConfig for release_ms=-50.0, got {result:?}"
630 );
631 }
632
633 #[test]
634 fn filter_step_compressor_should_have_correct_filter_name() {
635 let step = FilterStep::ACompressor {
636 threshold_db: -20.0,
637 ratio: 4.0,
638 attack_ms: 10.0,
639 release_ms: 100.0,
640 makeup_db: 6.0,
641 };
642 assert_eq!(step.filter_name(), "acompressor");
643 }
644
645 #[test]
646 fn filter_step_compressor_should_produce_correct_args() {
647 let step = FilterStep::ACompressor {
648 threshold_db: -20.0,
649 ratio: 4.0,
650 attack_ms: 10.0,
651 release_ms: 100.0,
652 makeup_db: 6.0,
653 };
654 assert_eq!(
655 step.args(),
656 "threshold=-20dB:ratio=4:attack=10:release=100:makeup=6dB"
657 );
658 }
659
660 #[test]
661 fn builder_compressor_valid_should_build_successfully() {
662 let result = FilterGraph::builder()
663 .compressor(-20.0, 4.0, 10.0, 100.0, 6.0)
664 .build();
665 assert!(
666 result.is_ok(),
667 "compressor(-20.0, 4.0, 10.0, 100.0, 6.0) must build successfully, got {result:?}"
668 );
669 }
670
671 #[test]
672 fn builder_compressor_with_unity_ratio_should_build_successfully() {
673 let result = FilterGraph::builder()
675 .compressor(-20.0, 1.0, 10.0, 100.0, 0.0)
676 .build();
677 assert!(
678 result.is_ok(),
679 "compressor with ratio=1.0 must build successfully, got {result:?}"
680 );
681 }
682
683 #[test]
684 fn builder_compressor_with_ratio_below_one_should_return_invalid_config() {
685 let result = FilterGraph::builder()
686 .compressor(-20.0, 0.5, 10.0, 100.0, 0.0)
687 .build();
688 assert!(
689 matches!(result, Err(FilterError::InvalidConfig { .. })),
690 "expected InvalidConfig for ratio=0.5, got {result:?}"
691 );
692 }
693
694 #[test]
695 fn builder_compressor_with_zero_attack_should_return_invalid_config() {
696 let result = FilterGraph::builder()
697 .compressor(-20.0, 4.0, 0.0, 100.0, 0.0)
698 .build();
699 assert!(
700 matches!(result, Err(FilterError::InvalidConfig { .. })),
701 "expected InvalidConfig for attack_ms=0.0, got {result:?}"
702 );
703 }
704
705 #[test]
706 fn builder_compressor_with_zero_release_should_return_invalid_config() {
707 let result = FilterGraph::builder()
708 .compressor(-20.0, 4.0, 10.0, 0.0, 0.0)
709 .build();
710 assert!(
711 matches!(result, Err(FilterError::InvalidConfig { .. })),
712 "expected InvalidConfig for release_ms=0.0, got {result:?}"
713 );
714 }
715
716 #[test]
717 fn filter_step_stereo_to_mono_should_have_correct_filter_name() {
718 assert_eq!(FilterStep::StereoToMono.filter_name(), "pan");
719 }
720
721 #[test]
722 fn filter_step_stereo_to_mono_should_produce_correct_args() {
723 assert_eq!(FilterStep::StereoToMono.args(), "mono|c0=0.5*c0+0.5*c1");
724 }
725
726 #[test]
727 fn builder_stereo_to_mono_should_build_successfully() {
728 let result = FilterGraph::builder().stereo_to_mono().build();
729 assert!(
730 result.is_ok(),
731 "stereo_to_mono() must build successfully, got {result:?}"
732 );
733 }
734
735 #[test]
736 fn filter_step_channel_map_should_have_correct_filter_name() {
737 let step = FilterStep::ChannelMap {
738 mapping: "FR|FL".to_string(),
739 };
740 assert_eq!(step.filter_name(), "channelmap");
741 }
742
743 #[test]
744 fn filter_step_channel_map_should_produce_correct_args() {
745 let step = FilterStep::ChannelMap {
746 mapping: "FR|FL".to_string(),
747 };
748 assert_eq!(step.args(), "map=FR|FL");
749 }
750
751 #[test]
752 fn builder_channel_map_valid_should_build_successfully() {
753 let result = FilterGraph::builder().channel_map("FR|FL").build();
754 assert!(
755 result.is_ok(),
756 "channel_map(\"FR|FL\") must build successfully, got {result:?}"
757 );
758 }
759
760 #[test]
761 fn builder_channel_map_with_empty_mapping_should_return_invalid_config() {
762 let result = FilterGraph::builder().channel_map("").build();
763 assert!(
764 matches!(result, Err(FilterError::InvalidConfig { .. })),
765 "expected InvalidConfig for empty mapping, got {result:?}"
766 );
767 }
768
769 #[test]
770 fn filter_step_audio_delay_positive_should_have_correct_filter_name() {
771 let step = FilterStep::AudioDelay { ms: 100.0 };
772 assert_eq!(step.filter_name(), "adelay");
773 }
774
775 #[test]
776 fn filter_step_audio_delay_negative_should_have_correct_filter_name() {
777 let step = FilterStep::AudioDelay { ms: -100.0 };
780 assert_eq!(step.filter_name(), "adelay");
781 }
782
783 #[test]
784 fn filter_step_audio_delay_positive_should_produce_adelay_args() {
785 let step = FilterStep::AudioDelay { ms: 100.0 };
786 assert_eq!(step.args(), "delays=100:all=1");
787 }
788
789 #[test]
790 fn filter_step_audio_delay_zero_should_produce_adelay_args() {
791 let step = FilterStep::AudioDelay { ms: 0.0 };
792 assert_eq!(step.args(), "delays=0:all=1");
793 }
794
795 #[test]
796 fn filter_step_audio_delay_negative_should_produce_atrim_args() {
797 let step = FilterStep::AudioDelay { ms: -100.0 };
798 assert_eq!(step.args(), "start=0.1");
800 }
801
802 #[test]
803 fn builder_audio_delay_positive_should_build_successfully() {
804 let result = FilterGraph::builder().audio_delay(100.0).build();
805 assert!(
806 result.is_ok(),
807 "audio_delay(100.0) must build successfully, got {result:?}"
808 );
809 }
810
811 #[test]
812 fn builder_audio_delay_zero_should_build_successfully() {
813 let result = FilterGraph::builder().audio_delay(0.0).build();
814 assert!(
815 result.is_ok(),
816 "audio_delay(0.0) must build successfully, got {result:?}"
817 );
818 }
819
820 #[test]
821 fn builder_audio_delay_negative_should_build_successfully() {
822 let result = FilterGraph::builder().audio_delay(-100.0).build();
823 assert!(
824 result.is_ok(),
825 "audio_delay(-100.0) must build successfully, got {result:?}"
826 );
827 }
828
829 #[test]
830 fn filter_step_concat_audio_should_have_correct_filter_name() {
831 let step = FilterStep::ConcatAudio { n: 2 };
832 assert_eq!(step.filter_name(), "concat");
833 }
834
835 #[test]
836 fn filter_step_concat_audio_should_produce_correct_args_for_n2() {
837 let step = FilterStep::ConcatAudio { n: 2 };
838 assert_eq!(step.args(), "n=2:v=0:a=1");
839 }
840
841 #[test]
842 fn filter_step_concat_audio_should_produce_correct_args_for_n3() {
843 let step = FilterStep::ConcatAudio { n: 3 };
844 assert_eq!(step.args(), "n=3:v=0:a=1");
845 }
846
847 #[test]
848 fn builder_concat_audio_valid_should_build_successfully() {
849 let result = FilterGraph::builder().concat_audio(2).build();
850 assert!(
851 result.is_ok(),
852 "concat_audio(2) must build successfully, got {result:?}"
853 );
854 }
855
856 #[test]
857 fn builder_concat_audio_with_n1_should_return_invalid_config() {
858 let result = FilterGraph::builder().concat_audio(1).build();
859 assert!(
860 matches!(result, Err(FilterError::InvalidConfig { .. })),
861 "expected InvalidConfig for n=1, got {result:?}"
862 );
863 }
864
865 #[test]
866 fn builder_concat_audio_with_n0_should_return_invalid_config() {
867 let result = FilterGraph::builder().concat_audio(0).build();
868 assert!(
869 matches!(result, Err(FilterError::InvalidConfig { .. })),
870 "expected InvalidConfig for n=0, got {result:?}"
871 );
872 }
873}