ChannelTimingCalibration.java

package org.hammer.audio.acquisition;

/**
 * Affine timing and level calibration for one microphone channel.
 *
 * @param channel zero-based channel index
 * @param referenceFrame frame at which {@code offsetSamples} was measured
 * @param offsetSamples observed channel delay relative to the profile reference channel
 * @param driftPpm relative sample-rate drift in parts per million
 * @param residualRmsSamples RMS calibration-fit residual in samples
 * @param jitterRmsSamples RMS short-term timing jitter in samples
 * @param gainLinear multiplicative level correction
 * @param invertedPolarity whether samples must be polarity-inverted
 */
public record ChannelTimingCalibration(
    int channel,
    long referenceFrame,
    double offsetSamples,
    double driftPpm,
    double residualRmsSamples,
    double jitterRmsSamples,
    double gainLinear,
    boolean invertedPolarity) {

  // Validate one channel calibration.
  public ChannelTimingCalibration {
    if (channel < 0) {
      throw new IllegalArgumentException("channel must be >= 0");
    }
    if (referenceFrame < 0) {
      throw new IllegalArgumentException("referenceFrame must be >= 0");
    }
    requireFinite(offsetSamples, "offsetSamples");
    requireFinite(driftPpm, "driftPpm");
    requireNonNegativeFinite(residualRmsSamples, "residualRmsSamples");
    requireNonNegativeFinite(jitterRmsSamples, "jitterRmsSamples");
    requirePositiveFinite(gainLinear, "gainLinear");
  }

  /** Returns the predicted channel delay at an absolute nominal frame index. */
  public double offsetAtFrame(long frameIndex) {
    if (frameIndex < 0) {
      throw new IllegalArgumentException("frameIndex must be >= 0");
    }
    return offsetSamples + (frameIndex - referenceFrame) * driftPpm * 1.0e-6;
  }

  /** Conservative one-sigma timing uncertainty in samples. */
  public double timingUncertaintySamples() {
    return Math.hypot(residualRmsSamples, jitterRmsSamples);
  }

  private static void requireFinite(double value, String name) {
    if (Double.isFinite(value)) {
      return;
    }
    throw new IllegalArgumentException(name + " must be finite");
  }

  private static void requireNonNegativeFinite(double value, String name) {
    if (Double.isFinite(value) && value >= 0.0) {
      return;
    }
    throw new IllegalArgumentException(name + " must be finite and >= 0");
  }

  private static void requirePositiveFinite(double value, String name) {
    if (Double.isFinite(value) && value > 0.0) {
      return;
    }
    throw new IllegalArgumentException(name + " must be finite and > 0");
  }
}