Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ def analyze(self, audio_path: str | Path) -> TemporalFeatures:
)

with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", category=DeprecationWarning, module=r"^audioread"
)
warnings.filterwarnings("ignore", category=FutureWarning, module=r"^audioread")

# Keep the loader's known third-party churn quiet without hiding
# unrelated decoder warnings that tests and callers should see.
for category, message, module in KNOWN_LIBROSA_NUMBA_WARNING_FILTERS:
Expand Down
56 changes: 22 additions & 34 deletions services/analysis-engine/tests/test_chord_recognizer.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
"""Tests for the chord recognizer module."""

import warnings
from unittest.mock import patch

import numpy as np

from bandscope_analysis.chords.chord_recognizer import ChordRecognizer

SAMPLE_RATE = 22050
DURATION_SECONDS = 3


def test_chord_recognizer_empty_audio() -> None:
"""Test chord recognition with empty audio array."""
Expand All @@ -20,31 +22,17 @@ def test_chord_recognizer_unvoiced_audio() -> None:
recognizer = ChordRecognizer()
# Create random noise
np.random.seed(42)
y = np.random.randn(22050 * 3) * 0.1
result = recognizer.recognize(y, sr=22050)
y = np.random.randn(SAMPLE_RATE * 2) * 0.1
result = recognizer.recognize(y, sr=SAMPLE_RATE)
# Could be N (No chord) or empty
assert all(chord["chord"] in ("N", "Unknown", "") for chord in result) if result else True


def test_chord_recognizer_short_audio_does_not_emit_fft_warnings() -> None:
"""Short clips should not leak librosa FFT-size warnings."""
recognizer = ChordRecognizer()
np.random.seed(42)
y = np.random.randn(22050) * 0.1

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
result = recognizer.recognize(y, sr=22050)

assert isinstance(result, list)
assert not [warning for warning in caught if "n_fft" in str(warning.message)]


def test_chord_recognizer_c_major_chord() -> None:
"""Test chord recognition with a clear C major chord."""
recognizer = ChordRecognizer()
sr = 22050
t = np.linspace(0, 3.0, sr * 3)
sr = SAMPLE_RATE
t = np.linspace(0, DURATION_SECONDS, sr * DURATION_SECONDS, endpoint=False)
# C major: C4 (261.63Hz), E4 (329.63Hz), G4 (392.00Hz)
y = (
np.sin(2 * np.pi * 261.63 * t)
Expand All @@ -62,86 +50,86 @@ def test_chord_recognizer_c_major_chord() -> None:
def test_chord_recognizer_hpss_exception() -> None:
"""Test for test_chord_recognizer_hpss_exception."""
recognizer = ChordRecognizer()
y = np.random.randn(22050 * 3)
y = np.random.randn(SAMPLE_RATE * DURATION_SECONDS)

with patch("librosa.effects.hpss", side_effect=Exception("HPSS Error")):
chords = recognizer.recognize(y, sr=22050)
chords = recognizer.recognize(y, sr=SAMPLE_RATE)
assert isinstance(chords, list)


def test_chord_recognizer_chroma_cqt_exception() -> None:
"""Test for test_chord_recognizer_chroma_cqt_exception."""
recognizer = ChordRecognizer()
y = np.random.randn(22050 * 3)
y = np.random.randn(SAMPLE_RATE * DURATION_SECONDS)

with patch("librosa.feature.chroma_cqt", side_effect=Exception("CQT Error")):
chords = recognizer.recognize(y, sr=22050)
chords = recognizer.recognize(y, sr=SAMPLE_RATE)
assert chords == []


def test_chord_recognizer_rms_exception() -> None:
"""Test for test_chord_recognizer_rms_exception."""
recognizer = ChordRecognizer()
y = np.random.randn(22050 * 3)
y = np.random.randn(SAMPLE_RATE * DURATION_SECONDS)

with patch("librosa.feature.rms", side_effect=Exception("RMS Error")):
chords = recognizer.recognize(y, sr=22050)
chords = recognizer.recognize(y, sr=SAMPLE_RATE)
assert isinstance(chords, list)


def test_chord_recognizer_rms_padding() -> None:
"""Test for test_chord_recognizer_rms_padding."""
recognizer = ChordRecognizer()
y = np.random.randn(22050 * 3)
y = np.random.randn(SAMPLE_RATE * DURATION_SECONDS)

# Mock RMS to return something shorter than chromagram
def mock_rms(*args, **kwargs):
return np.array([[0.1, 0.1]])

with patch("librosa.feature.rms", side_effect=mock_rms):
chords = recognizer.recognize(y, sr=22050)
chords = recognizer.recognize(y, sr=SAMPLE_RATE)
assert isinstance(chords, list)


def test_chord_recognizer_empty_chromagram() -> None:
"""Test for test_chord_recognizer_empty_chromagram."""
recognizer = ChordRecognizer()
y = np.random.randn(22050 * 3)
y = np.random.randn(SAMPLE_RATE * DURATION_SECONDS)

# Mock chroma_cqt to return empty array
with patch("librosa.feature.chroma_cqt", return_value=np.array([])):
chords = recognizer.recognize(y, sr=22050)
chords = recognizer.recognize(y, sr=SAMPLE_RATE)
assert chords == []


def test_chord_recognizer_rms_longer() -> None:
"""Test for test_chord_recognizer_rms_longer."""
recognizer = ChordRecognizer()
y = np.random.randn(22050 * 3)
y = np.random.randn(SAMPLE_RATE * DURATION_SECONDS)

# Mock RMS to return something longer than chromagram
def mock_rms(*args, **kwargs):
# Return a very long array
return np.array([np.ones(1000)])

with patch("librosa.feature.rms", side_effect=mock_rms):
chords = recognizer.recognize(y, sr=22050)
chords = recognizer.recognize(y, sr=SAMPLE_RATE)
assert isinstance(chords, list)


def test_chord_recognizer_changing_chords() -> None:
"""Test for test_chord_recognizer_changing_chords."""
recognizer = ChordRecognizer()
sr = 22050
t1 = np.linspace(0, 1.5, int(sr * 1.5), endpoint=False)
sr = SAMPLE_RATE
t1 = np.linspace(0, DURATION_SECONDS, sr * DURATION_SECONDS, endpoint=False)
# C major
y1 = (
np.sin(2 * np.pi * 261.63 * t1)
+ np.sin(2 * np.pi * 329.63 * t1)
+ np.sin(2 * np.pi * 392.00 * t1)
) / 3.0

t2 = np.linspace(0, 1.5, int(sr * 1.5), endpoint=False)
t2 = np.linspace(0, DURATION_SECONDS, sr * DURATION_SECONDS, endpoint=False)
# G major: G4 (392.00Hz), B4 (493.88Hz), D5 (587.33Hz)
y2 = (
np.sin(2 * np.pi * 392.00 * t2)
Expand Down
Loading