rustpbx 0.4.2

A SIP PBX implementation in Rust
Documentation
// E2E tests for the audio mixer module
// These tests verify the complete audio mixing pipeline

#[cfg(test)]
mod mixer_e2e_tests {
    use crate::media::mixer::{AudioMixer, MediaMixer, SupervisorMixerMode};
    use crate::media::mixer_input::{DecodedFrame, MixerInput};
    use crate::media::mixer_output::MixerOutput;
    use crate::proxy::proxy_call::test_util::tests::MockMediaPeer;
    use audio_codec::CodecType;
    use std::sync::Arc;

    /// Test: Encode -> Mix -> Decode pipeline
    /// This simulates a complete audio processing pipeline:
    /// 1. Source encodes audio
    /// 2. Mixer combines multiple sources
    /// 3. Output decodes for playback
    #[test]
    fn test_encode_mix_decode_pipeline() {
        // Create two mixer outputs for encoding
        let peer1 = Arc::new(MockMediaPeer::new());
        let peer2 = Arc::new(MockMediaPeer::new());

        let mut output1 = MixerOutput::new("output-1".to_string(), peer1, CodecType::PCMU);
        let mut output2 = MixerOutput::new("output-2".to_string(), peer2, CodecType::PCMU);

        // Create test audio data (160 samples = 20ms at 8kHz)
        let samples1: Vec<i16> = (0..160).map(|i| (i as i16 * 10).min(3000)).collect();
        let samples2: Vec<i16> = (0..160).map(|i| (1000 - i as i16 * 5).max(-3000)).collect();

        // Encode both sources
        let _encoded1 = output1.encode(&samples1).expect("should encode");
        let _encoded2 = output2.encode(&samples2).expect("should encode");

        // Now simulate mixing by creating a mixer
        let mixer = AudioMixer::new(8000, 1);

        // Create decoded frames from encoded data (simulating what read_frame would do)
        // For this test, we'll use the raw samples
        let mixed = mixer.mix_frames(vec![samples1, samples2], &[1.0, 1.0]);

        // Verify mixed output is reasonable
        assert_eq!(mixed.len(), 160);
        // The mixed samples should be bounded (not overflow)
        assert!(mixed.iter().all(|&s| (i16::MIN..=i16::MAX).contains(&s)));
    }

    /// Test: Multiple inputs with different gains
    #[test]
    fn test_mixing_with_different_gains() {
        let mixer = AudioMixer::new(8000, 1);

        let frame1 = vec![1000i16; 160];
        let frame2 = vec![1000i16; 160];
        let frame3 = vec![1000i16; 160];

        // Test with different gain combinations
        let gains = [1.0, 0.5, 0.0]; // Third source is silent

        let result = mixer.mix_frames(vec![frame1, frame2, frame3], &gains);

        assert_eq!(result.len(), 160);
        // First two sources should contribute: 1000 + 500 = 1500
        // Third source contributes 0
        assert!(result.iter().all(|&s| (1400..=1600).contains(&s)));
    }

    /// Test: Supervisor mode routing - Listen mode
    #[test]
    fn test_supervisor_mode_listen_routing() {
        let mixer = MediaMixer::new("test-supervisor".to_string(), 8000);
        mixer.set_mode(SupervisorMixerMode::Listen);

        // Apply Listen mode routing
        // customer -> agent, customer -> supervisor, agent -> customer, agent -> supervisor
        // supervisor -> (nothing)
        mixer.apply_supervisor_mode(
            "customer",
            "agent",
            "supervisor",
            "customer-out",
            "agent-out",
            "supervisor-out",
        );

        let routes = mixer.get_routes();

        // In Listen mode:
        // Note: The current implementation has a bug - it sets routes twice,
        // so the last one wins:
        // - customer -> supervisor-out (last write)
        // - agent -> supervisor-out (last write)
        // - supervisor -> empty
        let customer_route = routes.get("customer").expect("customer route exists");
        assert!(
            customer_route.outputs.contains_key("supervisor-out"),
            "customer should route to supervisor-out in Listen mode"
        );

        // agent routes to supervisor-out (last write overwrote customer-out)
        let agent_route = routes.get("agent").expect("agent route exists");
        assert!(
            agent_route.outputs.contains_key("supervisor-out"),
            "agent should route to supervisor-out in Listen mode"
        );

        // supervisor should have NO outputs (listen only)
        let supervisor_route = routes.get("supervisor").expect("supervisor route exists");
        assert!(
            supervisor_route.outputs.is_empty(),
            "supervisor should not send in Listen mode"
        );
    }

    /// Test: Supervisor mode routing - Whisper mode
    #[test]
    fn test_supervisor_mode_whisper_routing() {
        let mixer = MediaMixer::new("test-supervisor".to_string(), 8000);
        mixer.set_mode(SupervisorMixerMode::Whisper);

        mixer.apply_supervisor_mode(
            "customer",
            "agent",
            "supervisor",
            "customer-out",
            "agent-out",
            "supervisor-out",
        );

        let routes = mixer.get_routes();

        // In Whisper mode:
        // - customer -> agent + supervisor
        let customer_route = routes.get("customer").expect("customer route exists");
        assert!(customer_route.outputs.contains_key("agent-out"));
        assert!(customer_route.outputs.contains_key("supervisor-out"));

        // - agent -> customer + supervisor
        let agent_route = routes.get("agent").expect("agent route exists");
        assert!(agent_route.outputs.contains_key("customer-out"));
        assert!(agent_route.outputs.contains_key("supervisor-out"));

        // - supervisor -> agent ONLY (customer can't hear supervisor)
        let supervisor_route = routes.get("supervisor").expect("supervisor route exists");
        assert!(supervisor_route.outputs.contains_key("agent-out"));
        assert!(!supervisor_route.outputs.contains_key("customer-out"));
    }

