secs-engine 0.18.0

The SECS 2 correction design: measurement, levels and delays, BA, XO, RC, decay, signal chain, MIMO and scorecard.
Documentation
use secs_types::calibration::CalibrationSettings;

use crate::calibration::{ProjectView, engine_inputs};

use super::cases;

#[test]
fn engine_inputs_match_constructor_goldens() {
    for name in cases::names() {
        let case = cases::load(&name);
        let settings: CalibrationSettings = serde_json::from_value(case.settings.clone()).unwrap();
        let groups = cases::pairs(&case.project["groups"])
            .map(|(group, roles)| {
                (
                    group.to_owned(),
                    roles
                        .as_array()
                        .unwrap()
                        .iter()
                        .map(|role| role.as_str().unwrap().to_owned())
                        .collect(),
                )
            })
            .collect::<Vec<_>>();
        let project_xo = cases::pairs(&case.project["xo"])
            .map(|(group, value)| (group.to_owned(), secs_oracle::number(value)))
            .collect::<Vec<_>>();
        let target = case.config.target.clone();
        let resolved = engine_inputs(
            &ProjectView {
                groups: &groups,
                project_xo: &project_xo,
                target: &target,
                inverted: &[],
            },
            &case.measurements,
            &settings,
        )
        .unwrap();
        assert_config(&resolved.config, &case.config, &name);
        assert!(resolved.notes.is_empty(), "{name}");
    }
}

fn assert_config(actual: &crate::EngineConfig, expected: &crate::EngineConfig, case: &str) {
    assert_eq!(actual.mains, expected.mains, "{case} mains");
    assert_eq!(
        actual.inverted_mains, expected.inverted_mains,
        "{case} polarity"
    );
    assert_eq!(actual.subs, expected.subs, "{case} subs");
    assert_eq!(actual.groups, expected.groups, "{case} groups");
    assert_eq!(actual.target, expected.target, "{case} target");
    assert_eq!(actual.xo, expected.xo, "{case} xo");
    assert_eq!(actual.main_position, expected.main_position, "{case}");
    assert_eq!(actual.taps, expected.taps, "{case}");
    assert_eq!(actual.lift_ms, expected.lift_ms, "{case}");
    assert_eq!(actual.max_boost_db, expected.max_boost_db, "{case}");
    assert_eq!(actual.peq_bands, expected.peq_bands, "{case}");
    assert_eq!(actual.max_latency_ms, expected.max_latency_ms, "{case}");
    assert_eq!(actual.rc_range, expected.rc_range, "{case}");
    assert_eq!(actual.room_volume_m3, expected.room_volume_m3, "{case}");
    assert_eq!(actual.phase_frac, expected.phase_frac, "{case}");
    assert_eq!(actual.sub_manual, expected.sub_manual, "{case}");
    assert_eq!(
        actual.sub_main_delay_ms, expected.sub_main_delay_ms,
        "{case}"
    );
    assert_eq!(actual.bundle_layouts, expected.bundle_layouts, "{case}");
    assert_eq!(
        actual.infra_boost_max_db, expected.infra_boost_max_db,
        "{case}"
    );
    assert_eq!(actual.xo_peq_on, expected.xo_peq_on, "{case}");
    assert_eq!(actual.xo_peq_lo_hz, expected.xo_peq_lo_hz, "{case}");
    assert_eq!(actual.xo_peq_hi_hz, expected.xo_peq_hi_hz, "{case}");
}

