Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,229 @@
"""Tests for the AudioFingerprinter chromagram + constellation pipeline."""
from __future__ import annotations
import tempfile
from pathlib import Path
import numpy as np
import pytest
import soundfile as sf
from src.fingerprint import AudioFingerprinter
@pytest.fixture
def fingerprinter() -> AudioFingerprinter:
return AudioFingerprinter()
def test_fingerprinter_initialization() -> None:
fp = AudioFingerprinter()
assert fp.sample_rate == 22050
assert fp.n_fft == 2048
assert fp.hop_length == 512
assert fp.n_chroma == 12
def test_fingerprinter_custom_parameters() -> None:
fp = AudioFingerprinter(
sample_rate=44100, n_fft=4096, hop_length=1024, n_chroma=24
)
assert fp.sample_rate == 44100
assert fp.n_fft == 4096
assert fp.hop_length == 1024
assert fp.n_chroma == 24
def test_load_audio(fingerprinter: AudioFingerprinter, audio_file: Path) -> None:
audio = fingerprinter.load_audio(audio_file)
assert isinstance(audio, np.ndarray)
assert audio.dtype == np.float32
assert len(audio) > 0
assert -1.0 <= audio.min() <= 1.0
assert -1.0 <= audio.max() <= 1.0
def test_load_audio_nonexistent_file(fingerprinter: AudioFingerprinter) -> None:
with pytest.raises(FileNotFoundError, match="Audio file not found"):
fingerprinter.load_audio("/nonexistent/file.wav")
def test_load_audio_invalid_data_raises_runtime_error(
fingerprinter: AudioFingerprinter,
) -> None:
"""A file that exists but isn't decodable must surface as RuntimeError."""
with tempfile.NamedTemporaryFile(
suffix=".wav", delete=False, mode="wb"
) as tmp:
tmp.write(b"this is not a wav file at all")
bad_path = Path(tmp.name)
try:
with pytest.raises(RuntimeError, match="Failed to load audio file"):
fingerprinter.load_audio(bad_path)
finally:
bad_path.unlink(missing_ok=True)
def test_extract_chromagram(
fingerprinter: AudioFingerprinter,
sample_audio: tuple[np.ndarray, int],
) -> None:
audio, _ = sample_audio
chromagram = fingerprinter.extract_chromagram(audio)
assert isinstance(chromagram, np.ndarray)
assert chromagram.shape[0] == fingerprinter.n_chroma
assert chromagram.shape[1] > 0
assert chromagram.min() >= 0.0
def test_find_peaks(
fingerprinter: AudioFingerprinter,
sample_audio: tuple[np.ndarray, int],
) -> None:
audio, _ = sample_audio
chromagram = fingerprinter.extract_chromagram(audio)
peaks = fingerprinter.find_peaks(chromagram, threshold=0.5)
assert isinstance(peaks, list)
assert len(peaks) > 0
for time, chroma, amplitude in peaks:
assert isinstance(time, int)
assert isinstance(chroma, int)
assert isinstance(amplitude, float)
assert 0 <= chroma < fingerprinter.n_chroma
assert 0.0 <= amplitude <= 1.0
def test_find_peaks_threshold(
fingerprinter: AudioFingerprinter,
sample_audio: tuple[np.ndarray, int],
) -> None:
audio, _ = sample_audio
chromagram = fingerprinter.extract_chromagram(audio)
peaks_low = fingerprinter.find_peaks(chromagram, threshold=0.3)
peaks_high = fingerprinter.find_peaks(chromagram, threshold=0.7)
assert len(peaks_low) >= len(peaks_high)
def test_find_peaks_zero_magnitude(fingerprinter: AudioFingerprinter) -> None:
"""A zero chromagram must return an empty peak list."""
zero_chroma = np.zeros((fingerprinter.n_chroma, 50), dtype=np.float32)
assert fingerprinter.find_peaks(zero_chroma) == []
def test_generate_fingerprint(
fingerprinter: AudioFingerprinter,
sample_audio: tuple[np.ndarray, int],
) -> None:
audio, _ = sample_audio
chromagram = fingerprinter.extract_chromagram(audio)
peaks = fingerprinter.find_peaks(chromagram)
fingerprints = fingerprinter.generate_fingerprint(peaks, fan_value=15)
assert isinstance(fingerprints, set)
assert len(fingerprints) > 0
for fp_hash, time, delta in fingerprints:
assert isinstance(fp_hash, int)
assert isinstance(time, int)
assert isinstance(delta, int)
assert time >= 0
assert delta >= 0
def test_generate_fingerprint_fan_value(
fingerprinter: AudioFingerprinter,
sample_audio: tuple[np.ndarray, int],
) -> None:
audio, _ = sample_audio
chromagram = fingerprinter.extract_chromagram(audio)
peaks = fingerprinter.find_peaks(chromagram)
fps_small = fingerprinter.generate_fingerprint(peaks, fan_value=5)
fps_large = fingerprinter.generate_fingerprint(peaks, fan_value=20)
assert len(fps_large) >= len(fps_small)
def test_fingerprint_file(
fingerprinter: AudioFingerprinter, audio_file: Path
) -> None:
result = fingerprinter.fingerprint_file(audio_file)
assert isinstance(result, dict)
for key in ("fingerprints", "duration", "peaks_count", "fingerprints_count"):
assert key in result
assert isinstance(result["fingerprints"], set)
assert len(result["fingerprints"]) > 0
assert result["duration"] > 0.0
assert result["peaks_count"] > 0
assert result["fingerprints_count"] == len(result["fingerprints"])
def test_match_fingerprints_identical(
fingerprinter: AudioFingerprinter, audio_file: Path
) -> None:
result1 = fingerprinter.fingerprint_file(audio_file)
result2 = fingerprinter.fingerprint_file(audio_file)
match = fingerprinter.match_fingerprints(
result1["fingerprints"], result2["fingerprints"], min_matches=10
)
assert match["matched"] is True
assert match["match_count"] > 0
assert match["confidence"] > 0.9
assert abs(match["time_offset"]) <= 5
def test_match_fingerprints_min_matches(
fingerprinter: AudioFingerprinter, audio_file: Path
) -> None:
result1 = fingerprinter.fingerprint_file(audio_file)
result2 = fingerprinter.fingerprint_file(audio_file)
match_reasonable = fingerprinter.match_fingerprints(
result1["fingerprints"], result2["fingerprints"], min_matches=10
)
match_impossible = fingerprinter.match_fingerprints(
result1["fingerprints"], result2["fingerprints"], min_matches=999999
)
assert match_reasonable["matched"] is True
assert match_impossible["matched"] is False
assert match_impossible["time_offset"] == 0
def test_match_fingerprints_empty(fingerprinter: AudioFingerprinter) -> None:
"""Empty inputs must yield zero match_count, zero confidence, zero offset."""
match = fingerprinter.match_fingerprints(set(), set(), min_matches=10)
assert match["match_count"] == 0
assert match["confidence"] == 0
assert match["time_offset"] == 0
assert match["matched"] is False
def test_match_fingerprints_with_offset(
fingerprinter: AudioFingerprinter,
) -> None:
"""When matches exist, the median time offset between hashes is reported."""
query = {(1001, 0, 3), (1002, 10, 4), (1003, 20, 5)}
reference = {(1001, 100, 3), (1002, 110, 4), (1003, 120, 5)}
match = fingerprinter.match_fingerprints(query, reference, min_matches=2)
assert match["matched"] is True
assert match["match_count"] == 3
assert match["time_offset"] == 100
def test_match_fingerprints_partial_overlap(
fingerprinter: AudioFingerprinter,
) -> None:
"""Query hashes not present in reference must be skipped without crashing."""
query = {(1001, 0, 3), (9999, 10, 4), (1003, 20, 5)}
reference = {(1001, 50, 3), (1003, 70, 5), (4242, 999, 9)}
match = fingerprinter.match_fingerprints(query, reference, min_matches=2)
assert match["match_count"] == 2
assert match["matched"] is True
# Offsets are 50 and 50 -> median 50.
assert match["time_offset"] == 50
def test_match_fingerprints_zero_threshold_empty_inputs(
fingerprinter: AudioFingerprinter,
) -> None:
"""min_matches=0 with empty inputs must not attempt median on empty list."""
match = fingerprinter.match_fingerprints(set(), set(), min_matches=0)
assert match["match_count"] == 0
assert match["matched"] is True # 0 >= 0
assert match["time_offset"] == 0