WingbeatEmitter2D.java

package org.hammer.audio.experimental.acoustic.simulation;

import org.hammer.audio.experimental.acoustic.scenario.AcousticGroundTruth;
import org.hammer.audio.geometry.Vector2;

/**
 * Deterministic emitter that reuses {@link WingbeatSignalParameters} and {@link
 * WingbeatSignalGenerator} for synthetic wingbeat simulation.
 */
public final class WingbeatEmitter2D implements AcousticEmitter2D {

  private final Vector2 initialPositionMeters;
  private final Vector2 velocityVectorMetersPerSecond;
  private final double signalAmplitude;
  private final WingbeatSignalParameters signalParameters;
  private final double sampleRateHz;
  private final long randomSeed;
  private final WingbeatSignalGenerator.PhaseAccumulatorCache unitScalePhaseCache;

  /** Create a wingbeat-backed emitter. */
  public WingbeatEmitter2D(
      Vector2 startMeters,
      Vector2 velocityMetersPerSecond,
      double amplitude,
      WingbeatSignalParameters params,
      double sampleRate,
      long randomSeed) {
    if (startMeters == null || velocityMetersPerSecond == null) {
      throw new IllegalArgumentException("positions must not be null");
    }
    if (amplitude < 0.0 || amplitude > 1.0 || !Double.isFinite(amplitude)) {
      throw new IllegalArgumentException("amplitude must be finite and in [0,1]");
    }
    if (params == null) {
      throw new IllegalArgumentException("params must not be null");
    }
    if (sampleRate <= 0.0 || !Double.isFinite(sampleRate)) {
      throw new IllegalArgumentException("sampleRate must be finite and > 0");
    }
    this.initialPositionMeters = startMeters;
    this.velocityVectorMetersPerSecond = velocityMetersPerSecond;
    this.signalAmplitude = amplitude;
    this.signalParameters = params;
    this.sampleRateHz = sampleRate;
    this.randomSeed = randomSeed;
    this.unitScalePhaseCache =
        WingbeatSignalGenerator.newPhaseAccumulatorCache(params, randomSeed, sampleRate);
  }

  @Override
  public Vector2 startMeters() {
    return initialPositionMeters;
  }

  @Override
  public Vector2 velocityMetersPerSecond() {
    return velocityVectorMetersPerSecond;
  }

  @Override
  public double frequencyHz() {
    return signalParameters.fundamentalFrequencyHz();
  }

  @Override
  public double amplitude() {
    return signalAmplitude;
  }

  @Override
  public double sampleAt(double seconds) {
    return signalAmplitude
        * WingbeatSignalGenerator.sampleAtTime(
            signalParameters, randomSeed, sampleRateHz, seconds, 1.0, unitScalePhaseCache);
  }

  @Override
  public double sampleAt(double seconds, double observedFrequencyHz) {
    if (observedFrequencyHz <= 0.0 || !Double.isFinite(observedFrequencyHz)) {
      throw new IllegalArgumentException("observedFrequencyHz must be finite and > 0");
    }
    double frequencyScale = observedFrequencyHz / signalParameters.fundamentalFrequencyHz();
    if (Double.compare(frequencyScale, 1.0) == 0) {
      return sampleAt(seconds);
    }
    return signalAmplitude
        * WingbeatSignalGenerator.sampleAtTime(
            signalParameters, randomSeed, sampleRateHz, seconds, frequencyScale);
  }

  @Override
  public AcousticGroundTruth acousticGroundTruth() {
    return signalParameters.toGroundTruth();
  }
}