GainNodeExecutor.java
package org.hammer.audio.dsp.workflow;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.dsp.GainProcessor;
import org.hammer.audio.dsp.workflow.DeterministicAudioParameters.Gain;
import org.hammer.audio.workflow.Edge;
import org.hammer.audio.workflow.Node;
import org.hammer.audio.workflow.catalog.ExperimentNodeProtocol;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Control;
import org.hammer.audio.workflow.execution.WorkflowRunModels.Violation;
/** Applies the shared gain processor to an immutable audio value in bounded chunks. */
final class GainNodeExecutor implements DeterministicAudioNodeExecutor {
private static final int CHUNK_FRAMES = 4_096;
@Override
public String nodeType() {
return ExperimentNodeProtocol.TYPE_GAIN;
}
@Override
public List<Violation> validate(Node node, List<Edge> incomingEdges) {
Objects.requireNonNull(node, "node");
Objects.requireNonNull(incomingEdges, "incomingEdges");
List<Violation> violations = new ArrayList<>();
if (incomingEdges.size() != 1) {
violations.add(
new Violation(
DeterministicAudioDiagnostics.INVALID_TOPOLOGY,
"Gain nodes require exactly one incoming audio edge, found " + incomingEdges.size(),
node.id()));
}
try {
DeterministicAudioParameters.parseGain(node);
} catch (IllegalArgumentException exception) {
violations.add(
new Violation(
DeterministicAudioDiagnostics.INVALID_PARAMETER, exception.getMessage(), node.id()));
}
return List.copyOf(violations);
}
@Override
public AudioBlock execute(Node node, AudioBlock input, Control control) {
Objects.requireNonNull(node, "node");
Objects.requireNonNull(control, "control");
if (input == null) {
throw new IllegalArgumentException("Gain nodes require one computed audio input");
}
Gain parameters = DeterministicAudioParameters.parseGain(node);
GainProcessor processor = new GainProcessor(parameters.factor());
float[][] output = new float[input.channels()][input.frames()];
int offset = 0;
while (offset < input.frames()) {
requireNotCancelled(control);
int frames = Math.min(CHUNK_FRAMES, input.frames() - offset);
AudioBlock chunk = inputChunk(input, offset, frames);
AudioBlock processed = processor.process(chunk);
copyInto(processed, output, offset);
offset += frames;
}
requireNotCancelled(control);
return AudioBlock.wrap(input.format(), output, input.frameIndex(), 0L);
}
private static AudioBlock inputChunk(AudioBlock input, int offset, int frames) {
float[][] samples = new float[input.channels()][frames];
for (int channel = 0; channel < input.channels(); channel++) {
System.arraycopy(input.channelView(channel), offset, samples[channel], 0, frames);
}
return AudioBlock.wrap(input.format(), samples, input.frameIndex() + offset, 0L);
}
private static void copyInto(AudioBlock source, float[][] target, int offset) {
for (int channel = 0; channel < source.channels(); channel++) {
System.arraycopy(source.channelView(channel), 0, target[channel], offset, source.frames());
}
}
private static void requireNotCancelled(Control control) {
if (control.cancellationRequested()) {
throw new DeterministicAudioCancellationException();
}
}
}