1#![forbid(unsafe_code)]
7#![allow(clippy::cast_sign_loss)]
8#![allow(clippy::cast_possible_wrap)]
9#![allow(clippy::cast_lossless)]
10#![allow(clippy::needless_pass_by_value)]
11#![allow(clippy::needless_range_loop)]
12#![allow(clippy::get_first)]
13#![allow(clippy::doc_markdown)]
14
15use bytes::{Bytes, BytesMut};
16
17use crate::error::{GraphError, GraphResult};
18use crate::frame::FilterFrame;
19use crate::node::{Node, NodeId, NodeState, NodeType};
20use crate::port::{AudioPortFormat, InputPort, OutputPort, PortFormat, PortId, PortType};
21
22use oximedia_audio::{AudioBuffer, AudioFrame, ChannelLayout};
23use oximedia_core::SampleFormat;
24
25pub const MAX_CHANNELS: usize = 16;
27
28#[derive(Clone, Debug)]
34pub struct MixMatrix {
35 coefficients: Vec<Vec<f64>>,
37 input_channels: usize,
39 output_channels: usize,
41}
42
43impl MixMatrix {
44 #[must_use]
46 pub fn new(input_channels: usize, output_channels: usize) -> Self {
47 let coefficients = vec![vec![0.0; input_channels]; output_channels];
48 Self {
49 coefficients,
50 input_channels,
51 output_channels,
52 }
53 }
54
55 #[must_use]
57 pub fn identity(channels: usize) -> Self {
58 let mut matrix = Self::new(channels, channels);
59 for i in 0..channels {
60 matrix.coefficients[i][i] = 1.0;
61 }
62 matrix
63 }
64
65 #[must_use]
67 pub fn mono_to_stereo() -> Self {
68 let mut matrix = Self::new(1, 2);
69 matrix.coefficients[0][0] = 1.0; matrix.coefficients[1][0] = 1.0; matrix
72 }
73
74 #[must_use]
76 pub fn stereo_to_mono() -> Self {
77 let mut matrix = Self::new(2, 1);
78 matrix.coefficients[0][0] = 0.5; matrix.coefficients[0][1] = 0.5; matrix
81 }
82
83 #[must_use]
89 pub fn surround51_to_stereo() -> Self {
90 let mut matrix = Self::new(6, 2);
91 let center_gain = 0.707; let surround_gain = 0.707;
93
94 matrix.coefficients[0][0] = 1.0; matrix.coefficients[0][2] = center_gain; matrix.coefficients[0][4] = surround_gain; matrix.coefficients[1][1] = 1.0; matrix.coefficients[1][2] = center_gain; matrix.coefficients[1][5] = surround_gain; matrix
108 }
109
110 #[must_use]
112 pub fn surround71_to_stereo() -> Self {
113 let mut matrix = Self::new(8, 2);
114 let center_gain = 0.707;
115 let surround_gain = 0.5;
116 let back_gain = 0.5;
117
118 matrix.coefficients[0][0] = 1.0; matrix.coefficients[0][2] = center_gain; matrix.coefficients[0][4] = surround_gain; matrix.coefficients[0][6] = back_gain; matrix.coefficients[1][1] = 1.0; matrix.coefficients[1][2] = center_gain; matrix.coefficients[1][5] = surround_gain; matrix.coefficients[1][7] = back_gain; matrix
132 }
133
134 #[must_use]
138 pub fn stereo_to_surround51() -> Self {
139 let mut matrix = Self::new(2, 6);
140
141 matrix.coefficients[0][0] = 1.0; matrix.coefficients[1][1] = 1.0; matrix.coefficients[2][0] = 0.5; matrix.coefficients[2][1] = 0.5; matrix.coefficients[4][0] = 0.3; matrix.coefficients[5][1] = 0.3; matrix
151 }
152
153 pub fn set_coefficient(&mut self, output_channel: usize, input_channel: usize, gain: f64) {
155 if output_channel < self.output_channels && input_channel < self.input_channels {
156 self.coefficients[output_channel][input_channel] = gain;
157 }
158 }
159
160 #[must_use]
162 pub fn get_coefficient(&self, output_channel: usize, input_channel: usize) -> f64 {
163 if output_channel < self.output_channels && input_channel < self.input_channels {
164 self.coefficients[output_channel][input_channel]
165 } else {
166 0.0
167 }
168 }
169
170 #[must_use]
172 pub fn input_channels(&self) -> usize {
173 self.input_channels
174 }
175
176 #[must_use]
178 pub fn output_channels(&self) -> usize {
179 self.output_channels
180 }
181
182 fn apply(&self, input: &[Vec<f64>]) -> Vec<Vec<f64>> {
184 if input.is_empty() {
185 return vec![Vec::new(); self.output_channels];
186 }
187
188 let sample_count = input.get(0).map_or(0, Vec::len);
189 let mut output = vec![vec![0.0; sample_count]; self.output_channels];
190
191 for out_ch in 0..self.output_channels {
192 for sample_idx in 0..sample_count {
193 let mut sum = 0.0;
194 for in_ch in 0..self.input_channels.min(input.len()) {
195 if sample_idx < input[in_ch].len() {
196 sum += input[in_ch][sample_idx] * self.coefficients[out_ch][in_ch];
197 }
198 }
199 output[out_ch][sample_idx] = sum;
200 }
201 }
202
203 output
204 }
205}
206
207impl Default for MixMatrix {
208 fn default() -> Self {
209 Self::identity(2)
210 }
211}
212
213#[derive(Clone, Debug)]
215pub struct CrossfadeConfig {
216 pub duration_samples: usize,
218 pub position: usize,
220 pub from_matrix: MixMatrix,
222 pub to_matrix: MixMatrix,
224}
225
226impl CrossfadeConfig {
227 #[must_use]
229 pub fn new(from: MixMatrix, to: MixMatrix, duration_samples: usize) -> Self {
230 Self {
231 duration_samples,
232 position: 0,
233 from_matrix: from,
234 to_matrix: to,
235 }
236 }
237
238 #[must_use]
240 pub fn factor(&self) -> f64 {
241 if self.duration_samples == 0 {
242 return 1.0;
243 }
244 (self.position as f64 / self.duration_samples as f64).min(1.0)
245 }
246
247 #[must_use]
249 pub fn is_complete(&self) -> bool {
250 self.position >= self.duration_samples
251 }
252
253 pub fn advance(&mut self, samples: usize) {
255 self.position = self.position.saturating_add(samples);
256 }
257}
258
259#[derive(Clone, Debug)]
261pub struct ChannelMixConfig {
262 pub matrix: MixMatrix,
264 pub crossfade: Option<CrossfadeConfig>,
266}
267
268impl Default for ChannelMixConfig {
269 fn default() -> Self {
270 Self {
271 matrix: MixMatrix::identity(2),
272 crossfade: None,
273 }
274 }
275}
276
277impl ChannelMixConfig {
278 #[must_use]
280 pub fn new(matrix: MixMatrix) -> Self {
281 Self {
282 matrix,
283 crossfade: None,
284 }
285 }
286
287 #[must_use]
289 pub fn mono_to_stereo() -> Self {
290 Self::new(MixMatrix::mono_to_stereo())
291 }
292
293 #[must_use]
295 pub fn stereo_to_mono() -> Self {
296 Self::new(MixMatrix::stereo_to_mono())
297 }
298
299 #[must_use]
301 pub fn surround51_to_stereo() -> Self {
302 Self::new(MixMatrix::surround51_to_stereo())
303 }
304
305 #[must_use]
307 pub fn with_crossfade(mut self, from: MixMatrix, duration_samples: usize) -> Self {
308 self.crossfade = Some(CrossfadeConfig::new(
309 from,
310 self.matrix.clone(),
311 duration_samples,
312 ));
313 self
314 }
315}
316
317pub struct ChannelMixFilter {
332 id: NodeId,
333 name: String,
334 state: NodeState,
335 config: ChannelMixConfig,
336 inputs: Vec<InputPort>,
337 outputs: Vec<OutputPort>,
338}
339
340impl ChannelMixFilter {
341 #[must_use]
343 pub fn new(id: NodeId, name: impl Into<String>, config: ChannelMixConfig) -> Self {
344 let input_format = PortFormat::Audio(
345 AudioPortFormat::any().with_channels(config.matrix.input_channels() as u32),
346 );
347 let output_format = PortFormat::Audio(
348 AudioPortFormat::any().with_channels(config.matrix.output_channels() as u32),
349 );
350
351 Self {
352 id,
353 name: name.into(),
354 state: NodeState::Idle,
355 config,
356 inputs: vec![
357 InputPort::new(PortId(0), "input", PortType::Audio).with_format(input_format)
358 ],
359 outputs: vec![
360 OutputPort::new(PortId(0), "output", PortType::Audio).with_format(output_format)
361 ],
362 }
363 }
364
365 #[must_use]
367 pub fn config(&self) -> &ChannelMixConfig {
368 &self.config
369 }
370
371 pub fn set_config(&mut self, config: ChannelMixConfig) {
373 self.config = config;
374 }
375
376 pub fn crossfade_to(&mut self, target: MixMatrix, duration_samples: usize) {
378 let from = self.config.matrix.clone();
379 self.config.crossfade = Some(CrossfadeConfig::new(from, target.clone(), duration_samples));
380 self.config.matrix = target;
381 }
382
383 fn frame_to_samples(frame: &AudioFrame) -> Vec<Vec<f64>> {
385 let channels = frame.channels.count();
386 let sample_count = frame.sample_count();
387
388 if sample_count == 0 {
389 return vec![Vec::new(); channels];
390 }
391
392 let mut output = vec![Vec::with_capacity(sample_count); channels];
393
394 match &frame.samples {
395 AudioBuffer::Interleaved(data) => {
396 Self::convert_interleaved(data, frame.format, channels, &mut output);
397 }
398 AudioBuffer::Planar(planes) => {
399 Self::convert_planar(planes, frame.format, &mut output);
400 }
401 }
402
403 output
404 }
405
406 fn convert_interleaved(
408 data: &Bytes,
409 format: SampleFormat,
410 channels: usize,
411 output: &mut [Vec<f64>],
412 ) {
413 let bytes_per_sample = format.bytes_per_sample();
414 if bytes_per_sample == 0 || channels == 0 {
415 return;
416 }
417
418 let sample_count = data.len() / (bytes_per_sample * channels);
419
420 for i in 0..sample_count {
421 for ch in 0..channels {
422 let offset = (i * channels + ch) * bytes_per_sample;
423 if offset + bytes_per_sample <= data.len() {
424 let sample =
425 Self::bytes_to_f64(&data[offset..offset + bytes_per_sample], format);
426 output[ch].push(sample);
427 }
428 }
429 }
430 }
431
432 fn convert_planar(planes: &[Bytes], format: SampleFormat, output: &mut [Vec<f64>]) {
434 let bytes_per_sample = format.bytes_per_sample();
435 if bytes_per_sample == 0 {
436 return;
437 }
438
439 for (ch, plane) in planes.iter().enumerate() {
440 if ch >= output.len() {
441 break;
442 }
443 let sample_count = plane.len() / bytes_per_sample;
444 for i in 0..sample_count {
445 let offset = i * bytes_per_sample;
446 if offset + bytes_per_sample <= plane.len() {
447 let sample =
448 Self::bytes_to_f64(&plane[offset..offset + bytes_per_sample], format);
449 output[ch].push(sample);
450 }
451 }
452 }
453 }
454
455 fn bytes_to_f64(bytes: &[u8], format: SampleFormat) -> f64 {
457 match format {
458 SampleFormat::U8 => {
459 if bytes.is_empty() {
460 return 0.0;
461 }
462 (f64::from(bytes[0]) - 128.0) / 128.0
463 }
464 SampleFormat::S16 => {
465 if bytes.len() < 2 {
466 return 0.0;
467 }
468 let sample = i16::from_le_bytes([bytes[0], bytes[1]]);
469 f64::from(sample) / f64::from(i16::MAX)
470 }
471 SampleFormat::S32 => {
472 if bytes.len() < 4 {
473 return 0.0;
474 }
475 let sample = i32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
476 f64::from(sample) / f64::from(i32::MAX)
477 }
478 SampleFormat::F32 => {
479 if bytes.len() < 4 {
480 return 0.0;
481 }
482 f64::from(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
483 }
484 SampleFormat::F64 => {
485 if bytes.len() < 8 {
486 return 0.0;
487 }
488 f64::from_le_bytes([
489 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
490 ])
491 }
492 _ => 0.0,
493 }
494 }
495
496 fn samples_to_frame(
498 samples: Vec<Vec<f64>>,
499 format: SampleFormat,
500 sample_rate: u32,
501 output_layout: ChannelLayout,
502 ) -> AudioFrame {
503 let channel_count = output_layout.count();
504 if samples.is_empty() || samples[0].is_empty() || channel_count == 0 {
505 return AudioFrame::new(format, sample_rate, output_layout);
506 }
507
508 let sample_count = samples[0].len();
509 let bytes_per_sample = format.bytes_per_sample();
510 let mut buffer = BytesMut::with_capacity(sample_count * channel_count * bytes_per_sample);
511
512 for i in 0..sample_count {
513 for ch in 0..channel_count {
514 let sample = if ch < samples.len() && i < samples[ch].len() {
515 samples[ch][i]
516 } else {
517 0.0
518 };
519 Self::f64_to_bytes(sample, format, &mut buffer);
520 }
521 }
522
523 let mut frame = AudioFrame::new(format, sample_rate, output_layout);
524 frame.samples = AudioBuffer::Interleaved(buffer.freeze());
525 frame
526 }
527
528 fn f64_to_bytes(sample: f64, format: SampleFormat, buffer: &mut BytesMut) {
530 let clamped = sample.clamp(-1.0, 1.0);
531
532 match format {
533 SampleFormat::U8 => {
534 let value = ((clamped * 128.0) + 128.0) as u8;
535 buffer.extend_from_slice(&[value]);
536 }
537 SampleFormat::S16 => {
538 let value = (clamped * f64::from(i16::MAX)) as i16;
539 buffer.extend_from_slice(&value.to_le_bytes());
540 }
541 SampleFormat::S32 => {
542 let value = (clamped * f64::from(i32::MAX)) as i32;
543 buffer.extend_from_slice(&value.to_le_bytes());
544 }
545 SampleFormat::F32 => {
546 #[allow(clippy::cast_possible_truncation)]
547 let value = clamped as f32;
548 buffer.extend_from_slice(&value.to_le_bytes());
549 }
550 SampleFormat::F64 => {
551 buffer.extend_from_slice(&clamped.to_le_bytes());
552 }
553 _ => {}
554 }
555 }
556
557 fn apply_crossfade(&mut self, input: &[Vec<f64>]) -> Vec<Vec<f64>> {
559 if let Some(ref mut crossfade) = self.config.crossfade {
560 if crossfade.is_complete() {
561 self.config.crossfade = None;
562 return self.config.matrix.apply(input);
563 }
564
565 let from_output = crossfade.from_matrix.apply(input);
566 let to_output = crossfade.to_matrix.apply(input);
567 let _factor = crossfade.factor(); let sample_count = from_output.get(0).map_or(0, Vec::len);
570 let output_channels = crossfade.to_matrix.output_channels();
571 let mut output = vec![Vec::with_capacity(sample_count); output_channels];
572
573 for sample_idx in 0..sample_count {
574 let local_factor = ((crossfade.position + sample_idx) as f64
575 / crossfade.duration_samples as f64)
576 .min(1.0);
577
578 for ch in 0..output_channels {
579 let from_sample = from_output
580 .get(ch)
581 .and_then(|v| v.get(sample_idx))
582 .unwrap_or(&0.0);
583 let to_sample = to_output
584 .get(ch)
585 .and_then(|v| v.get(sample_idx))
586 .unwrap_or(&0.0);
587 let mixed = from_sample * (1.0 - local_factor) + to_sample * local_factor;
588 output[ch].push(mixed);
589 }
590 }
591
592 crossfade.advance(sample_count);
593 output
594 } else {
595 self.config.matrix.apply(input)
596 }
597 }
598}
599
600impl Node for ChannelMixFilter {
601 fn id(&self) -> NodeId {
602 self.id
603 }
604
605 fn name(&self) -> &str {
606 &self.name
607 }
608
609 fn node_type(&self) -> NodeType {
610 NodeType::Filter
611 }
612
613 fn state(&self) -> NodeState {
614 self.state
615 }
616
617 fn set_state(&mut self, state: NodeState) -> GraphResult<()> {
618 if !self.state.can_transition_to(state) {
619 return Err(GraphError::InvalidStateTransition {
620 node: self.id,
621 from: self.state.to_string(),
622 to: state.to_string(),
623 });
624 }
625 self.state = state;
626 Ok(())
627 }
628
629 fn inputs(&self) -> &[InputPort] {
630 &self.inputs
631 }
632
633 fn outputs(&self) -> &[OutputPort] {
634 &self.outputs
635 }
636
637 fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>> {
638 let frame = match input {
639 Some(FilterFrame::Audio(frame)) => frame,
640 Some(_) => {
641 return Err(GraphError::PortTypeMismatch {
642 expected: "Audio".to_string(),
643 actual: "Video".to_string(),
644 });
645 }
646 None => return Ok(None),
647 };
648
649 let input_samples = Self::frame_to_samples(&frame);
651
652 let output_samples = self.apply_crossfade(&input_samples);
654
655 let output_layout = ChannelLayout::from_count(self.config.matrix.output_channels());
657
658 let output_frame = Self::samples_to_frame(
660 output_samples,
661 frame.format,
662 frame.sample_rate,
663 output_layout,
664 );
665
666 Ok(Some(FilterFrame::Audio(output_frame)))
667 }
668
669 fn reset(&mut self) -> GraphResult<()> {
670 self.config.crossfade = None;
671 self.set_state(NodeState::Idle)
672 }
673}
674
675#[cfg(test)]
676mod tests {
677 use super::*;
678
679 #[test]
680 fn test_mix_matrix_identity() {
681 let matrix = MixMatrix::identity(2);
682 assert_eq!(matrix.input_channels(), 2);
683 assert_eq!(matrix.output_channels(), 2);
684 assert_eq!(matrix.get_coefficient(0, 0), 1.0);
685 assert_eq!(matrix.get_coefficient(1, 1), 1.0);
686 assert_eq!(matrix.get_coefficient(0, 1), 0.0);
687 }
688
689 #[test]
690 fn test_mix_matrix_mono_to_stereo() {
691 let matrix = MixMatrix::mono_to_stereo();
692 assert_eq!(matrix.input_channels(), 1);
693 assert_eq!(matrix.output_channels(), 2);
694 assert_eq!(matrix.get_coefficient(0, 0), 1.0);
695 assert_eq!(matrix.get_coefficient(1, 0), 1.0);
696 }
697
698 #[test]
699 fn test_mix_matrix_stereo_to_mono() {
700 let matrix = MixMatrix::stereo_to_mono();
701 assert_eq!(matrix.input_channels(), 2);
702 assert_eq!(matrix.output_channels(), 1);
703 assert_eq!(matrix.get_coefficient(0, 0), 0.5);
704 assert_eq!(matrix.get_coefficient(0, 1), 0.5);
705 }
706
707 #[test]
708 fn test_mix_matrix_apply() {
709 let matrix = MixMatrix::stereo_to_mono();
710 let input = vec![
711 vec![1.0, 0.0, -1.0], vec![1.0, 0.0, -1.0], ];
714 let output = matrix.apply(&input);
715
716 assert_eq!(output.len(), 1);
717 assert_eq!(output[0].len(), 3);
718 assert!((output[0][0] - 1.0).abs() < f64::EPSILON);
719 assert!(output[0][1].abs() < f64::EPSILON);
720 assert!((output[0][2] + 1.0).abs() < f64::EPSILON);
721 }
722
723 #[test]
724 fn test_mix_matrix_51_to_stereo() {
725 let matrix = MixMatrix::surround51_to_stereo();
726 assert_eq!(matrix.input_channels(), 6);
727 assert_eq!(matrix.output_channels(), 2);
728
729 assert_eq!(matrix.get_coefficient(0, 0), 1.0); assert_eq!(matrix.get_coefficient(1, 1), 1.0); }
733
734 #[test]
735 fn test_crossfade_config() {
736 let from = MixMatrix::identity(2);
737 let to = MixMatrix::stereo_to_mono();
738 let mut crossfade = CrossfadeConfig::new(from, to, 1000);
739
740 assert!(!crossfade.is_complete());
741 assert!(crossfade.factor() < f64::EPSILON);
742
743 crossfade.advance(500);
744 assert!(!crossfade.is_complete());
745 assert!((crossfade.factor() - 0.5).abs() < f64::EPSILON);
746
747 crossfade.advance(500);
748 assert!(crossfade.is_complete());
749 assert!((crossfade.factor() - 1.0).abs() < f64::EPSILON);
750 }
751
752 #[test]
753 fn test_channel_mix_filter_creation() {
754 let config = ChannelMixConfig::stereo_to_mono();
755 let filter = ChannelMixFilter::new(NodeId(1), "downmix", config);
756
757 assert_eq!(filter.id(), NodeId(1));
758 assert_eq!(filter.name(), "downmix");
759 assert_eq!(filter.node_type(), NodeType::Filter);
760 }
761
762 #[test]
763 fn test_channel_mix_filter_ports() {
764 let config = ChannelMixConfig::stereo_to_mono();
765 let filter = ChannelMixFilter::new(NodeId(0), "test", config);
766
767 assert_eq!(filter.inputs().len(), 1);
768 assert_eq!(filter.outputs().len(), 1);
769 assert_eq!(filter.inputs()[0].port_type, PortType::Audio);
770 }
771
772 #[test]
773 fn test_bytes_to_f64() {
774 let sample = ChannelMixFilter::bytes_to_f64(&[128], SampleFormat::U8);
775 assert!(sample.abs() < 0.01);
776
777 let sample = ChannelMixFilter::bytes_to_f64(&[0, 0], SampleFormat::S16);
778 assert!(sample.abs() < f64::EPSILON);
779 }
780
781 #[test]
782 fn test_f64_to_bytes_roundtrip() {
783 let original = 0.5;
784 let mut buffer = BytesMut::new();
785
786 ChannelMixFilter::f64_to_bytes(original, SampleFormat::F32, &mut buffer);
787 let converted = ChannelMixFilter::bytes_to_f64(&buffer, SampleFormat::F32);
788
789 assert!((original - converted).abs() < 0.0001);
790 }
791
792 #[test]
793 fn test_process_none() {
794 let config = ChannelMixConfig::default();
795 let mut filter = ChannelMixFilter::new(NodeId(0), "test", config);
796
797 let result = filter.process(None).expect("process should succeed");
798 assert!(result.is_none());
799 }
800
801 #[test]
802 fn test_process_stereo_to_mono() {
803 let config = ChannelMixConfig::stereo_to_mono();
804 let mut filter = ChannelMixFilter::new(NodeId(0), "test", config);
805
806 let mut frame = AudioFrame::new(SampleFormat::F32, 48000, ChannelLayout::Stereo);
807 let mut samples = BytesMut::new();
808 for _ in 0..100 {
810 samples.extend_from_slice(&0.5f32.to_le_bytes()); samples.extend_from_slice(&0.5f32.to_le_bytes()); }
813 frame.samples = AudioBuffer::Interleaved(samples.freeze());
814
815 let result = filter
816 .process(Some(FilterFrame::Audio(frame)))
817 .expect("process should succeed");
818 assert!(result.is_some());
819
820 if let Some(FilterFrame::Audio(output)) = result {
821 assert_eq!(output.channels.count(), 1);
822 }
823 }
824
825 #[test]
826 fn test_crossfade_to() {
827 let config = ChannelMixConfig::default();
828 let mut filter = ChannelMixFilter::new(NodeId(0), "test", config);
829
830 let target = MixMatrix::stereo_to_mono();
831 filter.crossfade_to(target, 1000);
832
833 assert!(filter.config.crossfade.is_some());
834 }
835
836 #[test]
837 fn test_state_transitions() {
838 let config = ChannelMixConfig::default();
839 let mut filter = ChannelMixFilter::new(NodeId(0), "test", config);
840
841 assert!(filter.set_state(NodeState::Processing).is_ok());
842 assert_eq!(filter.state(), NodeState::Processing);
843
844 assert!(filter.reset().is_ok());
845 assert_eq!(filter.state(), NodeState::Idle);
846 }
847
848 #[test]
849 fn test_set_coefficient() {
850 let mut matrix = MixMatrix::new(2, 2);
851 matrix.set_coefficient(0, 0, 0.7);
852 matrix.set_coefficient(0, 1, 0.3);
853
854 assert!((matrix.get_coefficient(0, 0) - 0.7).abs() < f64::EPSILON);
855 assert!((matrix.get_coefficient(0, 1) - 0.3).abs() < f64::EPSILON);
856 }
857
858 #[test]
859 fn test_stereo_to_surround51() {
860 let matrix = MixMatrix::stereo_to_surround51();
861 assert_eq!(matrix.input_channels(), 2);
862 assert_eq!(matrix.output_channels(), 6);
863
864 assert_eq!(matrix.get_coefficient(0, 0), 1.0);
866 assert_eq!(matrix.get_coefficient(1, 1), 1.0);
867 }
868
869 #[test]
870 fn test_surround71_to_stereo() {
871 let matrix = MixMatrix::surround71_to_stereo();
872 assert_eq!(matrix.input_channels(), 8);
873 assert_eq!(matrix.output_channels(), 2);
874 }
875
876 #[test]
877 fn test_apply_empty_input() {
878 let matrix = MixMatrix::identity(2);
879 let output = matrix.apply(&[]);
880 assert_eq!(output.len(), 2);
881 assert!(output[0].is_empty());
882 }
883}