DeterministicAudioParameters.java
package org.hammer.audio.dsp.workflow;
import java.util.Map;
import java.util.Objects;
import org.hammer.audio.dsp.GainProcessor;
import org.hammer.audio.workflow.Metadata;
import org.hammer.audio.workflow.Node;
import org.hammer.audio.workflow.catalog.ExperimentNodeParameters;
/** Parses and bounds executable node metadata for the deterministic audio slice. */
final class DeterministicAudioParameters {
static final float MIN_SAMPLE_RATE_HZ = 8_000.0f;
static final float MAX_SAMPLE_RATE_HZ = 384_000.0f;
static final int MAX_CHANNELS = 32;
static final int MAX_TOTAL_SAMPLES = 16_777_216;
private DeterministicAudioParameters() {
throw new UnsupportedOperationException("Utility class");
}
static SyntheticSignal parseSyntheticSignal(Node node) {
Map<String, String> entries = metadataEntries(node);
String waveform = required(entries, ExperimentNodeParameters.SIGNAL_WAVEFORM);
if (!ExperimentNodeParameters.WAVEFORM_SINE.equals(waveform)) {
throw invalid(
ExperimentNodeParameters.SIGNAL_WAVEFORM,
"must be '" + ExperimentNodeParameters.WAVEFORM_SINE + "', was '" + waveform + "'");
}
float sampleRate = finiteFloat(entries, ExperimentNodeParameters.SIGNAL_SAMPLE_RATE_HZ);
if (sampleRate < MIN_SAMPLE_RATE_HZ || sampleRate > MAX_SAMPLE_RATE_HZ) {
throw invalid(
ExperimentNodeParameters.SIGNAL_SAMPLE_RATE_HZ,
"must be in [8000, 384000], was " + sampleRate);
}
int channels = positiveInt(entries, ExperimentNodeParameters.SIGNAL_CHANNELS);
if (channels > MAX_CHANNELS) {
throw invalid(
ExperimentNodeParameters.SIGNAL_CHANNELS,
"must be <= " + MAX_CHANNELS + ", was " + channels);
}
int frameCount = positiveInt(entries, ExperimentNodeParameters.SIGNAL_FRAME_COUNT);
long sampleCount = (long) channels * frameCount;
if (sampleCount > MAX_TOTAL_SAMPLES) {
throw invalid(
ExperimentNodeParameters.SIGNAL_FRAME_COUNT,
"channels * frame-count must be <= " + MAX_TOTAL_SAMPLES + ", was " + sampleCount);
}
float frequency = finiteFloat(entries, ExperimentNodeParameters.SIGNAL_FREQUENCY_HZ);
if (frequency <= 0.0f || frequency >= sampleRate / 2.0f) {
throw invalid(
ExperimentNodeParameters.SIGNAL_FREQUENCY_HZ,
"must be > 0 and below Nyquist " + (sampleRate / 2.0f) + ", was " + frequency);
}
double phase = finiteDouble(entries, ExperimentNodeParameters.SIGNAL_PHASE_RADIANS);
float amplitude = finiteFloat(entries, ExperimentNodeParameters.SIGNAL_AMPLITUDE);
if (amplitude < 0.0f || amplitude > 1.0f) {
throw invalid(
ExperimentNodeParameters.SIGNAL_AMPLITUDE, "must be in [0, 1], was " + amplitude);
}
return new SyntheticSignal(
waveform, frequency, phase, amplitude, sampleRate, channels, frameCount);
}
static Gain parseGain(Node node) {
float factor = finiteFloat(metadataEntries(node), ExperimentNodeParameters.GAIN_FACTOR);
if (factor < 0.0f || factor > GainProcessor.MAX_GAIN) {
throw invalid(
ExperimentNodeParameters.GAIN_FACTOR,
"must be in [0, " + GainProcessor.MAX_GAIN + "], was " + factor);
}
return new Gain(factor);
}
private static Map<String, String> metadataEntries(Node node) {
Metadata metadata = node.metadata();
return metadata == null ? Map.of() : metadata.entries();
}
private static String required(Map<String, String> entries, String key) {
String value = entries.get(key);
if (value == null || value.isBlank()) {
throw invalid(key, "is required and must not be blank");
}
return value;
}
private static float finiteFloat(Map<String, String> entries, String key) {
String value = required(entries, key);
try {
float parsed = Float.parseFloat(value);
if (!Float.isFinite(parsed)) {
throw invalid(key, "must be finite, was " + value);
}
return parsed;
} catch (NumberFormatException exception) {
throw invalid(key, "must be a floating-point number, was '" + value + "'", exception);
}
}
private static double finiteDouble(Map<String, String> entries, String key) {
String value = required(entries, key);
try {
double parsed = Double.parseDouble(value);
if (!Double.isFinite(parsed)) {
throw invalid(key, "must be finite, was " + value);
}
return parsed;
} catch (NumberFormatException exception) {
throw invalid(key, "must be a floating-point number, was '" + value + "'", exception);
}
}
private static int positiveInt(Map<String, String> entries, String key) {
String value = required(entries, key);
try {
int parsed = Integer.parseInt(value);
if (parsed < 1) {
throw invalid(key, "must be >= 1, was " + parsed);
}
return parsed;
} catch (NumberFormatException exception) {
throw invalid(key, "must be an integer, was '" + value + "'", exception);
}
}
private static IllegalArgumentException invalid(String key, String detail) {
return new IllegalArgumentException("Parameter '" + key + "' " + detail);
}
private static IllegalArgumentException invalid(
String key, String detail, NumberFormatException cause) {
return new IllegalArgumentException("Parameter '" + key + "' " + detail, cause);
}
record SyntheticSignal(
String waveform,
float frequencyHz,
double phaseRadians,
float amplitude,
float sampleRateHz,
int channels,
int frameCount) {
SyntheticSignal {
Objects.requireNonNull(waveform, "waveform");
if (!Float.isFinite(frequencyHz)
|| !Double.isFinite(phaseRadians)
|| !Float.isFinite(amplitude)
|| !Float.isFinite(sampleRateHz)
|| channels < 1
|| frameCount < 1) {
throw new IllegalArgumentException("Synthetic signal parameters must remain valid");
}
}
}
record Gain(float factor) {
Gain {
if (!Float.isFinite(factor) || factor < 0.0f || factor > GainProcessor.MAX_GAIN) {
throw new IllegalArgumentException("Gain factor must remain within the supported range");
}
}
}
}