secs-engine 0.3.0

The SECS 2 correction design: measurement, levels and delays, BA, XO, RC, decay, signal chain, MIMO and scorecard.
Documentation
use std::sync::Arc;

use crate::{C, Samples, Spectra, measurement, setup::Setup, test_support};

#[test]
fn design_replay_xo_preview_matches_fresh_preparation_bits() {
    for name in ["dolby", "room_filters"] {
        let case = test_support::replay_case(name);
        let prepared =
            measurement::prepare(&case.measurements, &case.config, &Setup::new(&case.config))
                .unwrap();
        let fitters = test_support::Replay::for_case(name);
        let fresh = crate::design::design_prepared(
            &crate::DesignRequest {
                measurements: &case.measurements,
                config: &case.config,
                tuning: &test_support::load(name).tuning,
                subs_only: false,
                adapter_notes: &[],
            },
            &secs_compute::Context::serial(),
            &*fitters.factory(),
            &prepared,
            &|_, _| {},
        )
        .unwrap();
        fitters.finish();
        let replay_preview = crate::preview::XoPreview::new(&case.design);
        let fresh_preview = crate::preview::XoPreview::new(&fresh);
        for (group, roles) in case.config.groups.iter().filter(|(g, _)| g != "sub") {
            for corrected in [false, true] {
                for role in std::iter::once(None).chain(roles.iter().map(|r| Some(r.as_str()))) {
                    let actual = replay_preview.curves(&case.design, group, 80.0, corrected, role);
                    let expected = fresh_preview.curves(&fresh, group, 80.0, corrected, role);
                    for (a, b) in [
                        (&actual.grid, &expected.grid),
                        (&actual.main, &expected.main),
                        (&actual.sub, &expected.sub),
                        (&actual.sum, &expected.sum),
                        (&actual.target, &expected.target),
                    ] {
                        assert_eq!(
                            a.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
                            b.iter().map(|v| v.to_bits()).collect::<Vec<_>>()
                        );
                    }
                }
            }
        }
    }
}

#[test]
fn design_replay_mimo_responses_match_fresh_preparation_bits() {
    use crate::mimo::run::{self, Support};
    for name in ["room_dba", "room_filters"] {
        let case = test_support::replay_case(name);
        let ctx = secs_compute::Context::serial();
        let prepared = crate::mimo::prepared::Prepared::new(
            &case.design,
            &case.measurements,
            &case.config,
            &ctx,
        )
        .unwrap();
        for support in [
            Support::Iir(crate::IirSupport {
                gain_db: -20.0,
                delay_ms: 3.0,
                polarity: -1,
                peq: vec![(70.0, 2.0, -3.0)],
                allpass: vec![(90.0, 0.7)],
            }),
            Support::Fir {
                taps: vec![0.1, -0.05, 0.02],
                fs_low: 600.0,
            },
        ] {
            let key = crate::MimoKey {
                fsiso: 150.0,
                f_low: None,
                exclude: vec![],
                max_supports: 0,
                fir: matches!(support, Support::Fir { .. }),
            };
            let supports = vec![(
                case.config.mains[0].clone(),
                vec![(case.config.subs[0].clone(), support)],
            )];
            let ideal = crate::MimoDesign::default();
            let expected = run::assemble(
                &case.design,
                &case.measurements,
                &case.config,
                &ideal,
                supports.clone(),
                key.clone(),
                &ctx,
            )
            .unwrap();
            let actual =
                run::assemble_prepared(&case.design, &prepared, &ideal, supports, key, &ctx)
                    .unwrap();
            assert_eq!(actual.paths, expected.paths);
            for (a, b) in [
                (&actual.final_resp, &expected.final_resp),
                (&actual.applied, &expected.applied),
            ] {
                assert_eq!(a.len(), b.len());
                for ((role, a), (other, b)) in a.iter().zip(b) {
                    assert_eq!(role, other);
                    spectrum_bits(a, b);
                }
            }
        }
    }
}

