extern crate alloc;
use core::cmp::Ordering;
mod wang;
pub use wang::{WangMatchConfig, WangMatcher, WangRefIndex};
mod panako;
pub use panako::{PanakoMatchConfig, PanakoMatcher, PanakoRefIndex};
mod haitsma;
pub use haitsma::{HaitsmaMatchConfig, HaitsmaMatcher};
#[cfg(feature = "neural")]
mod neural;
#[cfg(feature = "neural")]
pub use neural::{Aggregation, NeuralMatchConfig, NeuralMatcher};
mod index;
pub use index::{HaitsmaIndex, PanakoIndex, WangIndex, match_best, match_ranked};
#[cfg(feature = "rayon")]
pub use index::{par_match_best, par_match_ranked};
mod maps;
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct TimeOffset {
pub frames: i64,
pub ms: i64,
}
impl TimeOffset {
#[must_use]
pub fn from_frames(frames: i64, frames_per_sec: f32) -> Self {
let ms = (frames as f64 * 1000.0 / frames_per_sec as f64).round() as i64;
Self { frames, ms }
}
pub const ZERO: TimeOffset = TimeOffset { frames: 0, ms: 0 };
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct MatchResult {
pub is_match: bool,
pub score: f32,
pub votes: u32,
pub prominence: f32,
pub offset: TimeOffset,
pub time_scale: f32,
}
impl MatchResult {
pub const NONE: MatchResult = MatchResult {
is_match: false,
score: 0.0,
votes: 0,
prominence: 0.0,
offset: TimeOffset::ZERO,
time_scale: 1.0,
};
}
pub trait Matcher {
type Fingerprint;
type Config: Clone + Send + Sync;
fn new(cfg: Self::Config) -> Self;
fn config(&self) -> &Self::Config;
fn match_one(&self, query: &Self::Fingerprint, reference: &Self::Fingerprint) -> MatchResult;
}
const FPS_REL_EPS: f32 = 1e-3;
#[inline]
#[must_use]
pub(crate) fn frames_per_sec_compatible(a: f32, b: f32) -> bool {
if !(a.is_finite() && b.is_finite()) || a <= 0.0 || b <= 0.0 {
return false;
}
let scale = a.abs().max(b.abs());
(a - b).abs() <= FPS_REL_EPS * scale
}
#[inline]
#[must_use]
pub fn score_compare(a: f32, b: f32) -> Ordering {
a.partial_cmp(&b).unwrap_or(Ordering::Equal)
}
#[inline]
#[must_use]
pub fn match_result_compare_desc(a: &MatchResult, b: &MatchResult) -> Ordering {
match score_compare(b.score, a.score) {
Ordering::Equal => score_compare(b.prominence, a.prominence),
other => other,
}
}
#[inline]
#[must_use]
pub fn clamp_score(s: f32) -> f32 {
s.clamp(0.0, 1.0)
}
#[inline]
#[must_use]
pub fn compute_prominence(values: &[u32], peak_idx: usize) -> f32 {
let peak = values[peak_idx] as f32;
if peak == 0.0 {
return 0.0;
}
let sum: u64 = values
.iter()
.enumerate()
.filter(|&(i, _)| i != peak_idx)
.map(|(_, &v)| v as u64)
.sum();
let rest_count = (values.len() - 1) as f32;
let mean_rest = if rest_count > 0.0 {
sum as f32 / rest_count
} else {
0.0
};
peak / (mean_rest + 1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn time_offset_zero() {
assert_eq!(TimeOffset::ZERO.frames, 0);
assert_eq!(TimeOffset::ZERO.ms, 0);
}
#[test]
fn time_offset_wang_framerate() {
let off = TimeOffset::from_frames(100, 62.5);
assert_eq!(off.frames, 100);
assert_eq!(off.ms, 1600); }
#[test]
fn time_offset_haitsma_framerate() {
let off = TimeOffset::from_frames(78, 78.125);
assert_eq!(off.frames, 78);
assert_eq!(off.ms, 998);
}
#[test]
fn time_offset_negative() {
let off = TimeOffset::from_frames(-50, 62.5);
assert_eq!(off.frames, -50);
assert_eq!(off.ms, -800);
}
#[test]
fn match_result_none_is_default() {
let n = MatchResult::NONE;
assert!(!n.is_match);
assert_eq!(n.score, 0.0);
assert_eq!(n.votes, 0);
assert_eq!(n.offset, TimeOffset::ZERO);
}
#[test]
fn score_compare_nan_is_equal() {
assert_eq!(score_compare(f32::NAN, f32::NAN), Ordering::Equal);
assert_eq!(score_compare(f32::NAN, 1.0), Ordering::Equal);
}
#[test]
fn clamp_score_bounds() {
assert_eq!(clamp_score(-0.5), 0.0);
assert_eq!(clamp_score(0.5), 0.5);
assert_eq!(clamp_score(1.5), 1.0);
}
#[test]
fn compute_prominence_clear_spike() {
let hist = [1, 1, 1, 100, 1, 1, 1, 1, 1, 1];
let p = compute_prominence(&hist, 3);
assert!(p > 5.0, "expected high prominence, got {p}");
}
#[test]
fn compute_prominence_flat() {
let hist = [5, 5, 5, 5, 5];
let p = compute_prominence(&hist, 2);
assert!(
p < 2.0,
"expected low prominence for flat histogram, got {p}"
);
}
#[test]
fn match_result_compare_desc_ranks_by_score() {
let a = MatchResult {
score: 0.8,
prominence: 1.0,
..MatchResult::NONE
};
let b = MatchResult {
score: 0.3,
prominence: 100.0,
..MatchResult::NONE
};
assert_eq!(match_result_compare_desc(&a, &b), Ordering::Less); }
#[test]
fn frames_per_sec_compatible_accepts_equal() {
assert!(frames_per_sec_compatible(62.5, 62.5));
assert!(frames_per_sec_compatible(78.125, 78.125));
}
#[test]
fn frames_per_sec_compatible_rejects_mismatch() {
assert!(!frames_per_sec_compatible(62.5, 31.25));
assert!(!frames_per_sec_compatible(62.5, f32::NAN));
assert!(!frames_per_sec_compatible(-1.0, 62.5));
assert!(!frames_per_sec_compatible(0.0, 62.5));
}
}