/
/
/
1"""Tests for the band-power signal module (bar fractions on the deck's own grid)."""
2
3from __future__ import annotations
4
5import numpy as np
6import pytest
7
8from music_assistant.controllers.streams.smart_fades.bands import (
9 build_band_profile,
10 instrumental_claim_confirmed,
11 loudness_referenced_level,
12 smoothstep,
13 window_duty,
14 window_fraction,
15)
16from music_assistant.controllers.streams.smart_fades.models import BandProfile
17from music_assistant.controllers.streams.smart_fades.vocal import VocalMask
18from tests.controllers.streams.smart_fades.conftest import _analysis_with_bands
19
20
21class TestBandProfile:
22 """Power fractions on the bar grid, invariant to the peak normalization."""
23
24 def test_fractions_are_power_not_amplitude(self) -> None:
25 """Equal amplitudes in two bands give 0.5 power fraction each."""
26 a = _analysis_with_bands(0.3, 0.3, 0.0, 0.0)
27 p = build_band_profile(a)
28 assert p is not None
29 assert window_fraction(p, "low", 0.0, 60.0) == pytest.approx(0.5, abs=1e-6)
30
31 def test_normalization_cancels(self) -> None:
32 """Scaling all envelopes by a constant leaves fractions unchanged."""
33 a1 = _analysis_with_bands(0.4, 0.1, 0.2, 0.05)
34 a2 = _analysis_with_bands(0.2, 0.05, 0.1, 0.025)
35 p1, p2 = build_band_profile(a1), build_band_profile(a2)
36 assert p1 is not None
37 assert p2 is not None
38 f1 = window_fraction(p1, "mid", 0.0, 60.0)
39 f2 = window_fraction(p2, "mid", 0.0, 60.0)
40 assert f1 == pytest.approx(f2, abs=1e-6)
41
42 def test_missing_band_rms_returns_none(self) -> None:
43 """v1 rows (no extra_data) yield None so policies bypass."""
44 a = _analysis_with_bands(0.3, 0.1, 0.1, 0.05)
45 a.extra_data = None
46 assert build_band_profile(a) is None
47
48 def test_all_silent_track_returns_none(self) -> None:
49 """All-zero envelopes have no active bars, so the profile is None."""
50 assert build_band_profile(_analysis_with_bands(0.0, 0.0, 0.0, 0.0)) is None
51
52 def test_too_few_downbeats_returns_none(self) -> None:
53 """Fewer than 8 downbeats leaves no usable bar grid, so the profile is None."""
54 a = _analysis_with_bands(0.3, 0.1, 0.1, 0.05)
55 assert a.downbeats is not None
56 a.downbeats = a.downbeats[:7]
57 assert build_band_profile(a) is None
58
59 def test_falsy_duration_returns_none(self) -> None:
60 """A zero/missing duration leaves no bin-to-second mapping, so the profile is None."""
61 a = _analysis_with_bands(0.3, 0.1, 0.1, 0.05)
62 a.duration = 0.0
63 assert build_band_profile(a) is None
64
65 def test_empty_window_is_zero(self) -> None:
66 """A window past the track end returns 0.0, never NaN."""
67 p = build_band_profile(_analysis_with_bands(0.3, 0.1, 0.1, 0.05))
68 assert p is not None
69 assert window_fraction(p, "low", 500.0, 600.0) == 0.0
70 assert window_duty(p, "low", 500.0, 600.0) == 0.0
71
72 def test_duty_counts_bars_above_reference_fraction(self) -> None:
73 """Duty = share of window bars at >= k x the track's sustained band power."""
74 a = _analysis_with_bands(0.3, 0.1, 0.1, 0.05)
75 assert a.extra_data is not None
76 bands = a.extra_data["band_rms"]
77 bands["mid"] = ([0.0] * 900) + ([0.4] * 900) # mid silent first half
78 p = build_band_profile(a)
79 assert p is not None
80 assert window_duty(p, "mid", 0.0, 120.0) == pytest.approx(0.0, abs=0.05)
81 assert window_duty(p, "mid", 120.0, 240.0) == pytest.approx(1.0, abs=0.05)
82
83
84class TestSmoothstep:
85 """The only depth-shaping primitive: zero at threshold, saturating, continuous."""
86
87 def test_endpoints_and_midpoint(self) -> None:
88 """Exactly 0 below lo, 1 above hi, 0.5 at the midpoint."""
89 assert smoothstep(0.09, 0.10, 0.25) == 0.0
90 assert smoothstep(0.30, 0.10, 0.25) == 1.0
91 assert smoothstep(0.175, 0.10, 0.25) == pytest.approx(0.5)
92
93
94class TestLoudnessReferencedLevel:
95 """Level normalized by the track's own active-bar total power."""
96
97 def test_normalization_cancels(self) -> None:
98 """Scaling all envelopes by a constant leaves the referenced level unchanged."""
99 a1 = _analysis_with_bands(0.4, 0.1, 0.2, 0.05)
100 a2 = _analysis_with_bands(0.2, 0.05, 0.1, 0.025)
101 p1, p2 = build_band_profile(a1), build_band_profile(a2)
102 assert p1 is not None
103 assert p2 is not None
104 r1 = loudness_referenced_level(p1, "mid", 0.0, 60.0)
105 r2 = loudness_referenced_level(p2, "mid", 0.0, 60.0)
106 assert r1 == pytest.approx(r2, abs=1e-6)
107
108 def test_louder_window_scores_higher(self) -> None:
109 """A window with more low-band power than the track average scores > 1."""
110 a = _analysis_with_bands(0.1, 0.1, 0.1, 0.1)
111 assert a.extra_data is not None
112 bands = a.extra_data["band_rms"]
113 bands["low"] = ([0.1] * 900) + ([0.5] * 900) # louder low band, second half
114 p = build_band_profile(a)
115 assert p is not None
116 quiet = loudness_referenced_level(p, "low", 0.0, 120.0)
117 loud = loudness_referenced_level(p, "low", 120.0, 240.0)
118 assert loud > quiet
119
120
121class TestInstrumentalClaimConfirmed:
122 """The MIR cross-check: no VAD-silent bar in the claimed region carries elevated mid power."""
123
124 def _profile(self, *, elevated_spike: bool) -> BandProfile:
125 """Build a profile whose mid band is loud before 120s, quiet from 120s (the region)."""
126 t = np.linspace(0.0, 240.0, 1800)
127 mid = np.where(t < 120.0, 0.5, 0.02).astype(np.float32)
128 if elevated_spike:
129 mid[(t >= 140.0) & (t < 142.0)] = 0.5
130 low = np.full(1800, 0.05, dtype=np.float32)
131 profile = build_band_profile(_analysis_with_bands(low, low, mid, low, duration=240.0))
132 assert profile is not None
133 return profile
134
135 def test_quiet_vad_silent_region_confirms(self) -> None:
136 """A genuinely quiet mid band across the region confirms the instrumental claim."""
137 profile = self._profile(elevated_spike=False)
138 assert instrumental_claim_confirmed(profile, VocalMask(windows=[]), 120.0, 240.0)
139
140 def test_elevated_bar_inside_vad_silent_region_denies(self) -> None:
141 """One elevated mid bar inside the VAD-silent region denies the instrumental claim."""
142 profile = self._profile(elevated_spike=True)
143 assert not instrumental_claim_confirmed(profile, VocalMask(windows=[]), 120.0, 240.0)
144
145 def test_elevated_bar_inside_a_vocal_window_is_exempt(self) -> None:
146 """An elevated bar that IS vocal-active carries no evidence against the claim."""
147 profile = self._profile(elevated_spike=True)
148 mask = VocalMask(windows=[(140.0, 142.0)])
149 assert instrumental_claim_confirmed(profile, mask, 120.0, 240.0)
150
151 def test_empty_range_confirms(self) -> None:
152 """A region with no bars at all vacuously confirms the claim."""
153 profile = self._profile(elevated_spike=False)
154 assert instrumental_claim_confirmed(profile, VocalMask(windows=[]), 500.0, 600.0)
155