#[test]
fn design_replay_export_ir_matches_fresh_preparation_bits() {
    for name in ["dolby", "room_filters"] {
        let case = test_support::replay_case(name);
        let design = &case.design;
        let fresh =
            measurement::prepare(&case.measurements, &case.config, &Setup::new(&case.config))
                .unwrap();
        let expected =
            crate::export_view::export_view_prepared(design, &case.measurements, &fresh, &|| false)
                .unwrap();
        let actual = design.replay.export_view(design).unwrap();
        for &position in expected.spectra.positions() {
            for role in expected.spectra.roles_at(position) {
                let mut spectra = vec![(
                    actual.spectra.get(position, role).unwrap().to_vec(),
                    expected.spectra.get(position, role).unwrap().to_vec(),
                )];
                if let Some(a) = crate::stage_responses(design, role, &actual.spectra, position) {
                    let b =
                        crate::stage_responses(design, role, &expected.spectra, position).unwrap();
                    spectra.extend([(a.top_xo, b.top_xo), (a.output, b.output)]);
                    if let (Some(a), Some(b)) = (a.sub_xo, b.sub_xo) {
                        spectra.push((a, b));
                    }
                }
                for (actual, expected) in spectra {
                    let expected: Vec<_> = secs_dsp::fft::irfft(&expected, crate::grid::N_FFT)
                        .iter()
                        .map(|v| f64::from(*v as f32).to_bits())
                        .collect();
                    for axis in [crate::ExportAxis::Measurement, crate::ExportAxis::Corrected] {
                        let (samples, t0) = design.replay.export_ir(design, &actual, axis);
                        assert_eq!(
                            samples.iter().map(|v| v.to_bits()).collect::<Vec<_>>(),
                            expected
                        );
                        let expected_t0 = match axis {
                            crate::ExportAxis::Measurement => fresh.base_s,
                            crate::ExportAxis::Corrected => {
                                fresh.base_s - design.chain.latency_ms / 1000.0
                            }
                        };
                        assert_eq!(t0.to_bits(), expected_t0.to_bits());
                    }
                }
            }
        }
    }
}

fn spectrum_bits(actual: &[C], expected: &[C]) {
    assert_eq!(actual.len(), expected.len());
    for (actual, expected) in actual.iter().zip(expected) {
        assert_eq!(
            (actual.re.to_bits(), actual.im.to_bits()),
            (expected.re.to_bits(), expected.im.to_bits())
        );
    }
}

fn spectra_bits(actual: &Spectra, expected: &Spectra) {
    assert_eq!(actual.positions(), expected.positions());
    for &position in expected.positions() {
        assert_eq!(
            actual.roles_at(position).collect::<Vec<_>>(),
            expected.roles_at(position).collect::<Vec<_>>()
        );
        for role in expected.roles_at(position) {
            spectrum_bits(
                actual.get(position, role).unwrap(),
                expected.get(position, role).unwrap(),
            );
        }
    }
}

#[test]
fn design_replay_export_matches_fresh_preparation_bits() {
    for name in ["dolby", "room_filters"] {
        let case = test_support::replay_case(name);
        let design = &case.design;
        let replay = &design.replay;
        let fresh =
            measurement::prepare(&case.measurements, &case.config, &Setup::new(&case.config))
                .unwrap();
        assert_eq!(replay.base_s().to_bits(), fresh.base_s.to_bits());
        assert_eq!(replay.config().main_position, case.config.main_position);
        for (position, roles) in &case.measurements.positions {
            for (role, measurement) in roles {
                let retained = replay.measurements().get(*position, role).unwrap();
                assert_eq!(retained.sample_rate, measurement.sample_rate);
                assert_eq!(retained.t0.to_bits(), measurement.t0.to_bits());
                match (&retained.samples, &measurement.samples) {
                    (Samples::F32(a), Samples::F32(b)) => assert!(Arc::ptr_eq(a, b)),
                    (Samples::F64(a), Samples::F64(b)) => assert!(Arc::ptr_eq(a, b)),
                    _ => panic!("replay changed sample storage"),
                }
            }
        }
        let expected =
            crate::export_view::export_view_prepared(design, &case.measurements, &fresh, &|| false)
                .unwrap();
        let builds = measurement::SPECTRA_BUILT.with(|count| count.get());
        let actual = replay.export_view(design).unwrap();
        assert_eq!(
            measurement::SPECTRA_BUILT.with(|count| count.get()) - builds,
            1,
            "export builds only the long window, not another preparation"
        );
        assert_eq!(actual.base_s.to_bits(), expected.base_s.to_bits());
        assert_eq!(actual.has_sub_filters, expected.has_sub_filters);
        spectra_bits(&actual.spectra, &expected.spectra);
        let ctx = secs_compute::Context::serial();
        let builds = measurement::SPECTRA_BUILT.with(|count| count.get());
        let mimo =
            crate::mimo::prepared::Prepared::new(design, &case.measurements, &case.config, &ctx)
                .unwrap();
        assert_eq!(
            measurement::SPECTRA_BUILT.with(|count| count.get()) - builds,
            1,
            "MIMO builds only the long window, not another preparation"
        );
        spectra_bits(&mimo.spectra, &expected.spectra);
        assert_eq!(mimo.align_delay_ms.len(), fresh.align_delay_ms.len());
        for ((role, actual), (expected_role, expected)) in
            mimo.align_delay_ms.iter().zip(&fresh.align_delay_ms)
        {
            assert_eq!(role, expected_role);
            assert_eq!(actual.to_bits(), expected.to_bits());
        }
        eprintln!(
            "REPLAY_MEMORY {name}: metadata reservation={} bytes, sample copies=0 bytes",
            crate::DesignReplay::bytes(&case.measurements, &case.config, &Setup::new(&case.config))
        );
    }
}