CalibratedTdoaEstimator.java
package org.hammer.audio.experimental.acoustic;
import java.time.Clock;
import java.util.Objects;
import org.hammer.audio.acquisition.Microphone;
import org.hammer.audio.acquisition.MicrophoneArray;
import org.hammer.audio.acquisition.MicrophoneArrayCalibration;
import org.hammer.audio.acquisition.SynchronizationAssessment;
import org.hammer.audio.core.AudioBlock;
/** Applies validated per-channel offset/drift calibration to another TDOA estimator. */
public final class CalibratedTdoaEstimator implements SynchronizationAwareTdoaEstimator {
private final TdoaEstimator delegate;
private final MicrophoneArrayCalibration calibration;
private final Clock clock;
private final double maximumErrorSamples;
private final double speedOfSoundMetersPerSecond;
/** Creates a calibrated decorator with an explicit timing-error budget. */
public CalibratedTdoaEstimator(
TdoaEstimator delegate,
MicrophoneArrayCalibration calibration,
Clock clock,
double maximumErrorSamples,
double speedOfSoundMetersPerSecond) {
this.delegate = Objects.requireNonNull(delegate, "delegate");
this.calibration = Objects.requireNonNull(calibration, "calibration");
this.clock = Objects.requireNonNull(clock, "clock");
this.maximumErrorSamples = requirePositiveFinite(maximumErrorSamples, "maximumErrorSamples");
this.speedOfSoundMetersPerSecond =
requirePositiveFinite(speedOfSoundMetersPerSecond, "speedOfSoundMetersPerSecond");
}
@Override
public TdoaEstimate estimate(
AudioBlock block, MicrophoneArray array, int firstChannel, int secondChannel) {
requireCompatibleArray(array);
SynchronizationAssessment assessment = synchronizationAssessment(block, array);
if (!assessment.usable()) {
throw new UnusableSynchronizationException(String.join(" ", assessment.diagnostics()));
}
TdoaEstimate raw = delegate.estimate(block, array, firstChannel, secondChannel);
double hardwareDelaySamples =
calibration.relativeOffsetSamples(firstChannel, secondChannel, block.frameIndex());
double rawDelaySamples = raw.delaySeconds() * block.format().sampleRate();
double correctedDelaySamples = rawDelaySamples - hardwareDelaySamples;
int roundedDelaySamples = (int) Math.round(correctedDelaySamples);
double correctedDelaySeconds = correctedDelaySamples / block.format().sampleRate();
double confidenceScale =
Math.max(0.0, 1.0 - assessment.estimatedErrorSamples() / maximumErrorSamples);
return new TdoaEstimate(
raw.firstMicrophoneId(),
raw.secondMicrophoneId(),
roundedDelaySamples,
correctedDelaySeconds,
correctedDelaySeconds * speedOfSoundMetersPerSecond,
raw.confidence() * confidenceScale);
}
@Override
public SynchronizationAssessment synchronizationAssessment(
AudioBlock block, MicrophoneArray array) {
Objects.requireNonNull(block, "block");
requireCompatibleArray(array);
return calibration.assess(clock.instant(), block.format().sampleRate(), maximumErrorSamples);
}
private void requireCompatibleArray(MicrophoneArray array) {
Objects.requireNonNull(array, "array");
if (array.channels() != calibration.array().channels()) {
throw new IllegalArgumentException("microphone array does not match calibration profile");
}
for (int channel = 0; channel < array.channels(); channel++) {
Microphone actual = array.microphone(channel);
Microphone calibrated = calibration.array().microphone(channel);
if (!actual.equals(calibrated)) {
throw new IllegalArgumentException("microphone array does not match calibration profile");
}
}
}
private static double requirePositiveFinite(double value, String name) {
if (Double.isFinite(value) && value > 0.0) {
return value;
}
throw new IllegalArgumentException(name + " must be finite and > 0");
}
}