RecordingComparator.java

package org.hammer.audio.compare;

import java.io.IOException;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
import org.hammer.audio.analysis.MeasurementCalculator;
import org.hammer.audio.analysis.MeasurementSnapshot;
import org.hammer.audio.analysis.SpectrumAnalyzer;
import org.hammer.audio.analysis.SpectrumSnapshot;
import org.hammer.audio.core.AudioBlock;
import org.hammer.audio.core.AudioFormatDescriptor;
import org.hammer.audio.diagnosis.DiagnosisAnalyzer;
import org.hammer.audio.diagnosis.DiagnosisSnapshot;
import org.hammer.audio.recording.AudioBlockRecordingReader;
import org.hammer.audio.spectrogram.SpectrogramAnalyzer;

/**
 * Replays two {@code .aar} recordings (or two in-memory block lists), runs the standard analyzer
 * stack on each, and returns a {@link ComparisonReport}.
 *
 * <p>The recording is analyzed block-by-block exactly as the live UI would, so the resulting
 * snapshots reflect the end state at the last block (matching the "freeze and inspect" workflow).
 */
public final class RecordingComparator {

  private static final int DEFAULT_FFT_SIZE = 1024;
  private static final int DEFAULT_SPECTROGRAM_FRAMES = 128;

  private final int fftSize;

  /** Create a comparator with the default {@value #DEFAULT_FFT_SIZE} FFT size. */
  public RecordingComparator() {
    this(DEFAULT_FFT_SIZE);
  }

  /**
   * @param fftSize FFT size used by the spectrum / spectrogram analyzers
   */
  public RecordingComparator(int fftSize) {
    if (fftSize <= 0 || Integer.bitCount(fftSize) != 1) {
      throw new IllegalArgumentException("fftSize must be a positive power of two, was " + fftSize);
    }
    this.fftSize = fftSize;
  }

  /**
   * Compare two recordings on disk.
   *
   * @param fileA recording A
   * @param fileB recording B
   * @param labelA human-readable label for A (e.g. the filename)
   * @param labelB human-readable label for B
   */
  public ComparisonReport compareFiles(Path fileA, Path fileB, String labelA, String labelB)
      throws IOException {
    Objects.requireNonNull(fileA, "fileA");
    Objects.requireNonNull(fileB, "fileB");
    List<AudioBlock> blocksA = AudioBlockRecordingReader.readAll(fileA);
    List<AudioBlock> blocksB = AudioBlockRecordingReader.readAll(fileB);
    return compareBlocks(blocksA, blocksB, labelA, labelB);
  }

  /** Compare two pre-loaded block sequences. */
  public ComparisonReport compareBlocks(
      List<AudioBlock> blocksA, List<AudioBlock> blocksB, String labelA, String labelB) {
    Objects.requireNonNull(blocksA, "blocksA");
    Objects.requireNonNull(blocksB, "blocksB");
    if (blocksA.isEmpty()) {
      throw new IllegalArgumentException("blocksA must be non-empty");
    }
    if (blocksB.isEmpty()) {
      throw new IllegalArgumentException("blocksB must be non-empty");
    }
    return new ComparisonReport(analyze(blocksA, labelA), analyze(blocksB, labelB));
  }

  private ComparisonReport.Side analyze(List<AudioBlock> blocks, String label) {
    AudioFormatDescriptor format = blocks.get(0).format();
    SpectrumAnalyzer spectrumAnalyzer = new SpectrumAnalyzer(fftSize, 0, format.sampleRate());
    MeasurementCalculator measurementCalculator = new MeasurementCalculator();
    SpectrogramAnalyzer spectrogramAnalyzer =
        new SpectrogramAnalyzer(fftSize, 0, format.sampleRate(), DEFAULT_SPECTROGRAM_FRAMES);
    DiagnosisAnalyzer diagnosisAnalyzer = new DiagnosisAnalyzer();

    SpectrumSnapshot lastSpectrum = null;
    AudioBlock lastBlock = null;
    long totalFrames = 0L;
    for (AudioBlock block : blocks) {
      if (block.frames() <= 0) {
        continue;
      }
      lastBlock = block;
      totalFrames += block.frames();
      if (block.channels() > 0 && block.frames() >= fftSize) {
        lastSpectrum = spectrumAnalyzer.analyze(block);
        spectrogramAnalyzer.analyze(block);
      }
    }
    MeasurementSnapshot measurement = measurementCalculator.calculate(lastBlock, lastSpectrum);
    DiagnosisSnapshot diagnosis =
        diagnosisAnalyzer.analyze(lastBlock, lastSpectrum, spectrogramAnalyzer.history(), null);
    return new ComparisonReport.Side(
        label, format, totalFrames, measurement, lastSpectrum, diagnosis);
  }
}