ChirpGenerator.java
package org.hammer.audio.signal;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;
/**
* Deterministic linear chirp / sweep generator.
*
* <p>Produces a sinusoidal signal whose instantaneous frequency increases linearly from {@code
* startHz} to {@code endHz} over {@code durationSeconds}. After the sweep completes the signal
* holds at {@code endHz} (or, if {@link #setLooping(boolean)} is enabled, restarts).
*
* @author refactoring
*/
public final class ChirpGenerator implements SignalGenerator {
private final AudioFormatDescriptor format;
private final double startHz;
private final double endHz;
private final double durationSeconds;
private final float amplitude;
private final double sampleRate;
private boolean looping;
private double timeSeconds;
private double phase;
private long frameIndex;
/**
* @param format output format descriptor
* @param startHz starting frequency (Hz); must be {@code > 0}
* @param endHz ending frequency (Hz); must be {@code > 0}
* @param durationSeconds sweep duration in seconds; must be {@code > 0}
* @param amplitude peak amplitude
*/
public ChirpGenerator(
AudioFormatDescriptor format,
double startHz,
double endHz,
double durationSeconds,
float amplitude) {
if (!(startHz > 0.0)) {
throw new IllegalArgumentException("startHz must be > 0");
}
if (!(endHz > 0.0)) {
throw new IllegalArgumentException("endHz must be > 0");
}
if (!(durationSeconds > 0.0)) {
throw new IllegalArgumentException("durationSeconds must be > 0");
}
this.format = format;
this.startHz = startHz;
this.endHz = endHz;
this.durationSeconds = durationSeconds;
this.amplitude = amplitude;
this.sampleRate = format.sampleRate();
}
/**
* Configure looping (the chirp restarts after {@code durationSeconds}).
*
* @param looping whether to loop
*/
public void setLooping(boolean looping) {
this.looping = looping;
}
@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 dt = 1.0 / sampleRate;
double t = timeSeconds;
double p = phase;
for (int i = 0; i < frames; i++) {
double tEff = looping ? (t % durationSeconds) : Math.min(t, durationSeconds);
double instFreq = startHz + (endHz - startHz) * (tEff / durationSeconds);
// integrate frequency to advance phase
p += 2.0 * Math.PI * instFreq * dt;
float v = (float) (Math.sin(p) * amplitude);
for (int c = 0; c < channels; c++) {
samples[c][i] = v;
}
t += dt;
}
long index = frameIndex;
timeSeconds = t;
phase = p % (2.0 * Math.PI);
frameIndex += frames;
return AudioBlock.wrap(format, samples, index, System.nanoTime());
}
@Override
public void reset() {
timeSeconds = 0.0;
phase = 0.0;
frameIndex = 0L;
}
}