ComparisonReport.java

package org.hammer.audio.compare;

import java.util.List;
import java.util.Objects;
import org.hammer.audio.analysis.MeasurementSnapshot;
import org.hammer.audio.analysis.SpectrumSnapshot;
import org.hammer.audio.core.AudioFormatDescriptor;
import org.hammer.audio.diagnosis.DiagnosisFinding;
import org.hammer.audio.diagnosis.DiagnosisSnapshot;

/**
 * Immutable A/B comparison of two analyzed audio recordings (or live sessions). Each side captures
 * the format and the three core snapshots produced by the analyzer chain.
 *
 * <p>Use {@link MarkdownComparisonReportRenderer} to format a report for diagnostics, QA notes or
 * bug tickets.
 */
public final class ComparisonReport {

  private final Side a;
  private final Side b;

  /** Create a report from two analyzed sides. */
  public ComparisonReport(Side a, Side b) {
    this.a = Objects.requireNonNull(a, "a");
    this.b = Objects.requireNonNull(b, "b");
  }

  public Side a() {
    return a;
  }

  public Side b() {
    return b;
  }

  /**
   * @return the absolute difference {@code |a - b|} treating {@link Double#NaN} as missing on
   *     either side (returns {@link Double#NaN} in that case)
   */
  public static double absDelta(double aValue, double bValue) {
    if (Double.isNaN(aValue) || Double.isNaN(bValue)) {
      return Double.NaN;
    }
    return Math.abs(aValue - bValue);
  }

  /**
   * One side of the comparison: label, format and the analyzed snapshots.
   *
   * @param label human-readable label (filename, "before", "after", ...)
   * @param format audio format descriptor of the recording
   * @param totalFrames total frame count consumed during analysis
   * @param measurement aggregate measurement snapshot
   * @param spectrum spectrum snapshot (may be {@code null} if not produced)
   * @param diagnosis diagnosis snapshot
   */
  public record Side(
      String label,
      AudioFormatDescriptor format,
      long totalFrames,
      MeasurementSnapshot measurement,
      SpectrumSnapshot spectrum,
      DiagnosisSnapshot diagnosis) {

    /** Compact constructor with null checks for required fields. */
    public Side {
      Objects.requireNonNull(label, "label");
      Objects.requireNonNull(format, "format");
      Objects.requireNonNull(measurement, "measurement");
      Objects.requireNonNull(diagnosis, "diagnosis");
    }

    /**
     * @return findings list (never {@code null}; possibly empty)
     */
    public List<DiagnosisFinding> findings() {
      return diagnosis.findings();
    }

    /**
     * @return duration in seconds derived from {@code totalFrames} and the format sample rate
     */
    public double durationSeconds() {
      float sampleRate = format.sampleRate();
      if (sampleRate <= 0f) {
        return Double.NaN;
      }
      return totalFrames / (double) sampleRate;
    }
  }
}