AcousticLocalizationPlugin.java

package org.hammer.audio.experimental.acoustic.plugin;

import java.awt.BorderLayout;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import org.hammer.audio.experimental.acoustic.workbench.AcousticLocalizationWorkbenchPanel;
import org.hammer.audio.experimental.acoustic.workbench.ImportedRecordingWorkbenchPanel;
import org.hammer.audio.plugin.AnalysisContribution;
import org.hammer.audio.plugin.AudioAnalyzerPlugin;
import org.hammer.audio.plugin.BenchmarkContribution;
import org.hammer.audio.plugin.CalibrationContribution;
import org.hammer.audio.plugin.DemoSignalContribution;
import org.hammer.audio.plugin.ExperimentContribution;
import org.hammer.audio.plugin.ExportFormatContribution;
import org.hammer.audio.plugin.MenuContribution;
import org.hammer.audio.plugin.PipelineContribution;
import org.hammer.audio.plugin.PluginDescriptor;
import org.hammer.audio.plugin.SignalSourceContribution;
import org.hammer.audio.plugin.SnapshotStreamContribution;
import org.hammer.audio.plugin.ViewContribution;
import org.hammer.audio.plugin.VisualizationContribution;

/**
 * Reference plugin that exposes the experimental acoustic-localization research code (wingbeat
 * frequency tracking, TDOA estimators, delay-and-sum beamforming and the 2D room simulator) to the
 * host application as a set of contributions.
 *
 * <p>This class deliberately only describes contributions through the stable plugin API. The
 * concrete DSP types remain in the {@code org.hammer.audio.experimental.acoustic} packages so the
 * stable plugin API does not need to depend on any audio-domain module.
 *
 * <p>The plugin implements all workbench contribution types defined by the generic plugin API,
 * making the acoustic-localization workflow the first reference implementation of the reusable
 * signal-processing workbench platform.
 */
public final class AcousticLocalizationPlugin implements AudioAnalyzerPlugin {

  private static final String CATEGORY_LOCALIZATION = "localization";

  private static final PluginDescriptor DESCRIPTOR =
      new PluginDescriptor(
          "acoustic-localization",
          "Experimental Acoustic Localization",
          "0.1.0",
          "Experimental localization of weak, intermittent or insect-like acoustic sources.",
          "docs/plugins/acoustic-localization.md",
          true);

  @Override
  public PluginDescriptor descriptor() {
    return DESCRIPTOR;
  }

  @Override
  public List<AnalysisContribution> analysisContributions() {
    return List.of(
        contribution(
            "wingbeat-frequency-tracker",
            "Tracks the dominant wingbeat frequency inside a configurable band."),
        contribution(
            "cross-correlation-tdoa",
            "Time-domain cross-correlation TDOA estimator for microphone pairs."),
        contribution(
            "gcc-phat-tdoa",
            "Frequency-domain GCC-PHAT TDOA estimator robust against narrow-band signals."),
        contribution(
            "delay-and-sum-beamformer",
            "Delay-and-sum beamformer producing a 2D heatmap over a candidate grid."),
        contribution(
            "doppler-velocity-tracking",
            "Doppler-based radial velocity estimation fused with multi-frame source tracking."),
        contribution(
            "mosquito-localization-pipeline",
            "Orchestrates frequency tracking, TDOA estimation and beamforming into a snapshot."));
  }

  @Override
  public List<DemoSignalContribution> demoSignalContributions() {
    return List.of(
        demo(
            "insect-burst",
            "Insect-like high-frequency burst",
            "Short, narrow-band burst around typical mosquito wingbeat frequencies."),
        demo(
            "moving-source",
            "Moving acoustic source",
            "Synthetic source moving across a 2D array, useful for tracking experiments."));
  }

  @Override
  public List<SignalSourceContribution> signalSourceContributions() {
    return List.of(
        signalSource(
            "simulated-microphone-array",
            "Simulated Microphone Array",
            "Deterministic synthetic microphone array generated from 2D room acoustics simulation.",
            "synthetic"),
        signalSource(
            "humbugdb-recording",
            "HumBugDB Recording",
            "Offline recording loaded from a local HumBugDB dataset export.",
            "dataset"));
  }

