AudioBlockEvidence.java
package org.hammer.audio.dsp.workflow;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.concurrent.ConcurrentHashMap;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;
/** Produces canonical digests and stable numerical summaries for immutable audio blocks. */
final class AudioBlockEvidence {
static final String DIGEST_ALGORITHM = "SHA-256";
static final String DIGEST_ENCODING = "audio-block-f32be-planar-v1";
private static final int PREVIEW_FRAMES = 16;
private AudioBlockEvidence() {
throw new UnsupportedOperationException("Utility class");
}
static Map<String, String> artifacts(AudioBlock block, String outputNodeId, String outputPortId) {
Objects.requireNonNull(block, "block");
Map<String, String> artifacts = new ConcurrentHashMap<>();
artifacts.put(DeterministicAudioArtifacts.OUTPUT_NODE_ID, outputNodeId);
artifacts.put(DeterministicAudioArtifacts.OUTPUT_PORT_ID, outputPortId);
artifacts.put(DeterministicAudioArtifacts.OUTPUT_DIGEST_ALGORITHM, DIGEST_ALGORITHM);
artifacts.put(DeterministicAudioArtifacts.OUTPUT_DIGEST_ENCODING, DIGEST_ENCODING);
artifacts.put(DeterministicAudioArtifacts.OUTPUT_DIGEST_SHA256, digest(block));
artifacts.put(
DeterministicAudioArtifacts.OUTPUT_SAMPLE_RATE_HZ,
Float.toString(block.format().sampleRate()));
artifacts.put(DeterministicAudioArtifacts.OUTPUT_CHANNELS, Integer.toString(block.channels()));
artifacts.put(DeterministicAudioArtifacts.OUTPUT_FRAMES, Integer.toString(block.frames()));
Summary summary = summarize(block);
artifacts.put(DeterministicAudioArtifacts.OUTPUT_MIN, Float.toHexString(summary.minimum()));
artifacts.put(DeterministicAudioArtifacts.OUTPUT_MAX, Float.toHexString(summary.maximum()));
artifacts.put(DeterministicAudioArtifacts.OUTPUT_MEAN, Double.toHexString(summary.mean()));
artifacts.put(DeterministicAudioArtifacts.OUTPUT_RMS, Double.toHexString(summary.rms()));
artifacts.put(
DeterministicAudioArtifacts.OUTPUT_CHANNEL_ZERO_PREVIEW, channelZeroPreview(block));
return Map.copyOf(artifacts);
}
private static String digest(AudioBlock block) {
MessageDigest digest = sha256();
AudioFormatDescriptor format = block.format();
updateInt(digest, Float.floatToIntBits(format.sampleRate()));
updateInt(digest, format.channels());
updateInt(digest, format.sourceSampleSizeInBits());
updateInt(digest, block.frames());
for (int channel = 0; channel < block.channels(); channel++) {
for (float sample : block.channelView(channel)) {
updateInt(digest, Float.floatToIntBits(sample));
}
}
return hex(digest.digest());
}
private static Summary summarize(AudioBlock block) {
float minimum = Float.POSITIVE_INFINITY;
float maximum = Float.NEGATIVE_INFINITY;
double sum = 0.0d;
double sumOfSquares = 0.0d;
long sampleCount = 0L;
for (int channel = 0; channel < block.channels(); channel++) {
for (float sample : block.channelView(channel)) {
if (!Float.isFinite(sample)) {
throw new IllegalArgumentException("Audio output contains non-finite samples");
}
minimum = Math.min(minimum, sample);
maximum = Math.max(maximum, sample);
sum += sample;
sumOfSquares += (double) sample * sample;
sampleCount++;
}
}
return new Summary(minimum, maximum, sum / sampleCount, Math.sqrt(sumOfSquares / sampleCount));
}
private static String channelZeroPreview(AudioBlock block) {
StringJoiner preview = new StringJoiner(",");
float[] samples = block.channelView(0);
int previewLength = Math.min(samples.length, PREVIEW_FRAMES);
for (int frame = 0; frame < previewLength; frame++) {
preview.add(Float.toHexString(samples[frame]));
}
return preview.toString();
}
private static MessageDigest sha256() {
try {
return MessageDigest.getInstance(DIGEST_ALGORITHM);
} catch (NoSuchAlgorithmException exception) {
throw new IllegalStateException("SHA-256 is unavailable", exception);
}
}
private static void updateInt(MessageDigest digest, int value) {
digest.update((byte) (value >>> 24));
digest.update((byte) (value >>> 16));
digest.update((byte) (value >>> 8));
digest.update((byte) value);
}
private static String hex(byte[] bytes) {
StringBuilder text = new StringBuilder(bytes.length * 2);
for (byte value : bytes) {
text.append(Character.forDigit((value >>> 4) & 0x0f, 16))
.append(Character.forDigit(value & 0x0f, 16));
}
return text.toString();
}
private record Summary(float minimum, float maximum, double mean, double rms) {
Summary {
if (!Float.isFinite(minimum)
|| !Float.isFinite(maximum)
|| !Double.isFinite(mean)
|| !Double.isFinite(rms)) {
throw new IllegalArgumentException("Audio evidence summary must remain finite");
}
}
}
}