#[test]
fn inverted_main_inputs_preserve_precision_and_match_flipped_measurements() {
    use crate::{Measurement, MeasurementSet, Samples};

    let groups = vec![("front".into(), vec!["L".into(), "R".into()])];
    let target = secs_types::target::TargetCurve::default();
    let measurements = MeasurementSet {
        positions: vec![
            (
                1,
                vec![
                    (
                        "L".into(),
                        Measurement {
                            samples: Samples::F32(vec![1.0, -0.25].into()),
                            sample_rate: 48_000,
                            t0: 0.0,
                        },
                    ),
                    (
                        "R".into(),
                        Measurement {
                            samples: Samples::F64(vec![0.5, -2.0].into()),
                            sample_rate: 48_000,
                            t0: 0.0,
                        },
                    ),
                ],
            ),
            (
                2,
                vec![(
                    "R".into(),
                    Measurement {
                        samples: Samples::F32(vec![0.75, -0.5].into()),
                        sample_rate: 48_000,
                        t0: 0.0,
                    },
                )],
            ),
        ],
    };
    let inverted = vec!["R".into()];
    let inputs = engine_inputs(
        &ProjectView {
            groups: &groups,
            project_xo: &[],
            target: &target,
            inverted: &inverted,
        },
        &measurements,
        &CalibrationSettings::default(),
    )
    .unwrap();
    assert_eq!(inputs.config.inverted_mains, ["R"]);
    assert!(matches!(
        &inputs.measurements.get(1, "L").unwrap().samples,
        Samples::F32(values) if values.as_ref() == [1.0, -0.25]
    ));
    assert!(matches!(
        &inputs.measurements.get(1, "R").unwrap().samples,
        Samples::F64(values) if values.as_ref() == [-0.5, 2.0]
    ));
    assert!(matches!(
        &inputs.measurements.get(2, "R").unwrap().samples,
        Samples::F32(values) if values.as_ref() == [-0.75, 0.5]
    ));
}

#[test]
fn inverted_main_designs_on_the_flipped_response() {
    use crate::{DesignRequest, Samples, Tuning, design};
    use secs_compute::Context;

    let case = cases::load("room");
    let settings: CalibrationSettings = serde_json::from_value(case.settings.clone()).unwrap();
    let inverted = vec!["C".into()];
    let configured = engine_inputs(
        &ProjectView {
            groups: &case.config.groups,
            project_xo: &case.config.xo,
            target: &case.config.target,
            inverted: &inverted,
        },
        &case.measurements,
        &settings,
    )
    .unwrap();
    let mut flipped = case.measurements.clone();
    for (_, roles) in &mut flipped.positions {
        if let Some((_, measurement)) = roles.iter_mut().find(|(role, _)| role == "C") {
            measurement.samples = match &measurement.samples {
                Samples::F32(values) => Samples::F32(
                    values
                        .iter()
                        .map(|value| -*value)
                        .collect::<Vec<_>>()
                        .into(),
                ),
                Samples::F64(values) => Samples::F64(
                    values
                        .iter()
                        .map(|value| -*value)
                        .collect::<Vec<_>>()
                        .into(),
                ),
            };
        }
    }
    let manual = engine_inputs(
        &ProjectView {
            groups: &case.config.groups,
            project_xo: &case.config.xo,
            target: &case.config.target,
            inverted: &[],
        },
        &flipped,
        &settings,
    )
    .unwrap();
    for ((configured_position, configured_roles), (manual_position, manual_roles)) in configured
        .measurements
        .positions
        .iter()
        .zip(&manual.measurements.positions)
    {
        assert_eq!(configured_position, manual_position);
        for ((configured_role, configured), (manual_role, manual)) in
            configured_roles.iter().zip(manual_roles)
        {
            assert_eq!(configured_role, manual_role);
            assert_eq!(configured.samples.len(), manual.samples.len());
            assert!(
                (0..configured.samples.len()).all(|index| configured.samples.at(index).to_bits()
                    == manual.samples.at(index).to_bits())
            );
        }
    }

    let context = Context::serial();
    let configured_design = design(
        &DesignRequest {
            measurements: &configured.measurements,
            config: &configured.config,
            tuning: &Tuning::default(),
            subs_only: false,
            adapter_notes: &[],
        },
        &context,
        &*crate::production_fitters(context.cancel()),
    )
    .unwrap();
    let manual_design = design(
        &DesignRequest {
            measurements: &manual.measurements,
            config: &manual.config,
            tuning: &Tuning::default(),
            subs_only: false,
            adapter_notes: &[],
        },
        &context,
        &*crate::production_fitters(context.cancel()),
    )
    .unwrap();
    let mut configured_chain = configured_design.chain;
    configured_chain
        .mains
        .iter_mut()
        .for_each(|main| main.polarity = None);
    assert_eq!(configured_chain, manual_design.chain);
    let without_polarity = |notes: &[crate::Note]| {
        notes
            .iter()
            .filter(|note| {
                !matches!(
                    note,
                    crate::Note::PolarityReversed { .. } | crate::Note::PolaritySplit { .. }
                )
            })
            .cloned()
            .collect::<Vec<_>>()
    };
    assert_eq!(
        without_polarity(&configured_design.notes),
        without_polarity(&manual_design.notes)
    );
    assert_eq!(configured_design.mains.len(), manual_design.mains.len());
    for (configured, manual) in configured_design.mains.iter().zip(&manual_design.mains) {
        assert_eq!(configured.role, manual.role);
        assert!(
            configured
                .after
                .iter()
                .zip(manual.after.iter())
                .all(|(left, right)| {
                    (left.re.to_bits(), left.im.to_bits())
                        == (right.re.to_bits(), right.im.to_bits())
                })
        );
    }
}