  @Override
  public List<ExperimentContribution> experimentContributions() {
    return List.of(
        experiment(
            "single-static-source",
            "Single Static Source",
            "Localize a single stationary mosquito-like emitter in a simulated room.",
            CATEGORY_LOCALIZATION),
        experiment(
            "moving-source",
            "Moving Source",
            "Track a single source moving linearly across the microphone array.",
            CATEGORY_LOCALIZATION),
        experiment(
            "two-sources",
            "Two Simultaneous Sources",
            "Localize two independent emitters active at the same time.",
            CATEGORY_LOCALIZATION),
        experiment(
            "reflections",
            "Room Reflections",
            "Test localization robustness under first-order wall reflections.",
            CATEGORY_LOCALIZATION),
        experiment(
            "noisy-environment",
            "Noisy Environment",
            "Measure localization accuracy under additive Gaussian noise.",
            CATEGORY_LOCALIZATION),
        experiment(
            "doppler-moving-source",
            "Doppler Moving Source",
            "Estimate Doppler radial velocity of a moving source.",
            CATEGORY_LOCALIZATION),
        experiment(
            "humbugdb-classification",
            "HumBugDB Classification",
            "Replay feature extraction and rule-based classification on imported HumBugDB clips.",
            "classification"));
  }

  @Override
  public List<PipelineContribution> pipelineContributions() {
    return List.of(
        pipeline(
            "mosquito-localization",
            "Mosquito Localization Pipeline",
            "End-to-end: peak detection, frequency clustering, TDOA, beamforming, Kalman tracking.",
            "peak-detection, frequency-clustering, TDOA, delay-and-sum, Kalman-tracking"),
        pipeline(
            "wingbeat-classification",
            "Wingbeat Classification Pipeline",
            "Feature extraction from a recording followed by rule-based species classification.",
            "feature-extraction, rule-based-classifier, evaluation"));
  }

  @Override
  public List<SnapshotStreamContribution> snapshotStreamContributions() {
    return List.of(
        snapshotStream(
            "tracking-snapshot-stream",
            "Tracking Snapshot Stream",
            "Per-frame TrackingSnapshot values produced by the localization pipeline.",
            "org.hammer.audio.experimental.acoustic.tracking.TrackingSnapshot"),
        snapshotStream(
            "acoustic-localization-snapshot-stream",
            "Acoustic Localization Snapshot Stream",
            "Per-frame AcousticLocalizationSnapshot values with frequency clusters and positions.",
            "org.hammer.audio.experimental.acoustic.AcousticLocalizationSnapshot"));
  }

  @Override
  public List<VisualizationContribution> visualizationContributions() {
    return List.of(
        visualization(
            "room-map-2d",
            "2D Room Map",
            "Top-down 2D view of the room with microphones, ground-truth and tracked positions.",
            "2d-spatial"),
        visualization(
            "frequency-cluster-timeline",
            "Frequency Cluster Timeline",
            "Time-series of detected frequency clusters per frame.",
            "time-series"),
        visualization(
            "tracking-log",
            "Tracking Log",
            "Per-frame text log of track IDs, positions, confidence and Doppler estimates.",
            "table"),
        visualization(
            "classification-confusion-matrix",
            "Classification Confusion Matrix",
            "Ground-truth vs predicted label grid from HumBugDB classification.",
            "table"));
  }

  @Override
  public List<CalibrationContribution> calibrationContributions() {
    return List.of(
        calibration(
            "generator-calibration",
            "Generator Calibration",
            "Tunes generator parameters to match real HumBugDB recording statistics.",
            "generator"),
        calibration(
            "feature-normalization",
            "Feature Normalization",
            "Normalizes WingbeatFeatureVector statistics from real corpus for classification.",
            "feature-normalization"));
  }

  @Override
  public List<BenchmarkContribution> benchmarkContributions() {
    return List.of(
        benchmark(
            "localization-position-error",
            "Localization Position Error",
            "Mean / median / 95th-percentile Euclidean position error against ground truth.",
            "metres"),
        benchmark(
            "doppler-velocity-error",
            "Doppler Velocity Error",
            "Mean absolute error between estimated and true radial velocity.",
            "metres/second"),
        benchmark(
            "classification-accuracy",
            "Classification Accuracy",
            "Overall accuracy and per-label precision/recall from HumBugDB evaluation.",
            "percent"),
        benchmark(
            "processing-latency",
            "Processing Latency",
            "Frame processing time relative to the real-time audio block budget.",
            "milliseconds"));
  }

  @Override
  public List<ExportFormatContribution> exportFormatContributions() {
    return List.of(
        exportFormat(
            "markdown-report",
            "Markdown Report",
            "md",
            "Full workbench run report with summary table, per-frame log and metrics."),
        exportFormat(
            "csv-tracks",
            "CSV Track Table",
            "csv",
            "Per-frame track positions, velocities and confidence as a flat CSV table."),
        exportFormat(
            "jsonl-snapshots",
            "JSON-lines Snapshots",
            "jsonl",
            "One JSON object per frame snapshot, suitable for offline analysis pipelines."));
  }