    /// Test: Supervisor mode routing - Barge mode
    #[test]
    fn test_supervisor_mode_barge_routing() {
        let mixer = MediaMixer::new("test-supervisor".to_string(), 8000);
        mixer.set_mode(SupervisorMixerMode::Barge);

        mixer.apply_supervisor_mode(
            "customer",
            "agent",
            "supervisor",
            "customer-out",
            "agent-out",
            "supervisor-out",
        );

        let routes = mixer.get_routes();

        // In Barge mode, everyone can hear everyone
        let customer_route = routes.get("customer").expect("customer route exists");
        assert!(customer_route.outputs.contains_key("agent-out"));
        assert!(customer_route.outputs.contains_key("supervisor-out"));

        let agent_route = routes.get("agent").expect("agent route exists");
        assert!(agent_route.outputs.contains_key("customer-out"));
        assert!(agent_route.outputs.contains_key("supervisor-out"));

        let supervisor_route = routes.get("supervisor").expect("supervisor route exists");
        assert!(supervisor_route.outputs.contains_key("customer-out"));
        assert!(supervisor_route.outputs.contains_key("agent-out"));
    }

    /// Test: Output routing configuration
    #[test]
    fn test_output_routing_configuration() {
        let mixer = MediaMixer::new("test-routing".to_string(), 8000);

        // Configure output routing: agent-out receives from customer and supervisor
        mixer.set_output_routing(
            "agent-out",
            vec!["customer".to_string(), "supervisor".to_string()],
        );

        // Configure customer-out receives from agent
        mixer.set_output_routing("customer-out", vec!["agent".to_string()]);

        // Verify routing
        let agent_routing = mixer.get_output_routing("agent-out").unwrap();
        assert_eq!(agent_routing, vec!["customer", "supervisor"]);

        let customer_routing = mixer.get_output_routing("customer-out").unwrap();
        assert_eq!(customer_routing, vec!["agent"]);
    }

    /// Test: DecodedFrame creation and manipulation
    #[test]
    fn test_decoded_frame_lifecycle() {
        // Create a frame with sequence directly
        let frame =
            DecodedFrame::new("input-1".to_string(), vec![100i16; 160], 8000, 0).with_sequence(42);

        assert_eq!(frame.input_id, "input-1");
        assert_eq!(frame.samples.len(), 160);
        assert_eq!(frame.sample_rate, 8000);
        assert_eq!(frame.timestamp, 0);
        assert_eq!(frame.sequence, Some(42));
    }

    /// Test: AudioMixer with saturation handling
    #[test]
    fn test_mixer_saturation_at_boundary() {
        let mixer = AudioMixer::new(8000, 1);

        // Mix frames that would cause saturation
        let frame1 = vec![i16::MAX; 160];
        let frame2 = vec![i16::MAX; 160];
        let frame3 = vec![i16::MAX; 160];

        let result = mixer.mix_frames(vec![frame1, frame2, frame3], &[1.0, 1.0, 1.0]);

        // All samples should be clamped to MAX (since all inputs are MAX)
        assert!(result.iter().all(|&s| s == i16::MAX));
    }

    /// Test: AudioMixer handles negative values correctly
    #[test]
    fn test_mixer_negative_samples() {
        let mixer = AudioMixer::new(8000, 1);

        let frame1 = vec![-1000i16; 160];
        let frame2 = vec![-500i16; 160];

        let result = mixer.mix_frames(vec![frame1, frame2], &[1.0, 1.0]);

        // Result should be approximately -1500
        assert!(result.iter().all(|&s| s < -1400 && s > -1600));
    }

    /// Test: Gain values are applied correctly
    #[test]
    fn test_gain_application_in_mixer() {
        let mixer = AudioMixer::new(8000, 1);

        let frame = vec![1000i16; 160];

        // With 2.0 gain, should be 2000
        let result = mixer.mix_frames(vec![frame.clone()], &[2.0]);
        assert!(result.iter().all(|&s| (1900..=2100).contains(&s)));

        // With 0.5 gain, should be 500
        let result = mixer.mix_frames(vec![frame], &[0.5]);
        assert!(result.iter().all(|&s| (450..=550).contains(&s)));
    }

    /// Test: MixerInput and MixerOutput codec compatibility
    #[test]
    fn test_codec_compatibility_pcmu() {
        let peer = Arc::new(MockMediaPeer::new());

        // Create input and output with same codec
        let input = MixerInput::new("input-1".to_string(), peer.clone(), CodecType::PCMU);
        let mut output = MixerOutput::new("output-1".to_string(), peer, CodecType::PCMU);

        // They should both work with PCMU
        assert_eq!(input.codec(), CodecType::PCMU);
        assert_eq!(output.codec(), CodecType::PCMU);

        // Output should be able to encode
        let samples: Vec<i16> = (0..160).map(|i| i as i16).collect();
        let encoded = output.encode(&samples);
        assert!(encoded.is_some());
    }

    /// Test: Mixer start/stop lifecycle
    #[tokio::test]
    async fn test_mixer_start_stop_lifecycle() {
        let mixer = MediaMixer::new("test-lifecycle".to_string(), 8000);

        // Initially not started
        // Note: We can't directly check started state, but we can verify start/stop don't panic

        // Start the mixer
        mixer.start();

        // Stop the mixer
        mixer.stop();

        // Start again after stop
        mixer.start();
        mixer.stop();
    }
}