#[test]
fn calibration_replay_matches_assembled_design_bit_for_bit() {
    use crate::{
        CalibrationRequest, DesignRequest, Tuning, calibrate_with, design, normalize_measurements,
    };
    use secs_compute::Context;

    let mut case = cases::load("dolby_linear");
    // Resolution must retain its adapter note, but not the biamp measurement.
    let measurement = case.measurements.positions[0].1[0].1.clone();
    case.measurements.positions[0]
        .1
        .push(("L:LF".into(), measurement));
    let settings: CalibrationSettings = serde_json::from_value(case.settings).unwrap();
    let view = ProjectView {
        groups: &case.config.groups,
        project_xo: &case.config.xo,
        target: &case.config.target,
        inverted: &[],
    };
    let ctx = Context::serial();
    let inputs = engine_inputs(&view, &case.measurements, &settings).unwrap();
    let measurements =
        normalize_measurements(&inputs.measurements, settings.sample_rate_policy, &ctx).unwrap();
    let replay = cases::Replay::for_case("dolby_linear");
    let expected = design(
        &DesignRequest {
            measurements: &measurements,
            config: &inputs.config,
            tuning: &Tuning::default(),
            subs_only: false,
            adapter_notes: &inputs.notes,
        },
        &ctx,
        &*replay.factory(),
    )
    .unwrap();
    replay.finish();
    let replay = cases::Replay::for_case("dolby_linear");
    let actual = calibrate_with(
        &CalibrationRequest {
            view: &view,
            irs: &case.measurements,
            settings: &settings,
            subs_only: false,
        },
        &ctx,
        &*replay.factory(),
    )
    .unwrap();
    replay.finish();
    assert_eq!(actual.resampled, 0);
    assert_eq!(actual.config.mains, inputs.config.mains);
    assert_eq!(
        actual.measurements.positions.len(),
        measurements.positions.len()
    );
    let actual = actual.design;
    assert_eq!(actual.chain, expected.chain);
    assert_eq!(actual.notes, expected.notes);
    assert_eq!(actual.adapter_notes, ["바이앰프 대역 측정 제외: L:LF"]);
    assert_eq!(actual.adapter_notes, expected.adapter_notes);
    assert_eq!(actual.ref_level.to_bits(), expected.ref_level.to_bits());
    assert_eq!(actual.rc_hi.to_bits(), expected.rc_hi.to_bits());
    assert_eq!(actual.mains.len(), expected.mains.len());
    for (a, b) in actual.mains.iter().zip(&expected.mains) {
        assert_eq!(a.role, b.role);
        for (a, b) in [
            (&a.before, &b.before),
            (&a.combined, &b.combined),
            (&a.after, &b.after),
        ] {
            assert_eq!(a.len(), b.len());
            assert!(
                a.iter()
                    .zip(b.iter())
                    .all(|(a, b)| (a.re.to_bits(), a.im.to_bits())
                        == (b.re.to_bits(), b.im.to_bits()))
            );
        }
    }
    assert_eq!(actual.scores.len(), expected.scores.len());
    for (a, b) in actual.scores.iter().zip(&expected.scores) {
        assert_eq!(a.role, b.role);
        assert_eq!(a.before, b.before);
        assert_eq!(a.after, b.after);
        for (a, b) in [
            (&a.spl_before, &b.spl_before),
            (&a.spl_after, &b.spl_after),
            (&a.gd_before, &b.gd_before),
            (&a.gd_after, &b.gd_after),
        ] {
            assert_eq!(a.len(), b.len());
            assert!(a.iter().zip(b).all(|(a, b)| a.to_bits() == b.to_bits()));
        }
    }
}