  @Override
  public List<MenuContribution> menuContributions() {
    return List.of(
        new MenuContribution() {
          @Override
          public String id() {
            return "log-info";
          }

          @Override
          public String label() {
            return "Log plugin info";
          }

          @Override
          public Runnable action() {
            return () ->
                java.util.logging.Logger.getLogger(AcousticLocalizationPlugin.class.getName())
                    .info(
                        () ->
                            "Acoustic Localization plugin active: see "
                                + DESCRIPTOR.documentationPath());
          }
        });
  }

  @Override
  public List<ViewContribution> viewContributions() {
    return List.of(
        new ViewContribution() {
          @Override
          public String id() {
            return "acoustic-localization-workbench";
          }

          @Override
          public String title() {
            return "Acoustic Localization Workbench (experimental)";
          }

          @Override
          public java.util.function.Supplier<javax.swing.JComponent> componentFactory() {
            return AcousticLocalizationWorkbenchPanel::new;
          }
        },
        new ViewContribution() {
          @Override
          public String id() {
            return "acoustic-localization-overview";
          }

          @Override
          public String title() {
            return "Acoustic Localization (overview)";
          }

          @Override
          public java.util.function.Supplier<JPanel> componentFactory() {
            return AcousticLocalizationPlugin::createOverviewPanel;
          }
        },
        new ViewContribution() {
          @Override
          public String id() {
            return "imported-recording-workbench";
          }

          @Override
          public String title() {
            return "Imported Recording Workbench (experimental)";
          }

          @Override
          public java.util.function.Supplier<javax.swing.JComponent> componentFactory() {
            return ImportedRecordingWorkbenchPanel::new;
          }
        });
  }

  private static JPanel createOverviewPanel() {
    JPanel panel = new JPanel(new BorderLayout(8, 8));
    panel.add(new JLabel("Experimental Acoustic Localization plugin"), BorderLayout.NORTH);
    JTextArea text = new JTextArea();
    text.setEditable(false);
    text.setLineWrap(true);
    text.setWrapStyleWord(true);
    text.setText(
        """
        This panel is contributed by the acoustic-localization plugin.

        The plugin provides experimental wingbeat frequency tracking, GCC-PHAT /
        cross-correlation TDOA estimators, delay-and-sum beamforming and a 2D
        room simulator. The tracking pipeline also estimates Doppler radial
        velocity, frequency shift and a smoothed velocity vector per source.
        A companion imported-recording workbench can load a local HumBugDB
        export, browse imported clips and replay feature extraction /
        classification on one selected recording.
        See docs/plugins/acoustic-localization.md for details and limitations.

        Plugin-specific views, analyzers and demo signals are loaded dynamically
        by the host through the audio-plugin-api ServiceLoader contract; the
        main application does not depend on this code at compile time.
        """);
    panel.add(new JScrollPane(text), BorderLayout.CENTER);
    return panel;
  }

  private static AnalysisContribution contribution(String id, String description) {
    return new AnalysisContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String description() {
        return description;
      }
    };
  }

  private static DemoSignalContribution demo(String id, String label, String description) {
    return new DemoSignalContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String label() {
        return label;
      }

      @Override
      public String description() {
        return description;
      }
    };
  }

  private static SignalSourceContribution signalSource(
      String id, String name, String description, String category) {
    return new SignalSourceContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String category() {
        return category;
      }
    };
  }

  private static ExperimentContribution experiment(
      String id, String name, String description, String category) {
    return new ExperimentContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String category() {
        return category;
      }
    };
  }

  private static PipelineContribution pipeline(
      String id, String name, String description, String stages) {
    return new PipelineContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String stages() {
        return stages;
      }
    };
  }

  private static SnapshotStreamContribution snapshotStream(
      String id, String name, String description, String snapshotTypeName) {
    return new SnapshotStreamContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String snapshotTypeName() {
        return snapshotTypeName;
      }
    };
  }

  private static VisualizationContribution visualization(
      String id, String name, String description, String renderKind) {
    return new VisualizationContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String renderKind() {
        return renderKind;
      }
    };
  }

  private static CalibrationContribution calibration(
      String id, String name, String description, String category) {
    return new CalibrationContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String category() {
        return category;
      }
    };
  }

  private static BenchmarkContribution benchmark(
      String id, String name, String description, String unit) {
    return new BenchmarkContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String description() {
        return description;
      }

      @Override
      public String unit() {
        return unit;
      }
    };
  }

  private static ExportFormatContribution exportFormat(
      String id, String name, String fileExtension, String description) {
    return new ExportFormatContribution() {
      @Override
      public String id() {
        return id;
      }

      @Override
      public String name() {
        return name;
      }

      @Override
      public String fileExtension() {
        return fileExtension;
      }

      @Override
      public String description() {
        return description;
      }
    };
  }
}