MarkdownComparisonReportRenderer.java

package org.hammer.audio.compare;

import java.util.List;
import java.util.Locale;
import java.util.Objects;
import org.hammer.audio.analysis.MeasurementSnapshot;
import org.hammer.audio.analysis.SpectrumSnapshot;
import org.hammer.audio.diagnosis.DiagnosisFinding;

/**
 * Renders a {@link ComparisonReport} as a Markdown document. The format is intentionally simple
 * (one table per analyzed dimension) so the output can be pasted into QA notes, GitHub issues or a
 * regression log.
 */
public final class MarkdownComparisonReportRenderer {

  /** Render the given comparison report to a Markdown string. */
  public String render(ComparisonReport report) {
    Objects.requireNonNull(report, "report");
    StringBuilder sb = new StringBuilder(2048);
    sb.append("# A/B comparison report\n\n");
    sb.append("| Side | Label | Format | Duration | Frames |\n");
    sb.append("|------|-------|--------|----------|--------|\n");
    appendSideRow(sb, "A", report.a());
    appendSideRow(sb, "B", report.b());
    sb.append('\n');

    appendMeasurementsTable(sb, report);
    sb.append('\n');

    appendSpectrumSummary(sb, report);
    sb.append('\n');

    appendDiagnosisSummary(sb, report);
    return sb.toString();
  }

  private static void appendSideRow(StringBuilder sb, String tag, ComparisonReport.Side s) {
    sb.append("| ")
        .append(tag)
        .append(" | ")
        .append(escapePipes(s.label()))
        .append(" | ")
        .append(s.format().sampleRate())
        .append(" Hz / ")
        .append(s.format().channels())
        .append(" ch / ")
        .append(s.format().sourceSampleSizeInBits())
        .append(" bit | ")
        .append(String.format(Locale.ROOT, "%.3f s", s.durationSeconds()))
        .append(" | ")
        .append(s.totalFrames())
        .append(" |\n");
  }

  private static void appendMeasurementsTable(StringBuilder sb, ComparisonReport report) {
    MeasurementSnapshot a = report.a().measurement();
    MeasurementSnapshot b = report.b().measurement();
    sb.append("## Measurements\n\n");
    sb.append("| Metric | A | B | abs Δ |\n");
    sb.append("|--------|---|---|-------|\n");
    appendDoubleRow(sb, "RMS", a.rms(), b.rms(), "%.4f");
    appendDoubleRow(sb, "Peak level", a.peakLevel(), b.peakLevel(), "%.4f");
    appendDoubleRow(
        sb, "Dominant freq (Hz)", a.dominantFrequencyHz(), b.dominantFrequencyHz(), "%.1f");
    if (a.stereoCorrelationAvailable() || b.stereoCorrelationAvailable()) {
      double sa = a.stereoCorrelationAvailable() ? a.stereoCorrelation() : Double.NaN;
      double sb2 = b.stereoCorrelationAvailable() ? b.stereoCorrelation() : Double.NaN;
      appendDoubleRow(sb, "Stereo correlation", sa, sb2, "%.3f");
    }
    sb.append("| Clipping | ")
        .append(a.clipping() ? "YES" : "no")
        .append(" | ")
        .append(b.clipping() ? "YES" : "no")
        .append(" | ")
        .append(a.clipping() == b.clipping() ? "same" : "**changed**")
        .append(" |\n");
  }

  private static void appendDoubleRow(
      StringBuilder sb, String label, double a, double b, String fmt) {
    sb.append("| ").append(label).append(" | ");
    sb.append(formatDouble(a, fmt)).append(" | ");
    sb.append(formatDouble(b, fmt)).append(" | ");
    double delta = ComparisonReport.absDelta(a, b);
    sb.append(formatDouble(delta, fmt)).append(" |\n");
  }

  private static String formatDouble(double v, String fmt) {
    if (Double.isNaN(v)) {
      return "n/a";
    }
    return String.format(Locale.ROOT, fmt, v);
  }

  private static void appendSpectrumSummary(StringBuilder sb, ComparisonReport report) {
    SpectrumSnapshot a = report.a().spectrum();
    SpectrumSnapshot b = report.b().spectrum();
    sb.append("## Spectrum summary\n\n");
    if (a == null || b == null) {
      sb.append("_One or both recordings produced no spectrum snapshot._\n");
      return;
    }
    sb.append("| Metric | A | B | abs Δ |\n");
    sb.append("|--------|---|---|-------|\n");
    appendDoubleRow(
        sb, "FFT bin count", a.magnitudesView().length, b.magnitudesView().length, "%.0f");
    appendDoubleRow(sb, "Peak magnitude", peakMagnitude(a), peakMagnitude(b), "%.4f");
    appendDoubleRow(sb, "Spectral centroid (Hz)", centroid(a), centroid(b), "%.1f");
  }

  private static double peakMagnitude(SpectrumSnapshot s) {
    float[] m = s.magnitudesView();
    double peak = 0.0;
    for (float v : m) {
      double abs = Math.abs(v);
      if (abs > peak) {
        peak = abs;
      }
    }
    return peak;
  }

  private static double centroid(SpectrumSnapshot s) {
    float[] m = s.magnitudesView();
    if (m.length == 0) {
      return Double.NaN;
    }
    double binHz = m.length > 1 ? (double) s.sampleRate() / (2.0d * (double) (m.length - 1)) : 0.0d;
    double sum = 0.0;
    double weighted = 0.0;
    for (int i = 0; i < m.length; i++) {
      double mag = Math.abs(m[i]);
      sum += mag;
      weighted += mag * (i * binHz);
    }
    return sum > 0.0 ? weighted / sum : Double.NaN;
  }

  private static void appendDiagnosisSummary(StringBuilder sb, ComparisonReport report) {
    sb.append("## Diagnosis findings\n\n");
    appendFindings(sb, "A", report.a().findings());
    sb.append('\n');
    appendFindings(sb, "B", report.b().findings());
  }

  private static void appendFindings(StringBuilder sb, String tag, List<DiagnosisFinding> list) {
    sb.append("**").append(tag).append("**\n\n");
    if (list.isEmpty()) {
      sb.append("- _no findings_\n");
      return;
    }
    for (DiagnosisFinding f : list) {
      sb.append("- `")
          .append(f.severity())
          .append("` ")
          .append(f.type())
          .append(" — ")
          .append(escapePipes(f.message()))
          .append(" (conf ")
          .append(String.format(Locale.ROOT, "%.2f", f.confidence()))
          .append(")\n");
    }
  }

  private static String escapePipes(String s) {
    return s == null ? "" : s.replace("|", "\\|");
  }
}