Skip to main content

oximedia_graph/filters/audio/
mixer.rs

1//! Audio channel mixing filter.
2//!
3//! This module provides channel mixing and routing capabilities for audio streams,
4//! supporting common presets and custom mixing matrices.
5
6#![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
25/// Maximum number of channels supported.
26pub const MAX_CHANNELS: usize = 16;
27
28/// A mixing matrix that defines how input channels map to output channels.
29///
30/// Each row represents an output channel, and each column represents an input channel.
31/// The value at `[out][in]` is the gain applied to input channel `in` when contributing
32/// to output channel `out`.
33#[derive(Clone, Debug)]
34pub struct MixMatrix {
35    /// The mixing coefficients [output_channel][input_channel].
36    coefficients: Vec<Vec<f64>>,
37    /// Number of input channels.
38    input_channels: usize,
39    /// Number of output channels.
40    output_channels: usize,
41}
42
43impl MixMatrix {
44    /// Create a new mixing matrix with the specified dimensions.
45    #[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    /// Create an identity matrix (passthrough).
56    #[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    /// Create a mono to stereo matrix (duplicate mono to both channels).
66    #[must_use]
67    pub fn mono_to_stereo() -> Self {
68        let mut matrix = Self::new(1, 2);
69        matrix.coefficients[0][0] = 1.0; // Mono -> Left
70        matrix.coefficients[1][0] = 1.0; // Mono -> Right
71        matrix
72    }
73
74    /// Create a stereo to mono matrix (average L and R).
75    #[must_use]
76    pub fn stereo_to_mono() -> Self {
77        let mut matrix = Self::new(2, 1);
78        matrix.coefficients[0][0] = 0.5; // Left contribution
79        matrix.coefficients[0][1] = 0.5; // Right contribution
80        matrix
81    }
82
83    /// Create a 5.1 to stereo downmix matrix.
84    ///
85    /// Standard ITU-R BS.775 downmix coefficients:
86    /// - L' = L + 0.707*C + 0.707*Ls
87    /// - R' = R + 0.707*C + 0.707*Rs
88    #[must_use]
89    pub fn surround51_to_stereo() -> Self {
90        let mut matrix = Self::new(6, 2);
91        let center_gain = 0.707; // -3dB
92        let surround_gain = 0.707;
93
94        // Input order: L, R, C, LFE, Ls, Rs
95        // Left output
96        matrix.coefficients[0][0] = 1.0; // L -> L
97        matrix.coefficients[0][2] = center_gain; // C -> L
98        matrix.coefficients[0][4] = surround_gain; // Ls -> L
99
100        // Right output
101        matrix.coefficients[1][1] = 1.0; // R -> R
102        matrix.coefficients[1][2] = center_gain; // C -> R
103        matrix.coefficients[1][5] = surround_gain; // Rs -> R
104
105        // LFE is typically discarded in simple downmix
106
107        matrix
108    }
109
110    /// Create a 7.1 to stereo downmix matrix.
111    #[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        // Input order: L, R, C, LFE, Ls, Rs, Lb, Rb
119        // Left output
120        matrix.coefficients[0][0] = 1.0; // L -> L
121        matrix.coefficients[0][2] = center_gain; // C -> L
122        matrix.coefficients[0][4] = surround_gain; // Ls -> L
123        matrix.coefficients[0][6] = back_gain; // Lb -> L
124
125        // Right output
126        matrix.coefficients[1][1] = 1.0; // R -> R
127        matrix.coefficients[1][2] = center_gain; // C -> R
128        matrix.coefficients[1][5] = surround_gain; // Rs -> R
129        matrix.coefficients[1][7] = back_gain; // Rb -> R
130
131        matrix
132    }
133
134    /// Create a stereo to 5.1 upmix matrix.
135    ///
136    /// Basic upmix that places stereo content in front channels.
137    #[must_use]
138    pub fn stereo_to_surround51() -> Self {
139        let mut matrix = Self::new(2, 6);
140
141        // Output order: L, R, C, LFE, Ls, Rs
142        matrix.coefficients[0][0] = 1.0; // L -> L
143        matrix.coefficients[1][1] = 1.0; // R -> R
144        matrix.coefficients[2][0] = 0.5; // L -> C
145        matrix.coefficients[2][1] = 0.5; // R -> C
146                                         // LFE and surround channels are silent
147        matrix.coefficients[4][0] = 0.3; // L -> Ls (ambient)
148        matrix.coefficients[5][1] = 0.3; // R -> Rs (ambient)
149
150        matrix
151    }
152
153    /// Set a coefficient in the matrix.
154    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    /// Get a coefficient from the matrix.
161    #[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    /// Get the number of input channels.
171    #[must_use]
172    pub fn input_channels(&self) -> usize {
173        self.input_channels
174    }
175
176    /// Get the number of output channels.
177    #[must_use]
178    pub fn output_channels(&self) -> usize {
179        self.output_channels
180    }
181
182    /// Apply the mixing matrix to input samples.
183    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/// Configuration for crossfade transitions.
214#[derive(Clone, Debug)]
215pub struct CrossfadeConfig {
216    /// Duration of crossfade in samples.
217    pub duration_samples: usize,
218    /// Current position in crossfade (0 = start, duration = end).
219    pub position: usize,
220    /// Source matrix (fading from).
221    pub from_matrix: MixMatrix,
222    /// Target matrix (fading to).
223    pub to_matrix: MixMatrix,
224}
225
226impl CrossfadeConfig {
227    /// Create a new crossfade configuration.
228    #[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    /// Get the current interpolation factor (0.0 to 1.0).
239    #[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    /// Check if crossfade is complete.
248    #[must_use]
249    pub fn is_complete(&self) -> bool {
250        self.position >= self.duration_samples
251    }
252
253    /// Advance the crossfade position.
254    pub fn advance(&mut self, samples: usize) {
255        self.position = self.position.saturating_add(samples);
256    }
257}
258
259/// Configuration for the channel mix filter.
260#[derive(Clone, Debug)]
261pub struct ChannelMixConfig {
262    /// The mixing matrix.
263    pub matrix: MixMatrix,
264    /// Optional crossfade for smooth transitions.
265    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    /// Create a new configuration with the specified matrix.
279    #[must_use]
280    pub fn new(matrix: MixMatrix) -> Self {
281        Self {
282            matrix,
283            crossfade: None,
284        }
285    }
286
287    /// Create a mono to stereo configuration.
288    #[must_use]
289    pub fn mono_to_stereo() -> Self {
290        Self::new(MixMatrix::mono_to_stereo())
291    }
292
293    /// Create a stereo to mono configuration.
294    #[must_use]
295    pub fn stereo_to_mono() -> Self {
296        Self::new(MixMatrix::stereo_to_mono())
297    }
298
299    /// Create a 5.1 to stereo downmix configuration.
300    #[must_use]
301    pub fn surround51_to_stereo() -> Self {
302        Self::new(MixMatrix::surround51_to_stereo())
303    }
304
305    /// Set a crossfade transition.
306    #[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
317/// Audio channel mixing filter.
318///
319/// This filter remaps audio channels using a mixing matrix, supporting
320/// common operations like stereo to mono conversion and 5.1 downmixing.
321///
322/// # Example
323///
324/// ```ignore
325/// use oximedia_graph::filters::audio::mixer::{ChannelMixFilter, ChannelMixConfig};
326///
327/// // Create a stereo to mono downmix filter
328/// let config = ChannelMixConfig::stereo_to_mono();
329/// let filter = ChannelMixFilter::new(NodeId(0), "downmix", config);
330/// ```
331pub 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    /// Create a new channel mix filter.
342    #[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    /// Get the current configuration.
366    #[must_use]
367    pub fn config(&self) -> &ChannelMixConfig {
368        &self.config
369    }
370
371    /// Update the configuration.
372    pub fn set_config(&mut self, config: ChannelMixConfig) {
373        self.config = config;
374    }
375
376    /// Set a new target matrix with crossfade.
377    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    /// Convert audio frame to f64 samples per channel.
384    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    /// Convert interleaved samples.
407    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    /// Convert planar samples.
433    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    /// Convert bytes to f64 sample.
456    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    /// Convert f64 samples to audio frame.
497    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    /// Convert f64 sample to bytes.
529    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    /// Apply crossfade mixing if active.
558    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(); // Used for reference, local_factor is per-sample
568
569            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        // Convert to f64 samples
650        let input_samples = Self::frame_to_samples(&frame);
651
652        // Apply mixing (with crossfade if active)
653        let output_samples = self.apply_crossfade(&input_samples);
654
655        // Determine output channel layout
656        let output_layout = ChannelLayout::from_count(self.config.matrix.output_channels());
657
658        // Convert back to frame
659        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], // Left
712            vec![1.0, 0.0, -1.0], // Right
713        ];
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        // Check that main channels are preserved
730        assert_eq!(matrix.get_coefficient(0, 0), 1.0); // L -> L
731        assert_eq!(matrix.get_coefficient(1, 1), 1.0); // R -> R
732    }
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        // Create stereo samples: L=0.5, R=0.5
809        for _ in 0..100 {
810            samples.extend_from_slice(&0.5f32.to_le_bytes()); // L
811            samples.extend_from_slice(&0.5f32.to_le_bytes()); // R
812        }
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        // Front channels should have direct mapping
865        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}