SineGenerator.java

package org.hammer.audio.signal;

import java.util.Objects;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;

/**
 * Deterministic mono sine-wave generator.
 *
 * <p>Produces {@code amplitude * sin(2π * frequency * t + initialPhase)} samples, sampled at {@code
 * format.sampleRate()}. Phase is tracked across calls in double precision to avoid drift over long
 * runs.
 *
 * <p>Although mono internally, the generator can be configured with a multi-channel format; in that
 * case the same signal is broadcast on every channel.
 *
 * @author refactoring
 */
public final class SineGenerator implements SignalGenerator {

  private static final double TWO_PI = 2.0d * Math.PI;

  private final AudioFormatDescriptor format;
  private final double frequencyHz;
  private final float amplitude;
  private final double phaseStep;
  private final double initialPhase;
  private double phase;
  private long frameIndex;

  /**
   * Create a new sine generator with zero initial phase.
   *
   * @param format output format descriptor
   * @param frequencyHz oscillator frequency in Hz; must be finite and {@code > 0}
   * @param amplitude peak amplitude in normalized units
   */
  public SineGenerator(AudioFormatDescriptor format, double frequencyHz, float amplitude) {
    this(format, frequencyHz, amplitude, 0.0d);
  }

  /**
   * Create a new sine generator with an explicit initial phase.
   *
   * @param format output format descriptor
   * @param frequencyHz oscillator frequency in Hz; must be finite and {@code > 0}
   * @param amplitude peak amplitude in normalized units
   * @param initialPhaseRadians finite initial phase in radians
   */
  public SineGenerator(
      AudioFormatDescriptor format,
      double frequencyHz,
      float amplitude,
      double initialPhaseRadians) {
    this.format = Objects.requireNonNull(format, "format");
    if (!Double.isFinite(frequencyHz) || !(frequencyHz > 0.0d)) {
      throw new IllegalArgumentException("frequencyHz must be finite and > 0, was " + frequencyHz);
    }
    if (!Float.isFinite(amplitude)) {
      throw new IllegalArgumentException("amplitude must be finite, was " + amplitude);
    }
    if (!Double.isFinite(initialPhaseRadians)) {
      throw new IllegalArgumentException(
          "initialPhaseRadians must be finite, was " + initialPhaseRadians);
    }
    this.frequencyHz = frequencyHz;
    this.amplitude = amplitude;
    this.phaseStep = TWO_PI * frequencyHz / format.sampleRate();
    this.initialPhase = normalizePhase(initialPhaseRadians);
    this.phase = initialPhase;
    this.frameIndex = 0L;
  }

  @Override
  public AudioFormatDescriptor format() {
    return format;
  }

  @Override
  public AudioBlock nextBlock(int frames) {
    if (frames < 1) {
      throw new IllegalArgumentException("frames must be >= 1");
    }
    int channels = format.channels();
    float[][] samples = new float[channels][frames];
    double currentPhase = phase;
    for (int frame = 0; frame < frames; frame++) {
      float value = (float) (Math.sin(currentPhase) * amplitude);
      for (int channel = 0; channel < channels; channel++) {
        samples[channel][frame] = value;
      }
      currentPhase += phaseStep;
    }
    long index = frameIndex;
    phase = normalizePhase(currentPhase);
    frameIndex += frames;
    return AudioBlock.wrap(format, samples, index, System.nanoTime());
  }

  @Override
  public void reset() {
    phase = initialPhase;
    frameIndex = 0L;
  }

  /** Returns the oscillator frequency in hertz. */
  public double frequencyHz() {
    return frequencyHz;
  }

  /** Returns the peak amplitude. */
  public float amplitude() {
    return amplitude;
  }

  /** Returns the normalized initial phase in radians. */
  public double initialPhaseRadians() {
    return initialPhase;
  }

  private static double normalizePhase(double value) {
    double normalized = value % TWO_PI;
    return normalized < 0.0d ? normalized + TWO_PI : normalized;
  }
}