diff --git a/.github/workflows/nightly-hypothesis.yml b/.github/workflows/nightly-hypothesis.yml new file mode 100644 index 0000000..433aad7 --- /dev/null +++ b/.github/workflows/nightly-hypothesis.yml @@ -0,0 +1,32 @@ +name: Nightly Hypothesis + +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +permissions: + contents: read + +jobs: + property-tests: + name: Property tests (randomized nightly profile) + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: true + + - name: Install Python 3.12 + run: uv python install 3.12 + + - name: Install dependencies + run: uv sync --locked --python 3.12 --extra dev + + - name: Run property-based tests (randomized, 200 examples) + env: + HYPOTHESIS_PROFILE: nightly + run: uv run pytest tests/test_invariants.py -q --tb=short diff --git a/pyproject.toml b/pyproject.toml index 7bc620a..61e395c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,6 +50,7 @@ dev = [ "pytest-asyncio>=1.0", "pytest-timeout>=2.4", "pyloudnorm>=0.1.0", + "hypothesis>=6.0", "ruff>=0.15", "pre-commit>=4.0", ] diff --git a/tests/conftest.py b/tests/conftest.py index 65ef76e..a7be447 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -4,12 +4,41 @@ No WAV files are committed to the repository (D-11, D-12). """ +import os from pathlib import Path import numpy as np import pytest import scipy.signal as _sig import soundfile as sf +from hypothesis import HealthCheck, settings + +# -- Hypothesis profiles (issue #17) ------------------------------------------ +# +# "ci" (default): derandomized so every run replays the same examples -- +# deterministic in CI and locally, satisfying the reproducibility requirement. +# "nightly": randomized with a larger example budget for broader coverage, +# selected via HYPOTHESIS_PROFILE=nightly (see nightly-hypothesis.yml). +# +# function_scoped_fixture is suppressed because the audio fixtures used by +# property tests return immutable (samples, sr) value tuples that are never +# mutated, so reusing them across generated examples is safe. + +settings.register_profile( + "ci", + derandomize=True, + max_examples=20, + deadline=None, + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +settings.register_profile( + "nightly", + derandomize=False, + max_examples=200, + deadline=None, + suppress_health_check=[HealthCheck.function_scoped_fixture], +) +settings.load_profile(os.environ.get("HYPOTHESIS_PROFILE", "ci")) @pytest.fixture(autouse=True) diff --git a/tests/test_invariants.py b/tests/test_invariants.py new file mode 100644 index 0000000..d89f09e --- /dev/null +++ b/tests/test_invariants.py @@ -0,0 +1,225 @@ +"""Property-based invariant tests for the analysis modules (issue #17). + +Uses hypothesis to verify algorithmic invariants that must hold for *any* +valid input, complementing the example-based tests elsewhere in the suite: + +- Amplitude scaling shifts integrated LUFS by exactly 20*log10(scale). +- Mono duplicated to stereo: correlation == 1.0, width == 0, balance == 0. +- Polarity-flipped right channel: correlation == -1.0, polarity flag set. +- Time reversal leaves the magnitude spectrum (and stereo correlation) + unchanged within measurement precision. +- A DC-offset signal is always flagged by detect_problems. +- Leading silence is excluded by EBU R128 gating, so integrated LUFS and + true peak are unchanged. + +Hypothesis profiles are registered in conftest.py: the default "ci" +profile is derandomized (deterministic, reproducible in CI); set +HYPOTHESIS_PROFILE=nightly for randomized, broader coverage. + +All signals are generated in-memory (no audio files committed) and stay +well above the -80 dBFS near-silence guard. +""" + +import numpy as np +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from phantom.audio import AudioData +from phantom.loudness import analyze_loudness +from phantom.phase import analyze_phase +from phantom.problems import detect_problems +from phantom.spectral import analyze_spectrum +from phantom.stereo import analyze_stereo + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_mono(samples_1d: np.ndarray, sr: int) -> AudioData: + """Wrap a 1D mono signal into an AudioData instance.""" + samples_2d = samples_1d.reshape(-1, 1).astype(np.float32) + return AudioData( + samples=samples_2d, + sample_rate=sr, + num_channels=1, + duration=len(samples_1d) / sr, + num_samples=len(samples_1d), + ) + + +def _make_stereo(left: np.ndarray, right: np.ndarray, sr: int) -> AudioData: + """Wrap two 1D channel signals into a stereo AudioData instance.""" + samples_2d = np.column_stack([left, right]).astype(np.float32) + return AudioData( + samples=samples_2d, + sample_rate=sr, + num_channels=2, + duration=len(left) / sr, + num_samples=len(left), + ) + + +def _sine(freq: float, amp: float, duration: float = 1.0, sr: int = 44100): + """Generate a mono sine wave as a float32 array.""" + t = np.linspace(0, duration, int(sr * duration), endpoint=False, dtype=np.float32) + return (amp * np.sin(2 * np.pi * freq * t)).astype(np.float32), sr + + +# --------------------------------------------------------------------------- +# Loudness: amplitude scaling is exactly logarithmic +# --------------------------------------------------------------------------- + + +@given(scale=st.floats(min_value=0.1, max_value=0.9)) +def test_loudness_scales_logarithmically(scale, sine_1khz_minus23lufs): + """Scaling a signal by s shifts integrated LUFS by 20*log10(s).""" + samples, sr = sine_1khz_minus23lufs + base = analyze_loudness(_make_mono(samples, sr)) + scaled = analyze_loudness(_make_mono(samples * scale, sr)) + + assert base.integrated_lufs is not None + assert scaled.integrated_lufs is not None + expected_delta = 20 * np.log10(scale) + measured_delta = scaled.integrated_lufs - base.integrated_lufs + assert abs(measured_delta - expected_delta) < 0.1 + + +# --------------------------------------------------------------------------- +# Stereo / phase: identical channels +# --------------------------------------------------------------------------- + + +@given( + freq=st.floats(min_value=50.0, max_value=8000.0), + amp=st.floats(min_value=0.05, max_value=0.9), +) +def test_identical_channels_full_correlation(freq, amp): + """Mono duplicated to stereo: correlation 1.0, width 0, balance 0.""" + mono, sr = _sine(freq, amp) + audio = _make_stereo(mono, mono.copy(), sr) + + stereo = analyze_stereo(audio) + assert stereo.correlation == 1.0 + assert stereo.stereo_width == 0.0 + assert stereo.balance_db == 0.0 + + phase = analyze_phase(audio) + assert phase.phase_correlation == 1.0 + assert phase.polarity_inverted is False + + +@given( + freq=st.floats(min_value=50.0, max_value=8000.0), + amp=st.floats(min_value=0.05, max_value=0.9), +) +def test_polarity_flip_inverts_correlation(freq, amp): + """R = -L: correlation -1.0 and the polarity flag is set.""" + mono, sr = _sine(freq, amp) + audio = _make_stereo(mono, -mono, sr) + + stereo = analyze_stereo(audio) + assert stereo.correlation == -1.0 + + phase = analyze_phase(audio) + assert phase.phase_correlation == pytest.approx(-1.0, abs=1e-3) + assert phase.polarity_inverted is True + + +# --------------------------------------------------------------------------- +# Time reversal: magnitude spectrum and correlation are invariant +# --------------------------------------------------------------------------- + + +@given( + seed=st.integers(min_value=0, max_value=2**32 - 1), + freq=st.floats(min_value=100.0, max_value=5000.0), +) +def test_time_reversal_preserves_spectrum(seed, freq): + """Reversing a signal leaves octave-band energies and centroid intact. + + A 2-second signal keeps frame-boundary effects (zero-padded edge frames + differ between forward and reversed analysis) well below the tolerance. + """ + sr = 44100 + rng = np.random.default_rng(seed) + tone, _ = _sine(freq, 0.4, duration=2.0, sr=sr) + noise = (rng.standard_normal(len(tone)) * 0.1).astype(np.float32) + signal = tone + noise + + forward = analyze_spectrum(_make_mono(signal, sr)) + reversed_ = analyze_spectrum(_make_mono(signal[::-1].copy(), sr)) + + assert forward.octave_band_energy_db is not None + assert reversed_.octave_band_energy_db is not None + assert ( + forward.octave_band_energy_db.keys() == reversed_.octave_band_energy_db.keys() + ) + for band, fwd_db in forward.octave_band_energy_db.items(): + rev_db = reversed_.octave_band_energy_db[band] + assert abs(fwd_db - rev_db) < 1.0, f"band {band}: {fwd_db} vs {rev_db}" + + assert forward.spectral_centroid_hz is not None + assert reversed_.spectral_centroid_hz is not None + assert abs(forward.spectral_centroid_hz - reversed_.spectral_centroid_hz) <= ( + 0.02 * forward.spectral_centroid_hz + ) + + +@given(seed=st.integers(min_value=0, max_value=2**32 - 1)) +def test_time_reversal_preserves_stereo_correlation(seed): + """Pearson correlation is permutation-invariant, so reversal preserves it.""" + sr = 44100 + rng = np.random.default_rng(seed) + left = (rng.standard_normal(sr) * 0.3).astype(np.float32) + other = (rng.standard_normal(sr) * 0.3).astype(np.float32) + right = (0.6 * left + 0.4 * other).astype(np.float32) + + forward = analyze_stereo(_make_stereo(left, right, sr)) + reversed_ = analyze_stereo(_make_stereo(left[::-1].copy(), right[::-1].copy(), sr)) + + assert forward.correlation is not None + assert reversed_.correlation is not None + assert forward.correlation == pytest.approx(reversed_.correlation, abs=1e-3) + + +# --------------------------------------------------------------------------- +# Problem detection: DC offset is always flagged +# --------------------------------------------------------------------------- + + +@given(dc=st.floats(min_value=0.005, max_value=0.2)) +def test_dc_offset_always_detected(dc): + """Any DC offset well above the 5e-4 threshold is flagged.""" + mono, sr = _sine(440.0, 0.3) + audio = _make_mono(mono + np.float32(dc), sr) + + result = detect_problems(audio) + problem_types = [p.type for p in result.problems] + assert "dc_offset" in problem_types + + +# --------------------------------------------------------------------------- +# Loudness gating: leading silence is excluded +# --------------------------------------------------------------------------- + + +@given(silence_s=st.floats(min_value=0.5, max_value=3.0)) +def test_leading_silence_does_not_shift_integrated_lufs( + silence_s, sine_1khz_minus23lufs +): + """EBU R128 gating excludes silent blocks: integrated LUFS is unchanged.""" + samples, sr = sine_1khz_minus23lufs + base = analyze_loudness(_make_mono(samples, sr)) + + padded = np.concatenate([np.zeros(int(silence_s * sr), dtype=np.float32), samples]) + with_silence = analyze_loudness(_make_mono(padded, sr)) + + assert base.integrated_lufs is not None + assert with_silence.integrated_lufs is not None + assert abs(with_silence.integrated_lufs - base.integrated_lufs) < 0.5 + + assert base.true_peak_dbtp is not None + assert with_silence.true_peak_dbtp is not None + assert abs(with_silence.true_peak_dbtp - base.true_peak_dbtp) < 0.1 diff --git a/uv.lock b/uv.lock index d9abf69..79c2907 100644 --- a/uv.lock +++ b/uv.lock @@ -801,6 +801,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "hypothesis" +version = "6.156.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/83/8dbe89bdb8c6f25a7a52e7898af6d82fe35dfef08e5c702f6e33231ce6c6/hypothesis-6.156.6.tar.gz", hash = "sha256:96de02faefa3ce079873541da96f42595583bb001e8e4219294ed7d4501cc4cc", size = 476304, upload-time = "2026-07-10T20:56:49.96Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/dc/0c2a851f06c91d5ac9ef0f3b9615efc1ed650411d2eee23b6334f491c85e/hypothesis-6.156.6-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:caf6a93d011c10972da111c38ceb34ced20feaa8581e2b350c0655b022e27875", size = 747998, upload-time = "2026-07-10T20:56:16.311Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f8/59203ca978ab51595d12d6bc7e7a63300d7373431ab42ca3f1742e45db68/hypothesis-6.156.6-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:07f2bc9df1aeba80e12029c1618e2ee54abc440068c305d7075ffd6b85251843", size = 743073, upload-time = "2026-07-10T20:55:36.825Z" }, + { url = "https://files.pythonhosted.org/packages/68/d8/86a0023740434098d1b187a62bd5f99b198f098fb43e7fc58342283a8270/hypothesis-6.156.6-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7baca17f4803ad4aa151732326f3990baf54c3127df44aa872ac5bdf8a98a9a6", size = 1070169, upload-time = "2026-07-10T20:55:49.47Z" }, + { url = "https://files.pythonhosted.org/packages/9b/82/673453915fd0c67673f35a4876ba88f48c621335f293f3537d77b27d4286/hypothesis-6.156.6-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8083806645f84243aade727f4978185caaa0b7190af4318673999ee15fdbf424", size = 1121760, upload-time = "2026-07-10T20:55:53.502Z" }, + { url = "https://files.pythonhosted.org/packages/8a/c3/3a5557f52912f2fecc6ed59642dcf80dd8e89d0d9664502b68e23d66bf3d/hypothesis-6.156.6-cp310-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a922eedcd8618f9c2e17b79fa7b3f3f0b2df34e201958611cc3f0f46cca33c10", size = 1111440, upload-time = "2026-07-10T20:55:43.054Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/ae636d4ca7f996a1ccb4b3d5997d949f1718fba52b01559b3ab53b237b3f/hypothesis-6.156.6-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:5291bd33c4704d274d7c214d5c200e77f372a06644f5cbbe96dcbe53cb2fbf10", size = 1244944, upload-time = "2026-07-10T20:55:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/1e/79/c425d22d734be0268ca60d120c6296299e4220a1783cb1a4cc76232807bb/hypothesis-6.156.6-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:55f3ec50161b4a95bae63bff2b5166e45935b493013d3be30ede279bf6192318", size = 1288808, upload-time = "2026-07-10T20:56:06.249Z" }, + { url = "https://files.pythonhosted.org/packages/c5/3a/cc9f479d22cbdd36ddfc55a978378eddadd183b09339ebdb81be33bb18e7/hypothesis-6.156.6-cp310-abi3-win32.whl", hash = "sha256:e96570ca5cdd9a5f2ff9e80a6fb2fd5420ebf33b833d7de5b09b6ebb26a3eb6c", size = 634868, upload-time = "2026-07-10T20:55:37.959Z" }, + { url = "https://files.pythonhosted.org/packages/d6/89/2008d287289841a936456cb13443ca89d88da6e4527d611d482e9544164d/hypothesis-6.156.6-cp310-abi3-win_amd64.whl", hash = "sha256:32710718c22fe8c5571464e898bb87d282837b02617d6ad68130abf7cb4843cb", size = 640382, upload-time = "2026-07-10T20:55:30.634Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dc/b502504a972af7368b62e712268d310ffbc81d0ebfb31d5a9c60332a5063/hypothesis-6.156.6-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:afec0631a7a557acb50f665108b5d1d1123c21bc702d156a740db1cc33be4a7d", size = 749319, upload-time = "2026-07-10T20:55:35.708Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/a4867bc2b8b81d9b1648992fb7e4a732b3db480ff2d02df2c7b59189c812/hypothesis-6.156.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583a658162ee7e1a82155e3f146932a3a2269030294f3c83f5cf52fcaa0562c3", size = 743969, upload-time = "2026-07-10T20:55:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/2e/22/6ece4337e01c634594bb177009c049aa3133c151d9e397edccb6c3938567/hypothesis-6.156.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9600277defbfa769d8a6af264cbdff16294682c210c2d058ef39348fec16c0c", size = 1070874, upload-time = "2026-07-10T20:55:44.277Z" }, + { url = "https://files.pythonhosted.org/packages/05/00/5820a3b1fe264b23770f77dbb3ac0f6fdadd21b640624791a05950f934da/hypothesis-6.156.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7c3f067166bfc28b67f0042fc5fdcc87b3b58664da0c2f563fe544448b7ab3b", size = 1122191, upload-time = "2026-07-10T20:56:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/90/f8/0861ed15c96302577229334655e038e8c46e4b5b2a8cae971409a7e176e5/hypothesis-6.156.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f57842c1c5314839bdf4be5cff108589ff435cd7192c035dc48e6f14032915a5", size = 1246060, upload-time = "2026-07-10T20:55:41.834Z" }, + { url = "https://files.pythonhosted.org/packages/64/5c/63b60c6566eafac0b150f6acae5d1cbe0ed64dc294c863888eb87088a542/hypothesis-6.156.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c4a787c3af0035846034461792f2a62f1c43af850feb970a5b53a629d64b52fb", size = 1289510, upload-time = "2026-07-10T20:55:33.133Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/bfe57f182f51784549210903241057ae94784858117b965613089f5f89ad/hypothesis-6.156.6-cp310-cp310-win_amd64.whl", hash = "sha256:02accb187617ebaebb120da931f799a3cb0df7c38706f97f9d022441d4faf533", size = 640384, upload-time = "2026-07-10T20:56:26.939Z" }, + { url = "https://files.pythonhosted.org/packages/13/64/e4a0796190d8089e85f06731e21fdddd7e8edd3a4e562101527a048e21c4/hypothesis-6.156.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5baec7943a14d106e982121dd4f74cfc5ef45e37c17f94fe49338d3d1377f38c", size = 748988, upload-time = "2026-07-10T20:56:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a2/4a789b286cd2cced31992e1f683036b51dd6909b934ea007ffb43aa3a32f/hypothesis-6.156.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b5f519905ddeb10e23b8ba2c254541a5b1a8f146fe0551be94d972f4a77226f4", size = 743754, upload-time = "2026-07-10T20:56:09.113Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f7/3dd36c1c03d24ae3ffc3c5b0eca8cc4ae90c07abc320f76509eceb37019a/hypothesis-6.156.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c108655960b58ded3ca71b2dc5c69fb2ba7e9c723aeb6106facec3892d09087", size = 1070732, upload-time = "2026-07-10T20:56:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/57/51/befc4b816b471078034a875eb1ef69e0411ab84bcce582b4be173258785a/hypothesis-6.156.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c8d37bebb6924729bc0bbf5852689df568842948abe4d93dd0ad3377adf76fa", size = 1121988, upload-time = "2026-07-10T20:56:07.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/b7/a796f5e3e4b7cb911ff346008d49720296d1f4073490b8bc1cce6b3fbb07/hypothesis-6.156.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:74137bef6d502305c3648b2ed1a9bb4bc05fb1025e96b30a2c092204c40fe097", size = 1245596, upload-time = "2026-07-10T20:55:50.662Z" }, + { url = "https://files.pythonhosted.org/packages/37/65/849c4cba44a6f6cc888fd931124429b24180234ccc4883abab8cad5fcfcb/hypothesis-6.156.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5e0afdf79cceed20fcf0a9fb80d4064a9b2b53d4d4eecbac0e21208a13f5a31b", size = 1289172, upload-time = "2026-07-10T20:55:58.915Z" }, + { url = "https://files.pythonhosted.org/packages/13/03/7106a110df29eb631d66776e8aa8128f82f04a9dd2b6b22b612e6025e3a2/hypothesis-6.156.6-cp311-cp311-win_amd64.whl", hash = "sha256:84dc89caaf741a02f904ca7bd02b1af99650c75552868162290208aeecb70858", size = 640222, upload-time = "2026-07-10T20:56:10.396Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9f009005b9c796f4a40424484ac7e70847bc088456fd940a937f96bb4b6d/hypothesis-6.156.6-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a2a728b514fceb81e3f0464508911d5220fd74dadc3270f859427a686b60c4cf", size = 748844, upload-time = "2026-07-10T20:56:38.036Z" }, + { url = "https://files.pythonhosted.org/packages/02/2f/4d852bb8a9c73a68b18eca9b5b085285282122166e158f4d2a477639bfee/hypothesis-6.156.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7489b9a8f9df8227edd6c7cd8b9ccfab2483bab24da6a474c175973ca2294f58", size = 741936, upload-time = "2026-07-10T20:55:27.539Z" }, + { url = "https://files.pythonhosted.org/packages/74/89/b9968070ae042f9bf3149bb6ba6399d5f28f452e0fb7f638cafc69ff0b9a/hypothesis-6.156.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42760873d6db1069d6edbaa355a61b9078a9950259efcfc72fc695741d7db7cd", size = 1069749, upload-time = "2026-07-10T20:56:43.017Z" }, + { url = "https://files.pythonhosted.org/packages/00/a9/753806f5292b40aeab1d269e408e3a7e85be3c0d88828fb78ab4a34d6626/hypothesis-6.156.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4e66aaa7385538a5d617174d47c198ee807f06de99e282a67c6cb724c69340d", size = 1120983, upload-time = "2026-07-10T20:56:25.424Z" }, + { url = "https://files.pythonhosted.org/packages/85/88/8386d064d680be27e936eba94f1448bc93ef6fa05473ee5034139f1c4284/hypothesis-6.156.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:08796b674c0b31a5dd4119b2173823390055921588d13eb77324e861b00fd7f8", size = 1243911, upload-time = "2026-07-10T20:55:54.799Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8c/7524c1e5279e7728eb47c99f2357cbc5f08ae92e9bce49bf50118b53f9c9/hypothesis-6.156.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4ca8cc26ea2d31d22cf7710e92951cfaa921f0f8aa1b6db33a5176335f583a4f", size = 1287806, upload-time = "2026-07-10T20:56:02.176Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b3/c347ad913e1c5f2988956fe17826c0400b4ce470b973e6c248e97b6a0acf/hypothesis-6.156.6-cp312-cp312-win_amd64.whl", hash = "sha256:c3363d3fb8015594636689572510bb6090602d8e8e838a5693c2d52d3b5b09d8", size = 637679, upload-time = "2026-07-10T20:55:39.056Z" }, + { url = "https://files.pythonhosted.org/packages/70/5d/9583fe153573523dac27226c89e041a86ad4aeeae08c868160cbb93d39d2/hypothesis-6.156.6-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:59a8def90d9a5a9b67e1ac529e903a2363ceb6cf873c209da6b4284c5daab671", size = 749264, upload-time = "2026-07-10T20:56:46.118Z" }, + { url = "https://files.pythonhosted.org/packages/86/35/e4113d06769b544f0fb77ffea9195b598b4c56a298905c21fd47c4eed388/hypothesis-6.156.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c574c3224563d730848bc5d1ef1683c4f83993400c0167899fe328f4bfcd4725", size = 742095, upload-time = "2026-07-10T20:56:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/d8/5c/a47666ede10384e8978722cade7ab96a42df71d2ab577317092d0fed7c8a/hypothesis-6.156.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01bb8270c46b3ef53b0c2d23ff613ea506d609d06f936d823ea57c58b66b05f7", size = 1069917, upload-time = "2026-07-10T20:56:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/79/93/75f6057dadd9dc0134f37c08d5d14d04d3cd7374debbcb0cc4569c6712f1/hypothesis-6.156.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4ea6559c13606e13b645927f2e0906e52b5ac5d99b40d3abaaeb2e8c7ceeb75", size = 1121204, upload-time = "2026-07-10T20:55:52.008Z" }, + { url = "https://files.pythonhosted.org/packages/62/87/308efef08bc60d1e673d035e8ca8e9663f4b6b3ba519c3cdebf6583c2b76/hypothesis-6.156.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2d47054d0230f0dd9b6868fc030126c7a6c25527144272ff376cc4e9c39f7540", size = 1244168, upload-time = "2026-07-10T20:55:40.288Z" }, + { url = "https://files.pythonhosted.org/packages/3b/66/de8fff5bd9a40a4056dafbe7f904887ef12632282bbbac90f1977c30dd3b/hypothesis-6.156.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:050c8c0815f88d47dd0875a92698d20d61639b7b721ee043a6d687c7f14ff7d8", size = 1288127, upload-time = "2026-07-10T20:56:00.541Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8d/794fb26e1fd3ff004978f8f18b7aa7e1c2270ba72e1f977b987a812064f8/hypothesis-6.156.6-cp313-cp313-win_amd64.whl", hash = "sha256:f0d73edab7b8a0051b3634f2d04d62b7e7282f8f274963b11188ee4957d672ef", size = 637954, upload-time = "2026-07-10T20:56:33.35Z" }, + { url = "https://files.pythonhosted.org/packages/59/9c/b94f3a31665527b6181616b72990fcf8d6d5fa82b4187aab104ab5f548f0/hypothesis-6.156.6-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:8893b4da90e06828846c1b100c3414a7729d047a020d854c0899ae9339df0e70", size = 749575, upload-time = "2026-07-10T20:55:29.371Z" }, + { url = "https://files.pythonhosted.org/packages/21/74/dcf695f79f526543ae5d0f8c1325508e9fe990a996c0e0853129a9a5d81d/hypothesis-6.156.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7fc0b7df9b28d028e4cc295b2ac8fbbbc22e090a23382c92fff5e37696be74f", size = 744351, upload-time = "2026-07-10T20:56:36.46Z" }, + { url = "https://files.pythonhosted.org/packages/11/d3/5bff4c55c6995a6c43f66ec8e5866b56e34f03837fd0be0e4922f3bab168/hypothesis-6.156.6-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38ed3178526382d392d04ad699ad7a2e53845e521a09d40f1cbbc1e1ff63ba48", size = 1070916, upload-time = "2026-07-10T20:56:19.256Z" }, + { url = "https://files.pythonhosted.org/packages/ba/af/5ed42117a69221ea118caaff933d8212039a0ac0bc15afa915635f13984c/hypothesis-6.156.6-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5ad94e28aabf4db0d479297d43b8a2a01e7caaa9bdfccfdac7a4a3717e05b993", size = 1122625, upload-time = "2026-07-10T20:56:14.758Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d3/02499badc6e3f3e980941021edf5fd780c895d8d08c9015e78516340ed83/hypothesis-6.156.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:983a5cfd955994bffc7eb02976241f7a1f3c2d94dbc3389430c45858fa5c1ae0", size = 640823, upload-time = "2026-07-10T20:55:45.461Z" }, +] + [[package]] name = "identify" version = "2.6.19" @@ -1911,6 +1965,7 @@ analysis = [ { name = "librosa" }, ] dev = [ + { name = "hypothesis" }, { name = "pre-commit" }, { name = "pyloudnorm" }, { name = "pytest" }, @@ -1942,6 +1997,7 @@ requires-dist = [ { name = "essentia", specifier = "==2.1b6.dev1389" }, { name = "fastmcp", specifier = ">=3.2" }, { name = "ffmpeg-progress-yield", specifier = ">=0.7" }, + { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.0" }, { name = "librosa", marker = "extra == 'analysis'", specifier = ">=0.11.0" }, { name = "matchering", marker = "extra == 'matching'", specifier = ">=2.0" }, { name = "numpy", specifier = ">=2.0" }, @@ -2847,6 +2903,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] + [[package]] name = "soundfile" version = "0.13.1"