"""Tests for audio analysis module.""" import subprocess import sys from pathlib import Path import pytest import numpy as np sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src")) from analysis import analyze_clip, detect_bpm, detect_key, MAJOR_PROFILE, MINOR_PROFILE, KEY_NAMES class TestKeyProfiles: """Verify the Krumhansl-Schmuckler key profiles are well-formed.""" def test_major_profile_has_12_entries(self): assert len(MAJOR_PROFILE) == 12 def test_minor_profile_has_12_entries(self): assert len(MINOR_PROFILE) == 12 def test_key_names_has_24_entries(self): assert len(KEY_NAMES) == 24 # 12 major + 12 minor majors = [k for k in KEY_NAMES if "major" in k] minors = [k for k in KEY_NAMES if "minor" in k] assert len(majors) == 12 assert len(minors) == 12 class TestDetectBpm: """BPM detection tests.""" def test_detect_bpm_on_silent_file_raises(self, tmp_path): """A file that's not valid audio should raise an error from librosa.""" fake_audio = tmp_path / "silent.mp3" fake_audio.write_bytes(b"\x00" * 1024) with pytest.raises(Exception): # librosa will fail on invalid audio data detect_bpm(str(fake_audio)) @pytest.mark.skipif( subprocess.run(["ffmpeg", "-version"], capture_output=True).returncode != 0, reason="ffmpeg not available" ) def test_detect_bpm_on_generated_audio(self, tmp_path): """Generate a simple 120 BPM click track and verify BPM detection.""" mp3_path = tmp_path / "test_bpm.mp3" # Generate a 5-second audio file with ffmpeg # Using a sine wave at 440Hz — BPM detection should return a numeric value subprocess.run( [ "ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=5", "-q:a", "9", str(mp3_path) ], capture_output=True, check=True, ) bpm = detect_bpm(str(mp3_path)) # BPM detection on a pure sine might not be perfectly accurate, # but it should return a positive float # BPM detection on a pure sine returns 0.0 (no beats) # This is expected — a pure tone has no rhythmic content assert isinstance(bpm, float) assert bpm >= 0.0 class TestDetectKey: """Key detection tests.""" @pytest.mark.skipif( subprocess.run(["ffmpeg", "-version"], capture_output=True).returncode != 0, reason="ffmpeg not available" ) def test_detect_key_returns_valid_key_name(self, tmp_path): mp3_path = tmp_path / "test_key.mp3" subprocess.run( [ "ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=5", "-q:a", "9", str(mp3_path) ], capture_output=True, check=True, ) key = detect_key(str(mp3_path)) assert key in KEY_NAMES, f"Detected key '{key}' not in KEY_NAMES" class TestAnalyzeClip: """Full analysis integration test.""" def test_analyze_missing_file_raises(self, tmp_path): with pytest.raises(FileNotFoundError): analyze_clip(str(tmp_path / "nonexistent.mp3")) @pytest.mark.skipif( subprocess.run(["ffmpeg", "-version"], capture_output=True).returncode != 0, reason="ffmpeg not available" ) def test_analyze_clip_returns_bpm_and_key(self, tmp_path): mp3_path = tmp_path / "test_analyze.mp3" subprocess.run( [ "ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=5", "-q:a", "9", str(mp3_path) ], capture_output=True, check=True, ) result = analyze_clip(str(mp3_path)) assert "bpm" in result assert "key" in result assert isinstance(result["bpm"], float) assert result["bpm"] >= 0.0 # Pure sine has no beats, BPM may be 0 assert result["key"] in KEY_NAMES