SimulationScenarios.java
package org.hammer.audio.experimental.acoustic.simulation;
import java.util.List;
import org.hammer.audio.acquisition.Microphone;
import org.hammer.audio.acquisition.MicrophoneArray;
import org.hammer.audio.geometry.Vector2;
/**
* Catalog of reproducible localization scenarios used by validation tests and demos.
*
* <p>Every scenario is a self-contained, deterministic {@link Scenario} record bundling a {@link
* Room2D}, a {@link MicrophoneArray}, a list of {@link SoundEmitter2D}s, a sample rate, a duration
* in seconds and a random seed. Two calls with identical scenario parameters produce bit-identical
* signals via {@link SimulatedMicrophoneArraySource}.
*
* <p>The provided scenarios mirror the canonical research situations:
*
* <ul>
* <li>{@link #singleSource()} — one stationary tonal source in an anechoic room.
* <li>{@link #twoCloseFrequencies()} — two stationary sources at different positions whose
* frequencies are close enough to challenge naive single-peak trackers.
* <li>{@link #noisyRoom()} — single source with significant background noise.
* <li>{@link #movingSource()} — one source travelling across the room with constant velocity.
* <li>{@link #movingAcrossArray()} — one source travelling laterally across the array.
* <li>{@link #twoMovingSources()} — two tones with distinct velocities.
* <li>{@link #reflectedEnvironment()} — single source with wall reflections enabled.
* </ul>
*/
public final class SimulationScenarios {
private static final float SAMPLE_RATE = 16_000.0f;
private SimulationScenarios() {
// utility
}
/** One stationary 600 Hz tone at (1.5, 1.0) in an anechoic 3x2 m room. */
public static Scenario singleSource() {
return new Scenario(
"single-source",
new Room2D(3.0, 2.0, 0.0, 0.0),
defaultArray(),
List.of(new SoundEmitter2D(new Vector2(1.5, 1.0), Vector2.ZERO, 600.0, 0.5)),
SAMPLE_RATE,
0.5,
1L);
}
/** Two stationary tones at 600 and 640 Hz, located at distinct positions in an anechoic room. */
public static Scenario twoCloseFrequencies() {
return new Scenario(
"two-close-frequencies",
new Room2D(3.0, 2.0, 0.0, 0.0),
defaultArray(),
List.of(
new SoundEmitter2D(new Vector2(1.0, 1.0), Vector2.ZERO, 600.0, 0.5),
new SoundEmitter2D(new Vector2(2.0, 1.0), Vector2.ZERO, 640.0, 0.5)),
SAMPLE_RATE,
0.5,
2L);
}
/** Single source plus broadband room noise; tests robustness of peak detection. */
public static Scenario noisyRoom() {
return new Scenario(
"noisy-room",
new Room2D(3.0, 2.0, 0.0, 0.05),
defaultArray(),
List.of(new SoundEmitter2D(new Vector2(1.5, 1.2), Vector2.ZERO, 720.0, 0.5)),
SAMPLE_RATE,
0.5,
3L);
}
/** One source travelling from (0.5, 1.0) to (2.5, 1.0) over the scenario duration. */
public static Scenario movingSource() {
return new Scenario(
"moving-source",
new Room2D(3.0, 2.0, 0.0, 0.0),
defaultArray(),
List.of(new SoundEmitter2D(new Vector2(0.5, 1.0), new Vector2(4.0, 0.0), 660.0, 0.5)),
SAMPLE_RATE,
0.5,
4L);
}
/** One source moving primarily toward the array for Doppler validation. */
public static Scenario movingTowardArray() {
return new Scenario(
"moving-toward-array",
new Room2D(3.0, 2.0, 0.0, 0.0),
defaultArray(),
List.of(new SoundEmitter2D(new Vector2(1.5, 1.8), new Vector2(0.0, -2.0), 700.0, 0.5)),
SAMPLE_RATE,
0.5,
6L);
}
/** One source moving laterally across the array. */
public static Scenario movingAcrossArray() {
return new Scenario(
"moving-across-array",
new Room2D(3.0, 2.0, 0.0, 0.0),
defaultArray(),
List.of(new SoundEmitter2D(new Vector2(0.6, 1.0), new Vector2(2.0, 0.0), 760.0, 0.5)),
SAMPLE_RATE,
0.5,
7L);
}
/** Two moving sources with different frequencies and velocities. */
public static Scenario twoMovingSources() {
return new Scenario(
"two-moving-sources",
new Room2D(3.0, 2.0, 0.0, 0.0),
defaultArray(),
List.of(
new SoundEmitter2D(new Vector2(0.8, 1.0), new Vector2(1.4, 0.0), 620.0, 0.45),
new SoundEmitter2D(new Vector2(2.2, 1.4), new Vector2(-0.8, -0.4), 840.0, 0.45)),
SAMPLE_RATE,
0.5,
8L);
}
/** Single source with reflective walls (specular x-axis reflection in the simulator). */
public static Scenario reflectedEnvironment() {
return new Scenario(
"reflected-environment",
new Room2D(3.0, 2.0, 0.35, 0.01),
defaultArray(),
List.of(new SoundEmitter2D(new Vector2(0.8, 1.0), Vector2.ZERO, 580.0, 0.5)),
SAMPLE_RATE,
0.5,
5L);
}
/** All bundled scenarios in canonical order. */
public static List<Scenario> all() {
return List.of(
singleSource(),
twoCloseFrequencies(),
noisyRoom(),
movingSource(),
movingTowardArray(),
movingAcrossArray(),
twoMovingSources(),
reflectedEnvironment());
}
/** Default 4-microphone square array spanning roughly 30 cm, centered near (1.5, 0.1). */
public static MicrophoneArray defaultArray() {
return new MicrophoneArray(
List.of(
new Microphone("m0", new Vector2(1.35, 0.0), 0),
new Microphone("m1", new Vector2(1.65, 0.0), 1),
new Microphone("m2", new Vector2(1.35, 0.3), 2),
new Microphone("m3", new Vector2(1.65, 0.3), 3)));
}
/** One reproducible scenario. */
public record Scenario(
String name,
Room2D room,
MicrophoneArray array,
List<SoundEmitter2D> emitters,
float sampleRate,
double durationSeconds,
long randomSeed) {
/** Validate and defensively copy emitters. */
public Scenario {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("name must not be blank");
}
if (room == null || array == null) {
throw new IllegalArgumentException("room and array must not be null");
}
if (emitters == null || emitters.isEmpty()) {
throw new IllegalArgumentException("emitters must not be empty");
}
if (!(sampleRate > 0.0f)) {
throw new IllegalArgumentException("sampleRate must be > 0");
}
if (!(durationSeconds > 0.0)) {
throw new IllegalArgumentException("durationSeconds must be > 0");
}
emitters = List.copyOf(emitters);
}
/** Create a fresh deterministic audio source for this scenario. */
public SimulatedMicrophoneArraySource newSource() {
return new SimulatedMicrophoneArraySource(
room, array, emitters, sampleRate, durationSeconds, randomSeed);
}
}
}