DemoAudioCaptureService.java
package org.hammer.audio;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.sound.sampled.AudioFormat;
import org.hammer.audio.buffer.AudioRingBuffer;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;
import org.hammer.audio.signal.ChirpGenerator;
import org.hammer.audio.signal.DemoPresetGenerator;
import org.hammer.audio.signal.SignalGenerator;
import org.hammer.audio.signal.SineGenerator;
import org.hammer.audio.signal.SquareGenerator;
import org.hammer.audio.snapshot.WaveformSnapshot;
import org.hammer.audio.ui.WaveformRenderer;
/**
* Deterministic {@link AudioCaptureService} based on synthetic signal generators.
*
* <p>Used for UI demos and testing in environments without a microphone.
*/
public final class DemoAudioCaptureService implements AudioCaptureService {
private static final int RING_BUFFER_CAPACITY = 64;
private static final int DEFAULT_FRAMES_PER_BLOCK = 1024;
private static final int MIN_FRAMES_PER_BLOCK = 64;
private static final float DEMO_AMPLITUDE = 0.75f;
private static final float TICK_SECONDS = 1f / 1000f;
private final AtomicBoolean running = new AtomicBoolean(false);
private final AudioFormatDescriptor descriptor;
private final SignalGenerator signalGenerator;
private final AudioRingBuffer<AudioBlock> ringBuffer =
new AudioRingBuffer<>(RING_BUFFER_CAPACITY);
private final int tickEveryNSamples;
private final AudioFormat format;
private volatile WaveformModel latestModel = WaveformModel.EMPTY;
private volatile AudioBlock latestBlock;
private volatile int divisor;
private volatile int panelWidth = 640;
private volatile int panelHeight = 200;
private ExecutorService workerExecutor;
public DemoAudioCaptureService(
float sampleRate,
int sampleSizeInBits,
int channels,
int divisor,
DemoSignalType signalType) {
if (divisor < 1) {
throw new IllegalArgumentException("Divisor must be >= 1");
}
this.descriptor =
new AudioFormatDescriptor(sampleRate, Math.max(1, channels), sampleSizeInBits);
this.signalGenerator = createSignalGenerator(descriptor, signalType);
this.divisor = divisor;
this.tickEveryNSamples = (int) (sampleRate * TICK_SECONDS);
this.format = new AudioFormat(sampleRate, sampleSizeInBits, descriptor.channels(), true, false);
}
@Override
public void start() {
if (running.getAndSet(true)) {
return;
}
workerExecutor =
Executors.newSingleThreadExecutor(
runnable -> {
Thread worker = new Thread(runnable, "DemoAudioCaptureWorker");
worker.setDaemon(true);
return worker;
});
workerExecutor.submit(this::generateLoop);
}
@Override
public void stop() {
if (!running.getAndSet(false)) {
return;
}
if (workerExecutor != null) {
workerExecutor.shutdownNow();
try {
if (!workerExecutor.awaitTermination(1, TimeUnit.SECONDS)) {
workerExecutor.shutdownNow();
}
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
}
workerExecutor = null;
}
}
@Override
public boolean isRunning() {
return running.get();
}
@Override
public WaveformModel getLatestModel() {
return latestModel != null ? latestModel : WaveformModel.EMPTY;
}
@Override
public AudioFormat getFormat() {
return format;
}
@Override
public AudioFormatDescriptor getDescriptor() {
return descriptor;
}
@Override
public AudioBlock getLatestBlock() {
return latestBlock;
}
@Override
public AudioRingBuffer<AudioBlock> getRingBuffer() {
return ringBuffer;
}
@Override
public void setDivisor(int divisor) {
if (divisor < 1) {
throw new IllegalArgumentException("Divisor must be >= 1");
}
this.divisor = divisor;
}
@Override
public int getDivisor() {
return divisor;
}
@Override
public void recomputeLayout(int width, int height) {
panelWidth = width;
panelHeight = height;
AudioBlock block = latestBlock;
if (block != null) {
latestModel = buildLegacyModel(block);
}
}
private void generateLoop() {
while (running.get() && !Thread.currentThread().isInterrupted()) {
int frames = Math.max(MIN_FRAMES_PER_BLOCK, DEFAULT_FRAMES_PER_BLOCK / Math.max(1, divisor));
AudioBlock block = signalGenerator.nextBlock(frames);
latestBlock = block;
ringBuffer.offer(block);
latestModel = buildLegacyModel(block);
int sleepMillis =
Math.max(10, Math.round((1000f * frames) / Math.max(1f, descriptor.sampleRate())));
try {
Thread.sleep(sleepMillis);
} catch (InterruptedException interruptedException) {
Thread.currentThread().interrupt();
return;
}
}
}
private WaveformModel buildLegacyModel(AudioBlock block) {
WaveformSnapshot snapshot =
WaveformSnapshot.wrap(
block.samples(),
block.format().sampleRate(),
block.frameIndex(),
block.timestampNanos());
int[] xPoints = WaveformRenderer.computeXPoints(snapshot.frames(), panelWidth);
int[][] yPoints;
if (panelHeight <= 0) {
yPoints = new int[snapshot.channels()][0];
} else {
yPoints = WaveformRenderer.computeYPointsAllChannels(snapshot, panelHeight);
}
int dataSizeBytes =
snapshot.frames()
* snapshot.channels()
* Math.max(1, descriptor.sourceSampleSizeInBits() / 8);
return new WaveformModel(xPoints, yPoints, tickEveryNSamples, dataSizeBytes);
}
private static SignalGenerator createSignalGenerator(
AudioFormatDescriptor format, DemoSignalType signalType) {
DemoSignalType selected = signalType == null ? DemoSignalType.SINE : signalType;
return switch (selected) {
case SINE -> new SineGenerator(format, 440.0, DEMO_AMPLITUDE);
case SQUARE -> new SquareGenerator(format, 440.0, DEMO_AMPLITUDE);
case CHIRP -> {
ChirpGenerator chirpGenerator =
new ChirpGenerator(format, 120.0, 2800.0, 2.5, DEMO_AMPLITUDE);
chirpGenerator.setLooping(true);
yield chirpGenerator;
}
case MOSQUITO_BURST, MOVING_CHIRP, HUM_HARMONICS, CLIPPING_TEST, STEREO_DELAY_TEST ->
new DemoPresetGenerator(format, selected);
};
}
}