Skip to main content

secs_engine/
normalize.rs

1//! Calculation-only sample-rate normalization. Stored measurements stay intact.
2
3use secs_compute::{Context, MemoryError};
4use secs_dsp::resample::{IrResamplePlan, MAX_RESAMPLE_RATE, MIN_RESAMPLE_RATE};
5use secs_types::calibration::SampleRatePolicy;
6
7use crate::{EngineError, MeasurementSet, Samples, grid::SR};
8
9fn check(ctx: &Context) -> Result<(), EngineError> {
10    ctx.cancel().check().map_err(|_| EngineError::Cancelled)
11}
12
13/// Convert non-48 kHz IRs only when explicitly selected.
14///
15/// Same-rate and reject paths keep the original sample Arcs and t0 bits. The
16/// transient reservation covers all new sample buffers, the largest Vec-to-Arc
17/// transfer and the largest kernel. It ends on return; the caller retains the
18/// result under the same input-memory rules as other engine measurements.
19pub fn normalize_measurements(
20    irs: &MeasurementSet,
21    policy: SampleRatePolicy,
22    ctx: &Context,
23) -> Result<MeasurementSet, EngineError> {
24    check(ctx)?;
25    if policy == SampleRatePolicy::Reject {
26        return Ok(irs.clone());
27    }
28    let mut output_bytes = 0_u64;
29    let mut largest_output = 0_u64;
30    let mut largest_kernel = 0_u64;
31    let mut count = 0_u64;
32    for (position, roles) in &irs.positions {
33        for (role, measurement) in roles {
34            check(ctx)?;
35            if measurement.sample_rate == SR {
36                continue;
37            }
38            if !(MIN_RESAMPLE_RATE..=MAX_RESAMPLE_RATE).contains(&measurement.sample_rate) {
39                return Err(EngineError::UnsupportedResamplingRate {
40                    position: *position,
41                    role: role.clone(),
42                    actual_hz: measurement.sample_rate,
43                });
44            }
45            if measurement.samples.is_empty() || !measurement.t0.is_finite() {
46                return Err(EngineError::InvalidMeasurement {
47                    position: *position,
48                    role: role.clone(),
49                });
50            }
51            for n in 0..measurement.samples.len() {
52                if n % 4096 == 0 {
53                    check(ctx)?;
54                }
55                if !measurement.samples.at(n).is_finite() {
56                    return Err(EngineError::InvalidMeasurement {
57                        position: *position,
58                        role: role.clone(),
59                    });
60                }
61            }
62            let plan = IrResamplePlan::new(measurement.samples.len(), measurement.sample_rate, SR)?;
63            let bytes = (plan.output_len() * size_of::<f64>()) as u64;
64            output_bytes = output_bytes.checked_add(bytes).ok_or_else(|| {
65                EngineError::Input("IR resampling memory estimate overflow".into())
66            })?;
67            largest_output = largest_output.max(bytes);
68            largest_kernel = largest_kernel.max(plan.kernel_bytes() as u64);
69            count += 1;
70        }
71    }
72    if output_bytes == 0 {
73        return Ok(irs.clone());
74    }
75    let bytes = output_bytes
76        .checked_add(largest_output)
77        .and_then(|n| n.checked_add(largest_kernel))
78        .ok_or_else(|| EngineError::Input("IR resampling memory estimate overflow".into()))?;
79    let _permit = ctx
80        .budget()
81        .acquire(bytes, ctx.cancel())
82        .map_err(|error| match error {
83            MemoryError::Cancelled => EngineError::Cancelled,
84            other => EngineError::Input(other.to_string()),
85        })?;
86    let progress = ctx.stage("engine.resample", count);
87    let mut normalized = irs.clone();
88    for (_, roles) in &mut normalized.positions {
89        for (_, measurement) in roles {
90            check(ctx)?;
91            if measurement.sample_rate == SR {
92                continue;
93            }
94            let plan = IrResamplePlan::new(measurement.samples.len(), measurement.sample_rate, SR)?;
95            let samples = plan.process(|n| measurement.samples.at(n), || check(ctx))?;
96            measurement.t0 -= plan.leading_output_samples() as f64 / f64::from(SR);
97            measurement.sample_rate = SR;
98            measurement.samples = Samples::F64(samples.into());
99            progress.advance(1);
100        }
101    }
102    Ok(normalized)
103}